Desk door: the answer path honors the listing gate (a hidden fork answers fork_unknown, stores nothing)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f3d0280535
commit
227ed9e350
3 changed files with 43 additions and 2 deletions
|
|
@ -211,6 +211,13 @@ fn rig(peer_uid: u32) -> Rig {
|
|||
}
|
||||
|
||||
impl Rig {
|
||||
/// The Mind lists the seeded fork, on its own door, so a non-decider reaches it.
|
||||
fn list_the_fork(&mut self) {
|
||||
let listing = PipelineDocument::DeskListing(huginn_mind::eureka_pipeline::PipelineDeskListing { campaign: slug("eureka-state"), listed: short(FORK), reason: "Listed.".into() });
|
||||
let admitted = self.daemon.handle(HuginnMindRequest::Admit(batch(INSTANCE, vec![listing])), now()).answered();
|
||||
assert!(matches!(admitted, HuginnMindResponse::Admit(huginn_mind::PipelineAdmissionOutcome::Committed { .. })), "{admitted:?}");
|
||||
}
|
||||
|
||||
/// Runs `client` on its own thread while this one serves the door, then drains what is still waiting.
|
||||
fn serving<T: Send>(&mut self, client: impl FnOnce(&Path) -> T + Send) -> T {
|
||||
let path = self.path.clone();
|
||||
|
|
@ -607,6 +614,7 @@ mod door_smoke {
|
|||
#[test]
|
||||
fn deciders_ride_each_request() {
|
||||
let mut rig = rig(my_uid());
|
||||
rig.list_the_fork();
|
||||
let (before, hash) = (rig.state(), rig.hash.clone());
|
||||
let out = rig.serving(|path| {
|
||||
let ask = |id: &str, who: &str, deciders: &[&str], request_id: &str| {
|
||||
|
|
@ -782,6 +790,7 @@ mod door_smoke {
|
|||
#[test]
|
||||
fn a_mind_refusal_and_a_stored_refusal_are_typed_replies() {
|
||||
let mut rig = rig(my_uid());
|
||||
rig.list_the_fork();
|
||||
let (before, hash) = (rig.state(), rig.hash.clone());
|
||||
let mut foreign = answer("x", DeskAuthority::Member, &["x"], wire(&hash, R1));
|
||||
let DeskRequest::Answer { mind, .. } = &mut foreign else { unreachable!() };
|
||||
|
|
|
|||
|
|
@ -549,10 +549,12 @@ impl<S: MindStore> Mind<S> {
|
|||
};
|
||||
let image = self.image()?;
|
||||
let docs = Docs::at(image, image.head);
|
||||
// A fork this reader may not see is unshowable here too, by the read side's own `visible`: the answer path
|
||||
// must not tell a hidden fork from a missing one, nor store a refusal for it.
|
||||
// A request already stored replays whatever the list is now: the lookup is `answer_desk`'s,
|
||||
// asked here only so the gate does not shadow it.
|
||||
let unshowable = bound_answer(&docs, &subject, &sent.request_id).is_none()
|
||||
&& matches!(docs.find(PipelineKind::Question, &sent.fork.0), Some(PipelineDocument::Question(question)) if !showable(question, deciders));
|
||||
&& matches!(docs.find(PipelineKind::Question, &sent.fork.0), Some(PipelineDocument::Question(question)) if !showable(question, deciders) || !reads::visible(&docs, &sent.fork.0, deciders.contains(&subject)));
|
||||
if unshowable {
|
||||
return Ok(DeskResponse::NotStored(fork_unknown()));
|
||||
}
|
||||
|
|
@ -616,6 +618,11 @@ mod tests {
|
|||
fn desk() -> Mind<MemoryStore> {
|
||||
let mut mind = seeded();
|
||||
committed(admit(&mut mind, vec![question_n(1), question_n(2)]));
|
||||
// Listed, so a reader who is not a decider reaches them (an unlisted fork is the reads' own tests').
|
||||
for fork in [FORK, FORK2] {
|
||||
let listing = D::DeskListing(eureka_pipeline::PipelineDeskListing { campaign: slug("eureka-state"), listed: s(fork), reason: "Listed.".into() });
|
||||
committed(admit(&mut mind, vec![listing]));
|
||||
}
|
||||
mind
|
||||
}
|
||||
|
||||
|
|
@ -1491,6 +1498,7 @@ mod tests {
|
|||
assert_eq!(shown_hash(&question).0, her_hash, "{name}: the card's hash");
|
||||
let mut mind = seeded();
|
||||
committed(admit(&mut mind, vec![D::Question(question)]));
|
||||
committed(admit(&mut mind, vec![D::DeskListing(eureka_pipeline::PipelineDeskListing { campaign: slug("eureka-state"), listed: s(FORK), reason: "Listed.".into() })]));
|
||||
let list: Vec<&str> = card["deciders"].as_array().unwrap().iter().map(|who| who.as_str().unwrap()).collect();
|
||||
if card["status"] == "closed" {
|
||||
close(&mut mind, FORK, "RQ1", "no");
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ fn card_of(docs: &Docs<'_>, key: &str, order: u64, question: &PipelineQuestion,
|
|||
/// Whether a reader may see this fork at all: a decider sees every showable fork (they must, to answer
|
||||
/// it); anyone else only while the Mind's `desk_listing` for it is in force (derived from the
|
||||
/// resolutions on every read; no status is stored). A fork nobody may see reads as a missing one.
|
||||
fn visible(docs: &Docs<'_>, key: &str, decider: bool) -> bool {
|
||||
pub(super) fn visible(docs: &Docs<'_>, key: &str, decider: bool) -> bool {
|
||||
decider || {
|
||||
let (root, label) = root_and_local(key);
|
||||
let listing = format!("{root}:desk_listing:{label}");
|
||||
|
|
@ -1103,6 +1103,30 @@ mod tests {
|
|||
assert_eq!(head(&mind), before);
|
||||
}
|
||||
|
||||
/// The answer path is no oracle: a non-decider answering a hidden fork gets the bytes of answering
|
||||
/// one that never existed, and nothing is stored. A listed fork still stores the NotADecider refusal.
|
||||
#[test]
|
||||
fn a_non_decider_answering_a_hidden_fork_learns_nothing_and_stores_nothing() {
|
||||
let mut mind = desk(2);
|
||||
let hash = hash_of(&mind, &fork(1));
|
||||
let before = (head(&mind), stored_answers(&mind));
|
||||
let hidden = answer(&mut mind, DAN, &[ANNA, BEN], &fork(1), &hash, None);
|
||||
let missing = answer(&mut mind, DAN, &[ANNA, BEN], &fork(99), &hash, None);
|
||||
unknown(hidden.clone());
|
||||
assert_eq!(rmp_serde::to_vec_named(&hidden).unwrap(), rmp_serde::to_vec_named(&missing).unwrap());
|
||||
assert_eq!((head(&mind), stored_answers(&mind)), before, "nothing stored");
|
||||
list_it(&mut mind, 1);
|
||||
let listed = answer(&mut mind, DAN, &[ANNA, BEN], &fork(1), &hash, None);
|
||||
let DeskResponse::Answered { answer: refused, .. } = listed else { panic!("a listed fork stores the refusal: {listed:?}") };
|
||||
assert_eq!(refused.code, Some(ForkAnswerCode::NotADecider));
|
||||
assert_eq!(stored_answers(&mind), before.1 + 1);
|
||||
}
|
||||
|
||||
fn stored_answers(mind: &Mind<MemoryStore>) -> usize {
|
||||
let image = mind.image().unwrap();
|
||||
Docs::at(image, image.head).of_kind(PipelineKind::DeskAnswer).count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_desk_door_cannot_admit_a_listing() {
|
||||
let mut mind = desk(1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue