From 35865bb934ff7acc9cf605f6f2e9f84113af8e96 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 17 Jul 2026 14:24:42 +0200 Subject: [PATCH] =?UTF-8?q?test(registry):=20RED=20pin=20=E2=80=94=20twin-?= =?UTF-8?q?store=20identity=20index=20must=20converge,=20not=20grow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Executable spec for the audit follow-up on the identity-index cycle: once one full repair walk has run over a same-identity-twin store (two blueprints sharing one identity id, distinct content ids), a further scan-triggering lookup must leave blueprint_identity_index.jsonl's line count unchanged while the twin identity still resolves. RED against the current per-entry snapshot comparison (appends one alternating line per scan-triggering lookup while twins coexist). refs #191 --- crates/aura-registry/src/lib.rs | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/crates/aura-registry/src/lib.rs b/crates/aura-registry/src/lib.rs index 2e2fc3b..e3f08e7 100644 --- a/crates/aura-registry/src/lib.rs +++ b/crates/aura-registry/src/lib.rs @@ -2071,6 +2071,59 @@ mod tests { assert_eq!(found.as_deref(), Some(blueprint_json.as_str())); } + /// Property (#191 audit): the identity-index sidecar CONVERGES for a + /// same-identity-twin store — two blueprints sharing one identity id but + /// with distinct content ids (they differ only in identity-blind debug + /// symbols). Once one full repair walk has run, a further scan-triggering + /// lookup appends nothing (the file does not grow) while resolution + /// answers stay unchanged. The pre-fix repair pass compares each walked + /// entry against a mid-walk-stale snapshot, so with twins the snapshot can + /// never equal both entries and every full-scan lookup appends one more + /// (alternating) line — unbounded growth. + #[test] + fn identity_index_twin_store_converges_and_stops_growing() { + let reg = Registry::open(temp_family_dir("identity_twin_converges")); + let resolve = |t: &str| aura_std::std_vocabulary(t); + + // Two same-identity twins: the one-node Bias composite, built with + // different debug names. Debug symbols are identity-blind, so the two + // share one identity id but hash to distinct content ids. + let (_json_a, content_a, identity) = seed_blueprint(®, &bias_fixture()); + let twin = aura_engine::Composite::new( + "fixture_twin", + vec![aura_std::Bias::builder().named("b_twin").into()], + vec![], + vec![], + vec![], + ); + let (_json_b, content_b, identity_twin) = seed_blueprint(®, &twin); + assert_eq!(identity, identity_twin, "twins must share one identity id"); + assert_ne!(content_a, content_b, "twins must have distinct content ids"); + + // One converging walk: an absent identity always scans the whole + // store (only a scan proves absence), repairing the index. + assert_eq!(reg.find_blueprint_by_identity("0000", &resolve).expect("io ok"), None); + let converged_lines = fs::read_to_string(reg.identity_index_path()) + .map(|t| t.lines().count()) + .unwrap_or(0); + + // A further scan-triggering lookup over the CONVERGED index must not + // append another line (pre-fix: it appends one alternating twin line). + assert_eq!(reg.find_blueprint_by_identity("0000", &resolve).expect("io ok"), None); + let after_lines = fs::read_to_string(reg.identity_index_path()) + .map(|t| t.lines().count()) + .unwrap_or(0); + assert_eq!( + after_lines, converged_lines, + "a converged twin index must not grow on a further scan-triggering lookup", + ); + + // Resolution answers stay unchanged: the twin identity still resolves + // to a valid stored blueprint for that identity (preservation pin). + let found = reg.find_blueprint_by_identity(&identity, &resolve).expect("io ok"); + assert!(found.is_some(), "the twin identity still resolves to a stored blueprint"); + } + /// Property (#246): a campaign axis naming a BOUND param (not an open /// one) is checked exactly like an open-param axis — a kind-correct axis /// over the bound path is fault-free, and a kind-mismatched one over that