diff --git a/crates/eureka-state/src/bin/huginn/import.rs b/crates/eureka-state/src/bin/huginn/import.rs index be47df3..67603ea 100644 --- a/crates/eureka-state/src/bin/huginn/import.rs +++ b/crates/eureka-state/src/bin/huginn/import.rs @@ -15,7 +15,7 @@ use eureka_state::hold; use huginn_mind::eureka_pipeline::{PipelineDocument, PipelineKind, PipelineRef, Short, Slug}; use huginn_mind::{ AdmissionFacts, Faculty, HuginnMindRequest, HuginnMindResponse, ImportBatch, ImportFault, ImportPlan, PipelineAdmissionBatch, PipelineAdmissionOutcome, - PipelineDocumentSummary, PipelineFacts, PipelineDocumentView, PipelinePageItems, PipelineProvenance, PipelineSelectionPage, inert_in_window, plan, + PipelineDocumentSummary, PipelineDocumentView, PipelineFacts, PipelinePageItems, PipelineProvenance, PipelineSelectionPage, inert_in_window, plan, }; use crate::config::{Settings, TOOL}; @@ -207,7 +207,9 @@ fn headers(ask: &dyn Fn(Selection) -> Result, campa /// from this source and are no planned document: writes in the import window (ruling /// `import-window-writes-ruled`: the import stops at a collision, and only a collision). They are /// left where they are; they cannot be a planned document's key, and the admission of each later -/// batch still refuses any replay that meets one. +/// batch still refuses any replay that meets one. A record whose key a later batch carries is no window write: the +/// tool keeps it planned and refuses the resume, and the daemon, which cannot see the later batch, +/// refuses that batch as an identity collision when it arrives. fn window_writes<'a>(root: impl Iterator)> + Clone, planned: &BTreeSet<&str>, source: &Slug) -> BTreeSet<&'a str> { let began = root.clone().filter(|(_, _, admission, _)| admission.provenance.imported_from.as_ref().is_some_and(|from| from.instance == *source)).map(|(_, _, admission, _)| admission.ordinal).min(); root.filter(|(kind, key, admission, subject)| inert_in_window(*kind, subject.map(|subject| (subject.kind, subject.id.0.as_str())), |id| planned.contains(id)) && admission.provenance.imported_from.is_none() && !planned.contains(key) && began.is_some_and(|began| admission.ordinal > began)) diff --git a/crates/huginn-mind/src/move_tests.rs b/crates/huginn-mind/src/move_tests.rs index 24f60e2..bc77204 100644 --- a/crates/huginn-mind/src/move_tests.rs +++ b/crates/huginn-mind/src/move_tests.rs @@ -638,6 +638,23 @@ fn an_import_resumes_past_the_resolution_of_a_follow_up_the_receiver_wrote_after assert_eq!(refusal(import_from_kiko(&mut recv, vec![question("Q2", &["A", "B"], "A")])), MindRefusal::ImportOutsideCampaign { root: CAMPAIGN.into() }); } +/// The daemon cannot see a later batch: a follow-up the receiver wrote natively and closed, whose +/// key a later batch replays, is inert to it now. The batch then meets the key and is refused as a +/// collision, so the window never reaches the receiver's own record. +#[test] +fn a_native_follow_up_a_later_batch_replays_is_refused_when_that_batch_arrives() { + let q1 = id("question", "Q1"); + let mut recv = receiver(); + committed(import_from_kiko(&mut recv, vec![campaign(&[REPO]), question("Q1", &["A", "B"], "A")])); + let native = follow_up("FU-L", r(K::Question, &q1)); + let native_ref = r(K::FollowUp, &pipeline_key(&native).unwrap()); + committed(admit(&mut recv, vec![native.clone()])); + committed(admit(&mut recv, vec![resolution(native_ref, ResolutionOutcome::Recorded { reason: "done".into() })])); + let before = head(&recv); + assert!(matches!(refusal(import_from_kiko(&mut recv, vec![native])), MindRefusal::IdentityCollision { .. })); + assert_eq!(head(&recv), before); +} + #[test] fn an_import_never_cites_or_resolves_a_follow_up_the_receiver_wrote() { let q1 = id("question", "Q1");