diff --git a/packages/heimdall-consent/README.md b/packages/heimdall-consent/README.md index fb446e64..e38c8318 100644 --- a/packages/heimdall-consent/README.md +++ b/packages/heimdall-consent/README.md @@ -14,7 +14,7 @@ Entry points: `src/lib.rs`; golden vectors `tests/vectors/consent_golden.csv`, r | Ruling | Doc | Crate | |---|---|---| | Timestamps are Unix ms UTC, signed 64-bit | L98-99; `received_at` is collector-assigned, L75 | `i64` on `accepted_at`, `received_at`, `terms_accepted_at` | -| Ids are non-empty opaque strings, `event_id` verbatim; every version field (`notice_version`, the `@N` of a sample id, `terms_version`) is a positive integer, no leading zeros, no sign, at most 4294967295 (Heimdall ruling 2026-11-07, one rule) | L100-103 | `check_version` is the single rule; `check_sample_id` and `MemberTerms::validate` call it; `notice_version: u32` with `validate()` refusing 0; golden vector `newcomer` carries `terms_version` 4294967295 | +| Ids are non-empty opaque strings, `event_id` verbatim; every version field (`notice_version`, the `@N` of a sample id, `terms_version`) is a positive integer, no leading zeros, no sign, at most 4294967295 (Heimdall ruling 2026-11-07, one rule; doc line "Version rule" under Representation, Heimdall PR #6 commit d6a8d42) | L100-103 | `check_version` is the single rule; `check_sample_id` and `MemberTerms::validate` call it; `notice_version: u32` with `validate()` refusing 0; golden vector `newcomer` carries `terms_version` 4294967295, and case `one-version-rule` pins 4294967295 accepted, 4294967296 and `01` refused for `notice_version`, the sample id `@N` and `terms_version` | | Key parts joined with U+001F; empty part or part containing it refused | L104-105 | `key()` | | Tombstone is the exited `Answer` with `text_hash` (32 bytes); HMAC-SHA-256 keyed by a per-member salt held by the member-side service; the collector cannot compute it | L74, L132-152, L186-193 | `Answer.text_hash`; `redacted([u8; 32])` takes the service's output and refuses an answer with no `text` (L148-149); `validate()` requires 32 bytes and `exited` | | A reply with no established notice version or scope is stored with both absent, writes no acceptance, is excluded from every use | L87-94 | `notice_version`/`scope` optional and absent together (`validate()`); vector case `no-scope` | diff --git a/packages/heimdall-consent/tests/golden.rs b/packages/heimdall-consent/tests/golden.rs index 01e9477b..615d5cb4 100644 --- a/packages/heimdall-consent/tests/golden.rs +++ b/packages/heimdall-consent/tests/golden.rs @@ -68,7 +68,7 @@ fn golden_vectors() { _ => cases.push((f[0], vec![f])), } } - assert_eq!(cases.len(), 9, "fixture shrank"); + assert_eq!(cases.len(), 10, "fixture shrank"); for (name, rows) in cases { let dir = tempfile::tempdir().unwrap(); let mut cache = new_cache(&dir); @@ -185,6 +185,29 @@ fn golden_vectors() { assert_eq!((a.notice_version, Some(a.scope), a.accepted_at), (nv.unwrap(), scope(sc), ts), "{ctx}"); assert_eq!(a.event_id, payload, "{ctx}"); } + "version_ok" | "version_refused" => { + // member column names the field; payload is the version text as it arrives + let ok = match member { + "notice_version" => check_version(payload).is_ok_and(|v| { + NoticeAcceptance { + member_id: "m".into(), + experiment_id: room.into(), + notice_version: v, + scope: Scope::Train, + accepted_at: 1, + event_id: "e".into(), + } + .validate() + .is_ok() + }), + "sample_id" => check_sample_id(&format!("s@{payload}")).is_ok(), + "terms_version" => { + MemberTerms { member_id: "m".into(), terms_version: payload.into(), terms_accepted_at: 1 }.validate().is_ok() + } + other => panic!("unknown version field {other}"), + }; + assert_eq!(ok, op == "version_ok", "{ctx} field {member} version {payload}"); + } other => panic!("unknown op {other}"), } } diff --git a/packages/heimdall-consent/tests/vectors/consent_golden.csv b/packages/heimdall-consent/tests/vectors/consent_golden.csv index 1323aef8..4343b0b2 100644 --- a/packages/heimdall-consent/tests/vectors/consent_golden.csv +++ b/packages/heimdall-consent/tests/vectors/consent_golden.csv @@ -6,6 +6,7 @@ # reply_rejected: validate must refuse the answer (nothing stored). answer_revoked: stores an exited answer with no text; payload is the tombstone hash (empty means none). expect_winner: the stored answer's event_id and text must equal event and payload. # Replacement at one key: the later reply is the greater (received_at, event_id), event_id compared bytewise; input order never decides (doc L78-81 and Heimdall PR #6 comment 759). # terms: member field terms_version (payload) and terms_accepted_at (ts). expect_terms: the stored pair for that member reads back as payload and ts; expect_no_terms: the member has none. expect_*: payload is what must hold. +# version_ok / version_refused: the member column names the field (notice_version, sample_id, terms_version), payload is the version text; a sample id is checked as s@. One rule for all three: positive integer, no leading zeros, at most 4294967295 (Heimdall PR #6 cafdc52). # Expected values are written by hand from docs/answer-collector-consent.md (Heimdall PR #6, doc commit 4e99bfa), not computed by the crate. versioned-key,reply,m1,room1,1,train,s1@1,first,100,e1 versioned-key,reply,m1,room1,1,train,s1@2,second,200,e2 @@ -66,3 +67,12 @@ tie-greater-last,reply,m15,room1,,,s1@1,long-aa,500,aa tie-greater-last,reply,m15,room1,,,s1@1,short-z,500,z tie-greater-last,expect_winner,m14,room1,,,s1@1,lower-a,,a tie-greater-last,expect_winner,m15,room1,,,s1@1,short-z,,z +one-version-rule,version_ok,notice_version,room1,,,,4294967295,,version_ok-notice_version-4294967295 +one-version-rule,version_refused,notice_version,room1,,,,4294967296,,version_refused-notice_version-4294967296 +one-version-rule,version_refused,notice_version,room1,,,,01,,version_refused-notice_version-01 +one-version-rule,version_ok,sample_id,room1,,,,4294967295,,version_ok-sample_id-4294967295 +one-version-rule,version_refused,sample_id,room1,,,,4294967296,,version_refused-sample_id-4294967296 +one-version-rule,version_refused,sample_id,room1,,,,01,,version_refused-sample_id-01 +one-version-rule,version_ok,terms_version,room1,,,,4294967295,,version_ok-terms_version-4294967295 +one-version-rule,version_refused,terms_version,room1,,,,4294967296,,version_refused-terms_version-4294967296 +one-version-rule,version_refused,terms_version,room1,,,,01,,version_refused-terms_version-01