Desk feed door: the door withholds account ids from non-deciders (proposed shapes, CultLib change owed)
Persona's ruling (follow-up chat-mind-20261111T150417226Z-t167): a reader is a decider when subject is in the call's deciders; any other reader gets no decider or answerer ids, only counts, and Huginn does it because the card is opaque to Bifrost. CultLib's closed card (deciders minItems 1, no count) and non-null answerer cannot carry it yet, so the doc names the proposal to Libby and the fixtures add clearly synthetic card fragments and a proposed non-decider record, with a test that no account id appears in a non-decider's bytes and a decider keeps them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
da90d7e74e
commit
d08b8880ff
9 changed files with 174 additions and 13 deletions
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use cultnet_rs::{CultNetMessage, CultNetWireContract, encode_cultnet_message_to_vec, encode_frame};
|
||||
use huginn_mind::MindRefusal;
|
||||
use huginn_mind::desk_wire::{
|
||||
|
|
@ -104,6 +106,106 @@ fn record() -> ForkRecord {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- PROPOSED reader views (ruling: the door withholds account ids from readers who are not
|
||||
// deciders, and returns counts). CultLib's card and desk_answer cannot carry this yet (card
|
||||
// `deciders` has minItems 1 and no count key; `answerer` is a required non-null id), so these are
|
||||
// fragments in the shape proposed to Libby (follow-up `cultlib-fork-card-reader-counts`), built from
|
||||
// mirror types that exist only here. They are not Huginn's output until that contract lands and
|
||||
// `cut-desk-feed-listing` builds it. ----
|
||||
|
||||
/// The two card keys the proposal changes, as a fragment: a decider gets the ids, anyone else `[]`
|
||||
/// and the count. Not a card.
|
||||
#[derive(Serialize)]
|
||||
struct CardViewers {
|
||||
deciders: Vec<String>,
|
||||
decider_count: u64,
|
||||
}
|
||||
|
||||
fn card_viewers(decider: bool) -> CardViewers {
|
||||
let ids = vec![READER.to_string(), OTHER_DECIDER.to_string()];
|
||||
CardViewers { decider_count: ids.len() as u64, deciders: if decider { ids } else { Vec::new() } }
|
||||
}
|
||||
|
||||
/// `gamecult.desk_answer.v0` as proposed for a reader who is not a decider: `answerer` nil, key kept.
|
||||
#[derive(Serialize)]
|
||||
struct WithheldAnswer {
|
||||
#[serde(rename = "schemaVersion")]
|
||||
schema_version: AnswerSchema,
|
||||
answer_id: String,
|
||||
fork_id: String,
|
||||
outcome: String,
|
||||
of: Option<String>,
|
||||
retracts: Option<String>,
|
||||
code: Option<huginn_mind::desk_wire::ForkAnswerCode>,
|
||||
options_hash: String,
|
||||
choice: Option<String>,
|
||||
note: Option<String>,
|
||||
answered_at: String,
|
||||
received_at: String,
|
||||
claim_in_future: bool,
|
||||
answerer: Option<String>,
|
||||
authority: DeskAuthority,
|
||||
request_id: String,
|
||||
}
|
||||
|
||||
/// `gamecult.fork.record.v0` as proposed for a reader who is not a decider: every `answerer` nil and
|
||||
/// `answerer_count`, the number of distinct answerers, in their place.
|
||||
#[derive(Serialize)]
|
||||
struct WithheldRecord {
|
||||
#[serde(rename = "schemaVersion")]
|
||||
schema_version: RecordSchema,
|
||||
fork_id: String,
|
||||
options_hash: String,
|
||||
counted_answer_id: Option<String>,
|
||||
answerer_count: u64,
|
||||
answers: Vec<WithheldAnswer>,
|
||||
}
|
||||
|
||||
fn withheld(record: ForkRecord) -> WithheldRecord {
|
||||
let mut answerers: Vec<&str> = record.answers.iter().map(|answer| answer.answerer.as_str()).collect();
|
||||
answerers.sort_unstable();
|
||||
answerers.dedup();
|
||||
let answerer_count = answerers.len() as u64;
|
||||
WithheldRecord {
|
||||
schema_version: record.schema_version,
|
||||
fork_id: record.fork_id,
|
||||
options_hash: record.options_hash,
|
||||
counted_answer_id: record.counted_answer_id,
|
||||
answerer_count,
|
||||
answers: record
|
||||
.answers
|
||||
.into_iter()
|
||||
.map(|answer| WithheldAnswer {
|
||||
schema_version: answer.schema_version,
|
||||
answer_id: answer.answer_id,
|
||||
fork_id: answer.fork_id,
|
||||
outcome: answer.outcome,
|
||||
of: answer.of,
|
||||
retracts: answer.retracts,
|
||||
code: answer.code,
|
||||
options_hash: answer.options_hash,
|
||||
choice: answer.choice,
|
||||
note: answer.note,
|
||||
answered_at: answer.answered_at,
|
||||
received_at: answer.received_at,
|
||||
claim_in_future: answer.claim_in_future,
|
||||
answerer: None,
|
||||
authority: answer.authority,
|
||||
request_id: answer.request_id,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The proposed fragments by reader kind: (file stem, bytes).
|
||||
fn reader_views() -> Vec<(&'static str, Vec<u8>)> {
|
||||
vec![
|
||||
("card-viewers-decider.synthetic", rmp_serde::to_vec_named(&card_viewers(true)).unwrap()),
|
||||
("card-viewers-non-decider.synthetic", rmp_serde::to_vec_named(&card_viewers(false)).unwrap()),
|
||||
("record-non-decider.proposed", rmp_serde::to_vec_named(&withheld(record())).unwrap()),
|
||||
]
|
||||
}
|
||||
|
||||
/// The named MessagePack payload a CultNet operation message carries (base64 in the envelope).
|
||||
fn payload(message: &CultNetMessage) -> Vec<u8> {
|
||||
use base64::Engine;
|
||||
|
|
@ -175,6 +277,10 @@ fn fixtures() -> Vec<(String, Vec<u8>)> {
|
|||
let whole = rmp_serde::to_vec_named(&record()).unwrap();
|
||||
files.push(("record-whole.payload.msgpack".into(), whole.clone()));
|
||||
files.push(("record-whole.payload.hex".into(), hex(&whole).into_bytes()));
|
||||
for (name, bytes) in reader_views() {
|
||||
files.push((format!("{name}.payload.msgpack"), bytes.clone()));
|
||||
files.push((format!("{name}.payload.hex"), hex(&bytes).into_bytes()));
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
|
|
@ -224,3 +330,27 @@ fn every_account_id_in_the_fixtures_is_a_lowercase_d_format_uuid() {
|
|||
assert!(!account_id(shape), "{shape} is not an account id");
|
||||
}
|
||||
}
|
||||
|
||||
fn holds(bytes: &[u8], text: &str) -> bool {
|
||||
bytes.windows(text.len()).any(|window| window == text.as_bytes())
|
||||
}
|
||||
|
||||
/// A reader who is not a decider gets counts and no account id at all: neither UUID appears
|
||||
/// anywhere in its fragments' bytes. A decider gets every id. (Proposed shapes; the build cut pins
|
||||
/// the same on Huginn's real list and read responses.)
|
||||
#[test]
|
||||
fn a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids() {
|
||||
let views = reader_views();
|
||||
let bytes = |name: &str| views.iter().find(|(stem, _)| *stem == name).unwrap().1.clone();
|
||||
for name in ["card-viewers-non-decider.synthetic", "record-non-decider.proposed"] {
|
||||
let view = bytes(name);
|
||||
assert!(!holds(&view, READER) && !holds(&view, OTHER_DECIDER), "{name} carries an account id");
|
||||
}
|
||||
let non_decider = card_viewers(false);
|
||||
assert_eq!((non_decider.deciders.len(), non_decider.decider_count), (0, 2));
|
||||
assert_eq!(withheld(record()).answerer_count, 2);
|
||||
let decider = bytes("card-viewers-decider.synthetic");
|
||||
assert!(holds(&decider, READER) && holds(&decider, OTHER_DECIDER));
|
||||
let whole = rmp_serde::to_vec_named(&record()).unwrap();
|
||||
assert!(holds(&whole, READER) && holds(&whole, OTHER_DECIDER), "a decider's record keeps the answerer ids");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ lands (see `docs/desk-feed-door.md`).
|
|||
| `unavailable.response` | Huginn to Bifrost | `Refused` `Unavailable` (Bifrost: 503 for this Mind) |
|
||||
| `failure.response` | Huginn to Bifrost | operation failure, `payloadSchema` `gamecult.cultnet.operation_failure.v1` (Bifrost: 503) |
|
||||
| `error.response` | Huginn to Bifrost | CultNet `Error` message, no payload (Bifrost: 503) |
|
||||
| `record-whole` | fragment | `gamecult.fork.record.v0` with a counted answer and a revision, answerer ids and notes raw, as the door sends them (Bifrost masks for a reader who is not a decider) |
|
||||
| `record-whole` | fragment | `gamecult.fork.record.v0` with a counted answer and a revision, answerer ids and notes whole: what the door sends today, and a decider's view |
|
||||
| `record-non-decider.proposed` | fragment | PROPOSED non-decider record: every `answerer` nil, `answerer_count` 2. Needs CultLib change `cultlib-fork-card-reader-counts`; not Huginn output yet |
|
||||
| `card-viewers-decider.synthetic` | fragment | SYNTHETIC: only `{deciders: [two ids], decider_count: 2}`. Not a card |
|
||||
| `card-viewers-non-decider.synthetic` | fragment | SYNTHETIC: only `{deciders: [], decider_count: 2}`. Not a card |
|
||||
|
||||
No case carries a card (`gamecult.fork.card.v0`). The card's bytes wait on CultLib's re-pinned card
|
||||
vectors for `raised_at`, so no entry-bearing list page or read is pinned yet.
|
||||
|
|
|
|||
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
82a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464ad646563696465725f636f756e7402
|
||||
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
‚¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4ddecider_count
|
||||
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
82a8646563696465727390ad646563696465725f636f756e7402
|
||||
|
|
@ -0,0 +1 @@
|
|||
‚¨deciders<EFBFBD>decider_count
|
||||
1
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7402a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032
|
||||
BIN
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack
vendored
Normal file
BIN
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack
vendored
Normal file
Binary file not shown.
Loading…
Reference in a new issue