From 40f8658dcc88c1c3e1fa527e63a83782a4d36c09 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 28 Sep 2026 19:54:58 +0000 Subject: [PATCH 1/4] =?UTF-8?q?Live-pool=20unit=20claims=20share=20one=20w?= =?UTF-8?q?hole-pool=20index=20per=20test=20process=20(=C2=A72=20demand=20?= =?UTF-8?q?at=20its=20LCA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit process_shared_index is per-thread (the index is Rc-based) and libtest runs each test on a fresh thread, so each of ~16 regen/floor claims rebuilt the whole [dag, src/v2] pool index. The entry-scoped loader cannot replace it: bare names resolve through a whole-pool census. on_live_pool_thread gives the index a process lifetime: one thread owns it and those claims run their bodies there. RUST_TEST_THREADS=1 already serializes the suite. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run/entry_resolve.rs | 113 + .../src/cli_run/required_floor_runner.rs | 173 +- src/v1/stage0/src/required_regen_host.rs | 2485 +++++++++-------- 3 files changed, 1467 insertions(+), 1304 deletions(-) diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 5a0569c5dc8..2b10e35a810 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -698,6 +698,58 @@ pub fn process_shared_index(source_roots: &[String]) -> Rc { try_process_shared_index(source_roots).unwrap_or_else(|e| panic!("{e}")) } +/// THE LIVE POOL'S ONE HOLDER UNDER TEST. `process_shared_index` is per-thread because the index +/// is `Rc`-based, and libtest runs every test on a fresh thread -- so every claim that resolves an +/// entry over the live `[dag, src/v2]` pool rebuilt the whole-pool index and re-typechecked the +/// shared prefix, the same fact once per claim with its least common ancestor at the PROCESS +/// (DESIGN §2 demand minimization; gunbc#12450 measured ~18 such claims at ~90s each). The +/// entry-scoped loader cannot stand in: bare references resolve through a census of the whole pool +/// (`admit_pool_bare_references`), so no entry resolves without it. +/// +/// So the fact moves to its ancestor: one thread lives for the test process, the index lives in +/// ITS thread-local slot, and a live-pool claim runs its body there. Nothing about resolution +/// changes -- the same `process_shared_index` builds the same pool on first demand -- only how +/// many times. `RUST_TEST_THREADS=1` (`.cargo/config.toml`) already serializes the suite, so the +/// queue costs no parallelism. A panic in the body is caught on the worker and re-raised on the +/// calling test's thread with its original payload, so a failing claim reds exactly as before and +/// the worker survives for the next one. +#[cfg(test)] +pub(crate) fn on_live_pool_thread( + body: impl FnOnce() -> T + Send + 'static, +) -> T { + type Job = Box; + static WORKER: OnceLock>> = OnceLock::new(); + let sender = WORKER + .get_or_init(|| { + let (tx, rx) = std::sync::mpsc::channel::(); + std::thread::Builder::new() + .name("live-pool".to_string()) + // The largest stack any live-pool claim spawned for itself before it ran here. + .stack_size(64 * 1024 * 1024) + .spawn(move || { + for job in rx { + job(); + } + }) + .expect("the live-pool thread starts"); + Mutex::new(tx) + }) + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + sender + .send(Box::new(move || { + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)); + let _ = done_tx.send(outcome); + })) + .expect("the live-pool thread accepts work"); + match done_rx.recv().expect("the live-pool thread answers") { + Ok(value) => value, + Err(payload) => std::panic::resume_unwind(payload), + } +} + /// Fallible twin of `process_shared_index`. The MEMO IS ONLY WRITTEN ON SUCCESS -- a failed /// discovery must not install a partial index that every later caller in the process would /// then read as complete. @@ -2784,3 +2836,64 @@ pub fn reference_resolution_facts( REFERENCE_EDGE_CACHE.with(|c| c.borrow_mut().insert(cache_key, edges.clone())); edges } + +#[cfg(test)] +mod live_pool_thread_tests { + use super::*; + + fn one_module_pool(tag: &str) -> (PathBuf, Vec) { + let root = std::env::temp_dir().join(format!( + "gunbc-live-pool-{tag}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock after epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("fx")).unwrap(); + std::fs::write( + root.join("fx/one.dag"), + "module fx.one\nfn one() -> Int { 1 }\n", + ) + .unwrap(); + let roots = vec![root.to_string_lossy().into_owned()]; + (root, roots) + } + + /// THE ROUTE, not only the answer: two separate claims reach ONE index. The discriminating red + /// is the per-test-thread shape this replaces -- the second call below, run on a fresh thread, + /// builds a new index with a new generation. + #[test] + fn two_claims_on_the_live_pool_thread_share_one_index() { + let (root, roots) = one_module_pool("route"); + let first = { + let roots = roots.clone(); + on_live_pool_thread(move || process_shared_index(&roots).generation) + }; + let second = { + let roots = roots.clone(); + on_live_pool_thread(move || process_shared_index(&roots).generation) + }; + let elsewhere = std::thread::spawn(move || process_shared_index(&roots).generation) + .join() + .unwrap(); + let _ = std::fs::remove_dir_all(root); + assert_eq!(first, second, "the live-pool thread kept its index"); + assert_ne!(first, elsewhere, "a fresh thread builds its own"); + } + + /// A claim that fails on the live-pool thread reds its own test with its own message, and the + /// thread survives to serve the next claim. + #[test] + fn a_panic_on_the_live_pool_thread_reds_the_caller_and_the_thread_survives() { + let payload = std::panic::catch_unwind(|| { + on_live_pool_thread::<()>(|| panic!("planted live-pool failure")) + }) + .expect_err("the planted panic reaches the caller"); + assert_eq!( + payload.downcast_ref::<&str>().copied(), + Some("planted live-pool failure") + ); + assert_eq!(on_live_pool_thread(|| 7), 7); + } +} diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index 5b71562532e..afda99780d0 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -11345,14 +11345,15 @@ mod pure_producer_share_tests { /// scratch paths also pin the memoization defect this path once had. #[test] fn unimported_bare_provider_roster_edit_is_judged_across_frames() { - let root = process_workspace_root(); - let roots: Vec = ["dag", "src/v2"] - .iter() - .map(|r| root.join(r).to_string_lossy().to_string()) - .collect(); - let judge = UnimportedBareProviderRosterReading::head(&roots).expect("head roster"); - let roster = |rows: &str| { - std::fs::read_to_string(root.join(UNIMPORTED_BARE_PROVIDER_ROSTER)) + crate::cli_run::on_live_pool_thread(|| { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().to_string()) + .collect(); + let judge = UnimportedBareProviderRosterReading::head(&roots).expect("head roster"); + let roster = |rows: &str| { + std::fs::read_to_string(root.join(UNIMPORTED_BARE_PROVIDER_ROSTER)) .expect("roster source") .split("data unimported_bare_provider_dispositions:") .next() @@ -11361,56 +11362,58 @@ mod pure_producer_share_tests { + "data unimported_bare_provider_dispositions: List = [\n" + rows + "]\n" - }; - let row = |file: &str, name: &str, standing: &str| { - format!(" UnimportedBareProviderDisposition {{ file: \"{file}\", name: \"{name}\", standing: {standing} }},\n") - }; - let a_active = row("dag/a.dag", "f", "ActiveDebt"); - let a_fixed = row("dag/a.dag", "f", "Retired { cause: ImportsFixed }"); - let a_deleted = row("dag/a.dag", "f", "Retired { cause: FileDeleted }"); - let b_active = row("dag/b.dag", "g", "ActiveDebt"); - let read = |src: String| unimported_bare_provider_base_reading(&src).expect("lone roster"); - let base = read(roster(&a_active)); - assert_eq!( - judge - .judge_edit(&base, &read(roster(&a_active))) - .expect("verdict"), - Vec::::new() - ); - assert_eq!( - judge - .judge_edit(&base, &read(roster(&format!("{a_active}{b_active}")))) - .expect("verdict"), - vec!["RosterGainedIdentity dag/b.dag#g".to_string()] - ); - // THE RETIREMENT PATH (never executed before gunbc#12278): ActiveDebt -> Retired { cause } - // under either cause is a typed disposition and admits; dropping the row instead refuses. - for retired in [&a_fixed, &a_deleted] { + }; + let row = |file: &str, name: &str, standing: &str| { + format!(" UnimportedBareProviderDisposition {{ file: \"{file}\", name: \"{name}\", standing: {standing} }},\n") + }; + let a_active = row("dag/a.dag", "f", "ActiveDebt"); + let a_fixed = row("dag/a.dag", "f", "Retired { cause: ImportsFixed }"); + let a_deleted = row("dag/a.dag", "f", "Retired { cause: FileDeleted }"); + let b_active = row("dag/b.dag", "g", "ActiveDebt"); + let read = + |src: String| unimported_bare_provider_base_reading(&src).expect("lone roster"); + let base = read(roster(&a_active)); assert_eq!( judge - .judge_edit(&base, &read(roster(retired))) + .judge_edit(&base, &read(roster(&a_active))) .expect("verdict"), Vec::::new() ); - } - assert_eq!( - judge - .judge_edit( - &read(roster(&format!("{a_active}{b_active}"))), - &read(roster(&a_active)) - ) - .expect("verdict"), - vec!["RosterRemovedIdentity dag/b.dag#g (was ActiveDebt)".to_string()] - ); - assert_eq!( - judge - .judge_edit(&read(roster(&a_fixed)), &read(roster(&a_deleted))) - .expect("verdict"), - vec![ + assert_eq!( + judge + .judge_edit(&base, &read(roster(&format!("{a_active}{b_active}")))) + .expect("verdict"), + vec!["RosterGainedIdentity dag/b.dag#g".to_string()] + ); + // THE RETIREMENT PATH (never executed before gunbc#12278): ActiveDebt -> Retired { cause } + // under either cause is a typed disposition and admits; dropping the row instead refuses. + for retired in [&a_fixed, &a_deleted] { + assert_eq!( + judge + .judge_edit(&base, &read(roster(retired))) + .expect("verdict"), + Vec::::new() + ); + } + assert_eq!( + judge + .judge_edit( + &read(roster(&format!("{a_active}{b_active}"))), + &read(roster(&a_active)) + ) + .expect("verdict"), + vec!["RosterRemovedIdentity dag/b.dag#g (was ActiveDebt)".to_string()] + ); + assert_eq!( + judge + .judge_edit(&read(roster(&a_fixed)), &read(roster(&a_deleted))) + .expect("verdict"), + vec![ "RosterRetirementChanged dag/a.dag#f (Retired ImportsFixed -> Retired FileDeleted)" .to_string() ] - ); + ); + }); } /// THE REAL BASE READ, END TO END: the roster at `HEAD` is read through the `.dag` @@ -11420,44 +11423,46 @@ mod pure_producer_share_tests { /// roster-editing change; the fixture-coproduct control beside it cannot fail for that reason. #[test] fn a_retirement_against_the_real_base_read_admits_and_a_growth_refuses() { - let root = process_workspace_root(); - let roots: Vec = ["dag", "src/v2"] - .iter() - .map(|r| root.join(r).to_string_lossy().to_string()) - .collect(); - let judge = UnimportedBareProviderRosterReading::head(&roots).expect("head roster"); - let base_source = unimported_bare_provider_roster_source_at_base(&roots, "HEAD") - .expect("the roster exists at HEAD and decodes as BaseRosterShown"); - let read = |src: &str| unimported_bare_provider_base_reading(src).expect("lone roster"); - let base = read(&base_source); - let active = "standing: ActiveDebt }"; - assert!( - base_source.contains(active), - "the base roster carries active debt" - ); - let retired = - base_source.replacen(active, "standing: Retired { cause: ImportsFixed } }", 1); - assert_eq!( - judge.judge_edit(&base, &read(&retired)).expect("verdict"), - Vec::::new() - ); - let marker = "data unimported_bare_provider_dispositions: List = [\n"; - assert!( - base_source.contains(marker), - "the roster's data row opens as expected" - ); - let grown = base_source.replacen( + crate::cli_run::on_live_pool_thread(|| { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().to_string()) + .collect(); + let judge = UnimportedBareProviderRosterReading::head(&roots).expect("head roster"); + let base_source = unimported_bare_provider_roster_source_at_base(&roots, "HEAD") + .expect("the roster exists at HEAD and decodes as BaseRosterShown"); + let read = |src: &str| unimported_bare_provider_base_reading(src).expect("lone roster"); + let base = read(&base_source); + let active = "standing: ActiveDebt }"; + assert!( + base_source.contains(active), + "the base roster carries active debt" + ); + let retired = + base_source.replacen(active, "standing: Retired { cause: ImportsFixed } }", 1); + assert_eq!( + judge.judge_edit(&base, &read(&retired)).expect("verdict"), + Vec::::new() + ); + let marker = "data unimported_bare_provider_dispositions: List = [\n"; + assert!( + base_source.contains(marker), + "the roster's data row opens as expected" + ); + let grown = base_source.replacen( marker, &format!("{marker} UnimportedBareProviderDisposition {{ file: \"dag/zz_new.dag\", name: \"g\", standing: ActiveDebt }},\n"), 1, ); - let refused = judge.judge_edit(&base, &read(&grown)).expect("verdict"); - assert!( - refused - .iter() - .any(|r| r.starts_with("RosterGainedIdentity dag/zz_new.dag")), - "{refused:?}" - ); + let refused = judge.judge_edit(&base, &read(&grown)).expect("verdict"); + assert!( + refused + .iter() + .any(|r| r.starts_with("RosterGainedIdentity dag/zz_new.dag")), + "{refused:?}" + ); + }); } /// THE BASE-ROSTER READ DECODES THE COPRODUCT THE `.dag` DECLARES (gunbc#12205 read it as a diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 347911c214e..2bfb5a9d6f1 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -6301,86 +6301,89 @@ mod regen_round_cost_tests { /// exercises the real host-to-model boundary's named refusal. #[test] fn live_module_mirrors_have_owners_and_removed_shell_owner_refuses() { - require_measurable_host_budget(); - use crate::v1_interpreter::{self, ExecutionMode, Value}; - let workspace = workspace_root(); - let stage0 = workspace.join("src/v1/stage0/src"); - let roots: Vec = ["dag", "src/v2"] - .iter() - .map(|r| workspace.join(r).to_string_lossy().into_owned()) - .collect(); - let shell = - super::super::emitted_closure_compile_host::closure_modules(&stage0.join("lib.rs")) - .expect("generated shell declarations are readable"); - let emitted = HashMap::from([( - format!("src/{}", emitted_population_manifest_basename()), - fs::read_to_string(stage0.join(emitted_population_manifest_basename())).unwrap(), - )]); - let mirrors = generated_basenames_from_emit(&emitted).unwrap(); - // Classify non-module products through the independent emitter authority, - // never by whether the ownership map happens to contain the mirror. A missing - // owner must leave the obligation present, not shrink this test's population. - let (_, _, _, products, _) = regen_generation_role_population(&roots, &[]).unwrap(); - let module_mirrors: Vec = mirrors - .into_iter() - .filter(|mirror| !products.contains_key(mirror)) - .collect(); - assert!( - !module_mirrors.is_empty(), - "no emitted module population observed" - ); - let entry = round_cost_entry(&roots).unwrap(); - let index = super::super::process_shared_index(&roots); - let (graph, indices) = - super::super::resolve_entry_with_index_for_discovery_corpus(&index, &entry).unwrap(); - let ctx = super::super::make_eval_context(&graph, indices, ExecutionMode::Hermetic); - let decision_line = |mirror: &str, shell: &[String]| { - let args = vec![ - ( - Some("changed_mirrors".to_string()), - model_string_list(&[mirror.to_string()]), - ), - (Some("unlocatable".to_string()), model_string_list(&[])), - ( - Some("host_shell_modules".to_string()), - model_string_list(shell), - ), - ]; - let value = v1_interpreter::with_active_context(&ctx, || { - v1_interpreter::run_in_context_with_args( - &ctx, - "stage0_partition_rebuild_decision_line_today", - &args, - false, - ) - }) - .unwrap(); - let Value::Str(line) = value else { - panic!("decision was not a String") + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + use crate::v1_interpreter::{self, ExecutionMode, Value}; + let workspace = workspace_root(); + let stage0 = workspace.join("src/v1/stage0/src"); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| workspace.join(r).to_string_lossy().into_owned()) + .collect(); + let shell = + super::super::emitted_closure_compile_host::closure_modules(&stage0.join("lib.rs")) + .expect("generated shell declarations are readable"); + let emitted = HashMap::from([( + format!("src/{}", emitted_population_manifest_basename()), + fs::read_to_string(stage0.join(emitted_population_manifest_basename())).unwrap(), + )]); + let mirrors = generated_basenames_from_emit(&emitted).unwrap(); + // Classify non-module products through the independent emitter authority, + // never by whether the ownership map happens to contain the mirror. A missing + // owner must leave the obligation present, not shrink this test's population. + let (_, _, _, products, _) = regen_generation_role_population(&roots, &[]).unwrap(); + let module_mirrors: Vec = mirrors + .into_iter() + .filter(|mirror| !products.contains_key(mirror)) + .collect(); + assert!( + !module_mirrors.is_empty(), + "no emitted module population observed" + ); + let entry = round_cost_entry(&roots).unwrap(); + let index = super::super::process_shared_index(&roots); + let (graph, indices) = + super::super::resolve_entry_with_index_for_discovery_corpus(&index, &entry) + .unwrap(); + let ctx = super::super::make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let decision_line = |mirror: &str, shell: &[String]| { + let args = vec![ + ( + Some("changed_mirrors".to_string()), + model_string_list(&[mirror.to_string()]), + ), + (Some("unlocatable".to_string()), model_string_list(&[])), + ( + Some("host_shell_modules".to_string()), + model_string_list(shell), + ), + ]; + let value = v1_interpreter::with_active_context(&ctx, || { + v1_interpreter::run_in_context_with_args( + &ctx, + "stage0_partition_rebuild_decision_line_today", + &args, + false, + ) + }) + .unwrap(); + let Value::Str(line) = value else { + panic!("decision was not a String") + }; + line.to_string() }; - line.to_string() - }; - for mirror in &module_mirrors { - let line = decision_line(mirror, &shell); - assert!(!line.contains("RebuildScopeRefused"), "{mirror}: {line}"); - } - let subject = "v1_compiler_compile.rs"; - assert!(module_mirrors.iter().any(|m| m == subject)); - let green = decision_line(subject, &shell); - assert!( - green.contains("owning_packages=[v1-compiler] package_closure=[v1-compiler]"), - "{green}" - ); - let removed: Vec = shell - .into_iter() - .filter(|m| m != "v1_compiler_compile") - .collect(); - let red = decision_line(subject, &removed); - assert_eq!(red, "partition-rebuild: RebuildScopeRefused MirrorHasNoOwningPackage mirror=v1_compiler_compile.rs"); - eprintln!( - "ownership identity join: {} emitted module mirrors; {green}; mutation: {red}", - module_mirrors.len() - ); + for mirror in &module_mirrors { + let line = decision_line(mirror, &shell); + assert!(!line.contains("RebuildScopeRefused"), "{mirror}: {line}"); + } + let subject = "v1_compiler_compile.rs"; + assert!(module_mirrors.iter().any(|m| m == subject)); + let green = decision_line(subject, &shell); + assert!( + green.contains("owning_packages=[v1-compiler] package_closure=[v1-compiler]"), + "{green}" + ); + let removed: Vec = shell + .into_iter() + .filter(|m| m != "v1_compiler_compile") + .collect(); + let red = decision_line(subject, &removed); + assert_eq!(red, "partition-rebuild: RebuildScopeRefused MirrorHasNoOwningPackage mirror=v1_compiler_compile.rs"); + eprintln!( + "ownership identity join: {} emitted module mirrors; {green}; mutation: {red}", + module_mirrors.len() + ); + }); } /// THE SEED-TO-MODEL LOCKSTEP the .dag witness says it cannot hold: the host builds the @@ -6389,44 +6392,45 @@ mod regen_round_cost_tests { /// forty-minute round. The expected text is the same fixture the .dag witness asserts. #[test] fn host_built_receipt_renders_through_the_model() { - require_measurable_host_budget(); - // Both roots the production driver passes: `std.observation`'s closure reaches - // `std.cache_interface`, which imports `v2.std.optional` from src/v2. - let roots: Vec = ["dag", "src/v2"] - .iter() - .map(|r| workspace_root().join(r).to_string_lossy().into_owned()) - .collect(); - let marks = vec![ - v1_rt::TraceLedgerRow { - label: "round.seed_build".to_string(), - wall_ms: 1500, - cpu_ms: Some(9000), - }, - v1_rt::TraceLedgerRow { - label: "compile.emit".to_string(), - wall_ms: 300000, - cpu_ms: None, - }, - ]; - let rendered = render_round_cost_receipt( - &roots, - "srv1", - "2a11b317d2caf3c37d1d38a4421e8e0c06188925", - true, - 0, - 2, - 7, - &marks, - &["v1_rt.rs".to_string()], - &["stage-1".to_string()], - &["v1_rt.rs".to_string()], - &[], - &["v1-stage0-runtime".to_string()], - "sha256:claim-executor", - "sha256:g1-candidate-tree", - ) - .expect("the model renders a host-built receipt"); - assert_eq!( + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + // Both roots the production driver passes: `std.observation`'s closure reaches + // `std.cache_interface`, which imports `v2.std.optional` from src/v2. + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| workspace_root().join(r).to_string_lossy().into_owned()) + .collect(); + let marks = vec![ + v1_rt::TraceLedgerRow { + label: "round.seed_build".to_string(), + wall_ms: 1500, + cpu_ms: Some(9000), + }, + v1_rt::TraceLedgerRow { + label: "compile.emit".to_string(), + wall_ms: 300000, + cpu_ms: None, + }, + ]; + let rendered = render_round_cost_receipt( + &roots, + "srv1", + "2a11b317d2caf3c37d1d38a4421e8e0c06188925", + true, + 0, + 2, + 7, + &marks, + &["v1_rt.rs".to_string()], + &["stage-1".to_string()], + &["v1_rt.rs".to_string()], + &[], + &["v1-stage0-runtime".to_string()], + "sha256:claim-executor", + "sha256:g1-candidate-tree", + ) + .expect("the model renders a host-built receipt"); + assert_eq!( rendered, "regen-round-cost: producer=claim_executor --regen-round-cost host=srv1 \ tree=2a11b317d2caf3c37d1d38a4421e8e0c06188925 tree_dirty=true \ @@ -6444,6 +6448,7 @@ mod regen_round_cost_tests { regen-round-cost: execution-identity rebuild_packages=1 [v1-stage0-runtime] \ executable_digest=sha256:claim-executor second_generation_candidate=sha256:g1-candidate-tree\n" ); + }); } /// The process-tree clock reads on this host, and it is monotone across work. @@ -6646,64 +6651,65 @@ mod regen_convergence_host_instrument_tests { /// sees them together; rebuild runs once. #[test] fn mixed_role_two_mirror_candidate_installs_all_then_rebuilds_once() { - require_measurable_host_budget(); - let (workspace, stage0, candidate, subject) = fixture_workspace(); - let rows = [ - ( - "fixture_producer.rs", - "fixture.producer", - "pub fn native_driver_producer_roster() {}\n", - ), - ( - "fixture_subject.rs", - "fixture.subject", - "pub use crate::fixture_producer::native_driver_producer_roster;\n", - ), - ]; - let (manifest, admitted) = fixture_manifest(&candidate, &rows); - let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); - let modules = fixture_modules(&rows); - let generation_modules = ["fixture.producer".to_string()] - .into_iter() - .collect::>(); - let seed_members = rows - .iter() - .map(|row| row.0.to_string()) - .collect::>(); - let empty = BTreeSet::new(); - let drifted: Vec = rows.iter().map(|row| row.0.to_string()).collect(); - let (kind, install_set, closure_id) = convergence_plan_from_model( - &model, - 1, - &manifest.generation_id, - &manifest.candidate_tree_id, - &manifest.candidate_tree_digest, - &drifted, - &admitted, - &stage0, - &modules, - &generation_modules, - &empty, - &empty, - &seed_members, - &HashMap::new(), - &RegenEmissionScope::WholePopulation, - &[], - "seed-0", - ) - .expect("mixed seed-embedded candidate is planned"); - assert_eq!( - kind, - RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut - ); - assert_eq!( - install_set.iter().cloned().collect::>(), - drifted.iter().cloned().collect::>() - ); - assert_eq!(closure_id, "seed-embedded-install-cut"); + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let (workspace, stage0, candidate, subject) = fixture_workspace(); + let rows = [ + ( + "fixture_producer.rs", + "fixture.producer", + "pub fn native_driver_producer_roster() {}\n", + ), + ( + "fixture_subject.rs", + "fixture.subject", + "pub use crate::fixture_producer::native_driver_producer_roster;\n", + ), + ]; + let (manifest, admitted) = fixture_manifest(&candidate, &rows); + let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); + let modules = fixture_modules(&rows); + let generation_modules = ["fixture.producer".to_string()] + .into_iter() + .collect::>(); + let seed_members = rows + .iter() + .map(|row| row.0.to_string()) + .collect::>(); + let empty = BTreeSet::new(); + let drifted: Vec = rows.iter().map(|row| row.0.to_string()).collect(); + let (kind, install_set, closure_id) = convergence_plan_from_model( + &model, + 1, + &manifest.generation_id, + &manifest.candidate_tree_id, + &manifest.candidate_tree_digest, + &drifted, + &admitted, + &stage0, + &modules, + &generation_modules, + &empty, + &empty, + &seed_members, + &HashMap::new(), + &RegenEmissionScope::WholePopulation, + &[], + "seed-0", + ) + .expect("mixed seed-embedded candidate is planned"); + assert_eq!( + kind, + RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut + ); + assert_eq!( + install_set.iter().cloned().collect::>(), + drifted.iter().cloned().collect::>() + ); + assert_eq!(closure_id, "seed-embedded-install-cut"); - let mut rebuilds = 0usize; - install_convergence_stage_with_backend( + let mut rebuilds = 0usize; + install_convergence_stage_with_backend( &model, &workspace, &stage0, @@ -6740,54 +6746,57 @@ mod regen_convergence_host_instrument_tests { || Ok("seed-1".to_string()), ) .expect("installing the coherent pair rebuilds"); - assert_eq!(rebuilds, 1, "the round rebuilds once after installing both"); + assert_eq!(rebuilds, 1, "the round rebuilds once after installing both"); + }); } /// Positive control: a single GenerationInput mirror still takes PromoteGenerationInputs. #[test] fn single_generation_input_still_promotes_alone() { - require_measurable_host_budget(); - let (workspace, stage0, candidate, _) = fixture_workspace(); - let rows = [( - "fixture_producer.rs", - "fixture.producer", - "pub fn native_driver_producer_roster() {}\n", - )]; - let (manifest, admitted) = fixture_manifest(&candidate, &rows); - let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); - let modules = fixture_modules(&rows); - let generation_modules = ["fixture.producer".to_string()] - .into_iter() - .collect::>(); - let seed_members = [rows[0].0.to_string()].into_iter().collect::>(); - let empty = BTreeSet::new(); - let drifted: Vec = rows.iter().map(|row| row.0.to_string()).collect(); - let (kind, install_set, closure_id) = convergence_plan_from_model( - &model, - 1, - &manifest.generation_id, - &manifest.candidate_tree_id, - &manifest.candidate_tree_digest, - &drifted, - &admitted, - &stage0, - &modules, - &generation_modules, - &empty, - &empty, - &seed_members, - &HashMap::new(), - &RegenEmissionScope::WholePopulation, - &[], - "seed-0", - ) - .expect("single generation-input candidate is planned"); - assert_eq!( - kind, - RegenConvergenceStageKindReceipt::PromoteGenerationInputs - ); - assert_eq!(install_set, drifted); - assert_eq!(closure_id, "generation-input-cut"); + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let (workspace, stage0, candidate, _) = fixture_workspace(); + let rows = [( + "fixture_producer.rs", + "fixture.producer", + "pub fn native_driver_producer_roster() {}\n", + )]; + let (manifest, admitted) = fixture_manifest(&candidate, &rows); + let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); + let modules = fixture_modules(&rows); + let generation_modules = ["fixture.producer".to_string()] + .into_iter() + .collect::>(); + let seed_members = [rows[0].0.to_string()].into_iter().collect::>(); + let empty = BTreeSet::new(); + let drifted: Vec = rows.iter().map(|row| row.0.to_string()).collect(); + let (kind, install_set, closure_id) = convergence_plan_from_model( + &model, + 1, + &manifest.generation_id, + &manifest.candidate_tree_id, + &manifest.candidate_tree_digest, + &drifted, + &admitted, + &stage0, + &modules, + &generation_modules, + &empty, + &empty, + &seed_members, + &HashMap::new(), + &RegenEmissionScope::WholePopulation, + &[], + "seed-0", + ) + .expect("single generation-input candidate is planned"); + assert_eq!( + kind, + RegenConvergenceStageKindReceipt::PromoteGenerationInputs + ); + assert_eq!(install_set, drifted); + assert_eq!(closure_id, "generation-input-cut"); + }); } /// THE DISCRIMINATING RED FOR `admit_install_target`, and the reason the wall is not a @@ -6814,58 +6823,60 @@ mod regen_convergence_host_instrument_tests { /// rather than evidence that something else refused first. #[test] fn install_admission_contains_the_destination_against_a_symlink() { - require_measurable_host_budget(); - let (workspace, stage0, candidate, subject) = fixture_workspace(); - let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let (workspace, stage0, candidate, subject) = fixture_workspace(); + let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); + + // The link points OUTSIDE stage0_src, so a following copy writes somewhere observably + // wrong rather than merely somewhere else. + let outside = workspace.join("outside_the_surface.rs"); + let preserved = "// must not be overwritten by an install\n"; + fs::write(&outside, preserved).unwrap(); + std::os::unix::fs::symlink(&outside, stage0.join("linked_generated.rs")).unwrap(); + + let rows = [( + "linked_generated.rs", + "fixture.linked", + "// bytes that must never reach the link target\n", + )]; + let (_, admitted) = fixture_manifest(&candidate, &rows); - // The link points OUTSIDE stage0_src, so a following copy writes somewhere observably - // wrong rather than merely somewhere else. - let outside = workspace.join("outside_the_surface.rs"); - let preserved = "// must not be overwritten by an install\n"; - fs::write(&outside, preserved).unwrap(); - std::os::unix::fs::symlink(&outside, stage0.join("linked_generated.rs")).unwrap(); - - let rows = [( - "linked_generated.rs", - "fixture.linked", - "// bytes that must never reach the link target\n", - )]; - let (_, admitted) = fixture_manifest(&candidate, &rows); - - let refused = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[rows[0].0.to_string()], - &admitted, - &fixture_modules(&rows), - 1, - RegenConvergenceStageKindReceipt::PublishNonSeedOutputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-0", - "closure-0", - &subject, - |_| -> Result { - panic!("a refused install must never reach the seed build") - }, - || -> Result { - panic!("a refused install must never reach a seed digest") - }, - ) - .unwrap_err(); + let refused = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[rows[0].0.to_string()], + &admitted, + &fixture_modules(&rows), + 1, + RegenConvergenceStageKindReceipt::PublishNonSeedOutputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-0", + "closure-0", + &subject, + |_| -> Result { + panic!("a refused install must never reach the seed build") + }, + || -> Result { + panic!("a refused install must never reach a seed digest") + }, + ) + .unwrap_err(); - assert!( - refused.contains("InstallDestinationNotARegularFile"), - "expected the destination admission to refuse, got: {refused}" - ); - assert_eq!( - fs::read_to_string(&outside).unwrap(), - preserved, - "an install escaped stage0_src through a destination symlink" - ); + assert!( + refused.contains("InstallDestinationNotARegularFile"), + "expected the destination admission to refuse, got: {refused}" + ); + assert_eq!( + fs::read_to_string(&outside).unwrap(), + preserved, + "an install escaped stage0_src through a destination symlink" + ); + }); } /// THE DISCRIMINATING RED FOR THE PLANNED/EXECUTED JOIN, and the reason it is not the @@ -6896,297 +6907,341 @@ mod regen_convergence_host_instrument_tests { /// join that refuses everything, which is the shape the positive controls exclude. #[test] fn population_joins_refuse_by_identity_and_admit_an_exact_partition() { - require_measurable_host_budget(); - let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); - let modules = fixture_modules(&[ - ("a.rs", "fixture.a", ""), - ("b.rs", "fixture.b", ""), - ("c.rs", "fixture.c", ""), - ]); - let names = |rows: &[&str]| rows.iter().map(|r| (*r).to_string()).collect::>(); - - // JOIN 1, the loss this lane exists to close: an admitted surface that is neither planned - // nor deferred vanishes with no typed disposition. - let dropped = admit_install_boundary_population_from_model( - &model, - &names(&["a.rs", "b.rs"]), - &names(&["a.rs"]), - &[], - &modules, - ) - .unwrap_err(); - assert!( - dropped.contains("StagePartitionPopulationDisagrees") - && dropped.contains("admitted_without_partition=[b.rs]"), - "the dropped surface must be named as a residue, got: {dropped}" - ); + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); + let modules = fixture_modules(&[ + ("a.rs", "fixture.a", ""), + ("b.rs", "fixture.b", ""), + ("c.rs", "fixture.c", ""), + ]); + let names = |rows: &[&str]| rows.iter().map(|r| (*r).to_string()).collect::>(); + + // JOIN 1, the loss this lane exists to close: an admitted surface that is neither planned + // nor deferred vanishes with no typed disposition. + let dropped = admit_install_boundary_population_from_model( + &model, + &names(&["a.rs", "b.rs"]), + &names(&["a.rs"]), + &[], + &modules, + ) + .unwrap_err(); + assert!( + dropped.contains("StagePartitionPopulationDisagrees") + && dropped.contains("admitted_without_partition=[b.rs]"), + "the dropped surface must be named as a residue, got: {dropped}" + ); - // THE DIRECTION A COUNT CANNOT SEE: one missing and one phantom, totals equal. - let compensating = admit_install_boundary_population_from_model( - &model, - &names(&["a.rs"]), - &names(&["b.rs"]), - &[], - &modules, - ) - .unwrap_err(); - assert!( + // THE DIRECTION A COUNT CANNOT SEE: one missing and one phantom, totals equal. + let compensating = admit_install_boundary_population_from_model( + &model, + &names(&["a.rs"]), + &names(&["b.rs"]), + &[], + &modules, + ) + .unwrap_err(); + assert!( compensating.contains("admitted_without_partition=[a.rs]") && compensating.contains("partition_without_admitted=[b.rs]"), "equal counts with different identities must name BOTH residues, got: {compensating}" ); - // POSITIVE CONTROL: an exact partition, planned and deferred together. - admit_install_boundary_population_from_model( - &model, - &names(&["a.rs", "b.rs"]), - &names(&["a.rs"]), - &names(&["b.rs"]), - &modules, - ) - .expect("an exact partition is admitted"); - - // JOIN 3, both directions and terminality. - let admitted: BTreeSet = names(&["a.rs", "b.rs"]).into_iter().collect(); - let mut lineage = BTreeMap::new(); - lineage.insert( - "a.rs".to_string(), - ConvergenceDisposition::Applied { - installed_digest: "digest-a".to_string(), - }, - ); - let missing = admit_transaction_lineage_from_model(&model, &admitted, &lineage, &modules) - .unwrap_err(); - assert!( - missing.contains("TransactionLineagePopulationDisagrees") - && missing.contains("admitted_without_lineage=[b.rs]"), - "a surface the receipts never accounted for must be named, got: {missing}" - ); + // POSITIVE CONTROL: an exact partition, planned and deferred together. + admit_install_boundary_population_from_model( + &model, + &names(&["a.rs", "b.rs"]), + &names(&["a.rs"]), + &names(&["b.rs"]), + &modules, + ) + .expect("an exact partition is admitted"); + + // JOIN 3, both directions and terminality. + let admitted: BTreeSet = names(&["a.rs", "b.rs"]).into_iter().collect(); + let mut lineage = BTreeMap::new(); + lineage.insert( + "a.rs".to_string(), + ConvergenceDisposition::Applied { + installed_digest: "digest-a".to_string(), + }, + ); + let missing = + admit_transaction_lineage_from_model(&model, &admitted, &lineage, &modules) + .unwrap_err(); + assert!( + missing.contains("TransactionLineagePopulationDisagrees") + && missing.contains("admitted_without_lineage=[b.rs]"), + "a surface the receipts never accounted for must be named, got: {missing}" + ); - // DEFERRED IS NONTERMINAL: the populations agree in both directions here, so a population - // equality alone would report this transaction as complete. - lineage.insert("b.rs".to_string(), ConvergenceDisposition::Deferred); - let unfinished = - admit_transaction_lineage_from_model(&model, &admitted, &lineage, &modules) - .unwrap_err(); - assert!( - unfinished.contains("SurfaceLineageUnfinished") && unfinished.contains("[b.rs]"), - "a lineage ending in a promise must refuse as unfinished, got: {unfinished}" - ); + // DEFERRED IS NONTERMINAL: the populations agree in both directions here, so a population + // equality alone would report this transaction as complete. + lineage.insert("b.rs".to_string(), ConvergenceDisposition::Deferred); + let unfinished = + admit_transaction_lineage_from_model(&model, &admitted, &lineage, &modules) + .unwrap_err(); + assert!( + unfinished.contains("SurfaceLineageUnfinished") && unfinished.contains("[b.rs]"), + "a lineage ending in a promise must refuse as unfinished, got: {unfinished}" + ); - // POSITIVE CONTROL: Superseded closes a lineage that was never installed, so a correct - // transaction is not refused for having postponed something a later candidate replaced. - lineage.insert( - "b.rs".to_string(), - ConvergenceDisposition::Superseded { - by_generation_id: "g2".to_string(), - }, - ); - admit_transaction_lineage_from_model(&model, &admitted, &lineage, &modules) - .expect("applied and superseded together close the lineage"); + // POSITIVE CONTROL: Superseded closes a lineage that was never installed, so a correct + // transaction is not refused for having postponed something a later candidate replaced. + lineage.insert( + "b.rs".to_string(), + ConvergenceDisposition::Superseded { + by_generation_id: "g2".to_string(), + }, + ); + admit_transaction_lineage_from_model(&model, &admitted, &lineage, &modules) + .expect("applied and superseded together close the lineage"); + }); } #[test] fn stage_execution_joins_the_plan_to_independently_observed_effects() { - require_measurable_host_budget(); - let (workspace, stage0, candidate, subject) = fixture_workspace(); - let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); - // One module map covering BOTH surfaces: an observed effect on an unplanned path must - // reach the population join, not stop at `SurfaceOwnershipUnresolved`. In production - // this map is the whole-corpus one `convergence_surface_roles` returns. - let modules = fixture_modules(&[ - ("fixture_producer.rs", "fixture.producer", ""), - ("fixture_subject.rs", "fixture.subject", ""), - ]); - let passing_build = |_: &Path| -> Result { - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }; + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let (workspace, stage0, candidate, subject) = fixture_workspace(); + let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); + // One module map covering BOTH surfaces: an observed effect on an unplanned path must + // reach the population join, not stop at `SurfaceOwnershipUnresolved`. In production + // this map is the whole-corpus one `convergence_surface_roles` returns. + let modules = fixture_modules(&[ + ("fixture_producer.rs", "fixture.producer", ""), + ("fixture_subject.rs", "fixture.subject", ""), + ]); + let passing_build = |_: &Path| -> Result { + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }; - // Stage 1: install the producer. This is also the POSITIVE CONTROL -- planned and - // independently observed effects agree, so the join admits. Without it, a join that - // refused everything would satisfy both arms below. - let p_rows = [( - "fixture_producer.rs", - "fixture.producer", - "// new producer\n", - )]; - let (_, p_admitted) = fixture_manifest(&candidate, &p_rows); - install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[p_rows[0].0.to_string()], - &p_admitted, - &modules, - 1, - RegenConvergenceStageKindReceipt::PromoteGenerationInputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-p", - "generation-input-cut", - &subject, - passing_build, - || Ok("seed-1".to_string()), - ) - .expect("planned and observed agree, so the stage is admitted"); + // Stage 1: install the producer. This is also the POSITIVE CONTROL -- planned and + // independently observed effects agree, so the join admits. Without it, a join that + // refused everything would satisfy both arms below. + let p_rows = [( + "fixture_producer.rs", + "fixture.producer", + "// new producer\n", + )]; + let (_, p_admitted) = fixture_manifest(&candidate, &p_rows); + install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[p_rows[0].0.to_string()], + &p_admitted, + &modules, + 1, + RegenConvergenceStageKindReceipt::PromoteGenerationInputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-p", + "generation-input-cut", + &subject, + passing_build, + || Ok("seed-1".to_string()), + ) + .expect("planned and observed agree, so the stage is admitted"); - // ARM A. - let s_rows = [("fixture_subject.rs", "fixture.subject", "// new subject\n")]; - let (_, s_admitted) = fixture_manifest(&candidate, &s_rows); - let outside_the_plan = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[s_rows[0].0.to_string()], - &s_admitted, - &modules, - 2, - RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, - "seed-1", - "generation-0", - "tree-0", - "manifest-s", - "seed-compatibility-cut", - &subject, - |root| { - fs::write( - root.join("src/v1/stage0/src/fixture_producer.rs"), - "// rewritten by a build that was not planned to touch this\n", - ) - .unwrap(); - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-2".to_string()), - ) - .unwrap_err(); - assert!( - outside_the_plan.contains("StagePlannedExecutedMismatch"), - "an effect on an already-dirty path outside the plan must refuse as a population \ + // ARM A. + let s_rows = [("fixture_subject.rs", "fixture.subject", "// new subject\n")]; + let (_, s_admitted) = fixture_manifest(&candidate, &s_rows); + let outside_the_plan = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[s_rows[0].0.to_string()], + &s_admitted, + &modules, + 2, + RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, + "seed-1", + "generation-0", + "tree-0", + "manifest-s", + "seed-compatibility-cut", + &subject, + |root| { + fs::write( + root.join("src/v1/stage0/src/fixture_producer.rs"), + "// rewritten by a build that was not planned to touch this\n", + ) + .unwrap(); + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-2".to_string()), + ) + .unwrap_err(); + assert!( + outside_the_plan.contains("StagePlannedExecutedMismatch"), + "an effect on an already-dirty path outside the plan must refuse as a population \ mismatch, got: {outside_the_plan}" - ); - restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); + ); + restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); - // ARM B: the build reverts the surface this stage just installed. - let planned_without_effect = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[s_rows[0].0.to_string()], - &s_admitted, - &modules, - 2, - RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, - "seed-1", - "generation-0", - "tree-0", - "manifest-s", - "seed-compatibility-cut", - &subject, - |root| { - fs::write( - root.join("src/v1/stage0/src/fixture_subject.rs"), - "// old subject\n", - ) - .unwrap(); - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-2".to_string()), - ) - .unwrap_err(); - assert!( - planned_without_effect.contains("StagePlannedExecutedMismatch"), - "a planned surface the stage left byte-identical must refuse as a population \ + // ARM B: the build reverts the surface this stage just installed. + let planned_without_effect = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[s_rows[0].0.to_string()], + &s_admitted, + &modules, + 2, + RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, + "seed-1", + "generation-0", + "tree-0", + "manifest-s", + "seed-compatibility-cut", + &subject, + |root| { + fs::write( + root.join("src/v1/stage0/src/fixture_subject.rs"), + "// old subject\n", + ) + .unwrap(); + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-2".to_string()), + ) + .unwrap_err(); + assert!( + planned_without_effect.contains("StagePlannedExecutedMismatch"), + "a planned surface the stage left byte-identical must refuse as a population \ mismatch, not as a content digest verdict, got: {planned_without_effect}" - ); - restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); - - // ARM C -- A FILE THE BUILD CREATES, which the first version of this observation could - // not see. `git_changed_stage0_paths` runs `git diff --name-only` and reports tracked - // modifications only, so an UNTRACKED creation is absent from the git observation; it was - // also absent from a roster-lookup observation and from the restoration journal, which is - // three producers blind at once. The observation enumerates the directory, so the path is - // an effect the moment it appears. - let created = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[s_rows[0].0.to_string()], - &s_admitted, - &fixture_modules(&[ - ("fixture_producer.rs", "fixture.producer", ""), - ("fixture_subject.rs", "fixture.subject", ""), - ("fixture_created.rs", "fixture.created", ""), - ]), - 2, - RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, - "seed-1", - "generation-0", - "tree-0", - "manifest-s", - "seed-compatibility-cut", - &subject, - |root| { - fs::write( - root.join("src/v1/stage0/src/fixture_created.rs"), - "// invented by the build, tracked by nothing\n", - ) - .unwrap(); - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-2".to_string()), - ) - .unwrap_err(); - assert!( - created.contains("StagePlannedExecutedMismatch"), - "an untracked file created by the build must refuse as a population mismatch, \ + ); + restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); + + // ARM C -- A FILE THE BUILD CREATES, which the first version of this observation could + // not see. `git_changed_stage0_paths` runs `git diff --name-only` and reports tracked + // modifications only, so an UNTRACKED creation is absent from the git observation; it was + // also absent from a roster-lookup observation and from the restoration journal, which is + // three producers blind at once. The observation enumerates the directory, so the path is + // an effect the moment it appears. + let created = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[s_rows[0].0.to_string()], + &s_admitted, + &fixture_modules(&[ + ("fixture_producer.rs", "fixture.producer", ""), + ("fixture_subject.rs", "fixture.subject", ""), + ("fixture_created.rs", "fixture.created", ""), + ]), + 2, + RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, + "seed-1", + "generation-0", + "tree-0", + "manifest-s", + "seed-compatibility-cut", + &subject, + |root| { + fs::write( + root.join("src/v1/stage0/src/fixture_created.rs"), + "// invented by the build, tracked by nothing\n", + ) + .unwrap(); + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-2".to_string()), + ) + .unwrap_err(); + assert!( + created.contains("StagePlannedExecutedMismatch"), + "an untracked file created by the build must refuse as a population mismatch, \ got: {created}" - ); - fs::remove_file(stage0.join("fixture_created.rs")).unwrap(); - restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); + ); + fs::remove_file(stage0.join("fixture_created.rs")).unwrap(); + restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); + }); } #[test] fn install_admission_refuses_unaddressable_and_hand_maintained() { - require_measurable_host_budget(); - let (workspace, stage0, candidate, subject) = fixture_workspace(); - let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); - // NO Cargo.toml ARM HERE, deliberately. The emitted manifest is stage0's own package - // manifest emitted incompletely, so it is a self-host GAP and not a foreign artifact; - // refusing it on its extension would cement the comparator's accidental denominator as - // this boundary's policy and refuse the correct end state. What keeps it off the roster - // stays upstream, and making its absence a typed disposition is the projection-identity - // subject, not this one. - let subject_before = fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(); - let mut mismatches: Vec = Vec::new(); - - for (basename, cause) in [ - ("../Cargo.toml", "InstallTargetNotABareBasename"), - ("nested/mod.rs", "InstallTargetNotABareBasename"), - ("cli_run.rs", "InstallTargetHandMaintained"), - ] { - let refused = install_convergence_stage_with_backend( + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let (workspace, stage0, candidate, subject) = fixture_workspace(); + let model = RegenConvergenceModel::load(&fixture_roots()).unwrap(); + // NO Cargo.toml ARM HERE, deliberately. The emitted manifest is stage0's own package + // manifest emitted incompletely, so it is a self-host GAP and not a foreign artifact; + // refusing it on its extension would cement the comparator's accidental denominator as + // this boundary's policy and refuse the correct end state. What keeps it off the roster + // stays upstream, and making its absence a typed disposition is the projection-identity + // subject, not this one. + let subject_before = fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(); + let mut mismatches: Vec = Vec::new(); + + for (basename, cause) in [ + ("../Cargo.toml", "InstallTargetNotABareBasename"), + ("nested/mod.rs", "InstallTargetNotABareBasename"), + ("cli_run.rs", "InstallTargetHandMaintained"), + ] { + let refused = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[basename.to_string()], + &HashMap::new(), + &HashMap::new(), + 1, + RegenConvergenceStageKindReceipt::PublishNonSeedOutputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-0", + "closure-0", + &subject, + |_| -> Result { + panic!("a refused install must never reach the seed build") + }, + || -> Result { + panic!("a refused install must never reach a seed digest") + }, + ) + .unwrap_err(); + // ACCUMULATED, NOT ASSERTED PER ARM. Asserting inside the loop aborts at the first + // mismatch, so a run proves only the FIRST arm discriminates and says nothing about + // the rest — and the arms exercise different branches. Collecting every mismatch + // makes one red run report all three causes at once. + if !refused.contains(cause) { + mismatches.push(format!( + "installing {basename} refused with {refused}, expected {cause}" + )); + } + } + // POSITIVE CONTROL for the symlink arm: the SAME basename shape with an ordinary regular + // destination must NOT refuse for containment. Without this, an admission that refused + // every destination would pass the arm above while discriminating nothing. + fs::write(stage0.join("plain_generated.rs"), "// regular file\n").unwrap(); + let control = install_convergence_stage_with_backend( &model, &workspace, &stage0, &candidate, - &[basename.to_string()], + &["plain_generated.rs".to_string()], &HashMap::new(), &HashMap::new(), 1, @@ -7198,101 +7253,66 @@ mod regen_convergence_host_instrument_tests { "closure-0", &subject, |_| -> Result { - panic!("a refused install must never reach the seed build") + panic!("this control must never reach the seed build") }, || -> Result { - panic!("a refused install must never reach a seed digest") + panic!("this control must never reach a seed digest") }, ) .unwrap_err(); - // ACCUMULATED, NOT ASSERTED PER ARM. Asserting inside the loop aborts at the first - // mismatch, so a run proves only the FIRST arm discriminates and says nothing about - // the rest — and the arms exercise different branches. Collecting every mismatch - // makes one red run report all three causes at once. - if !refused.contains(cause) { + if control.contains("InstallDestinationNotARegularFile") + || control.contains("InstallTargetNotABareBasename") + { mismatches.push(format!( - "installing {basename} refused with {refused}, expected {cause}" + "a regular destination was refused by admission: {control}" )); } - } - // POSITIVE CONTROL for the symlink arm: the SAME basename shape with an ordinary regular - // destination must NOT refuse for containment. Without this, an admission that refused - // every destination would pass the arm above while discriminating nothing. - fs::write(stage0.join("plain_generated.rs"), "// regular file\n").unwrap(); - let control = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &["plain_generated.rs".to_string()], - &HashMap::new(), - &HashMap::new(), - 1, - RegenConvergenceStageKindReceipt::PublishNonSeedOutputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-0", - "closure-0", - &subject, - |_| -> Result { - panic!("this control must never reach the seed build") - }, - || -> Result { panic!("this control must never reach a seed digest") }, - ) - .unwrap_err(); - if control.contains("InstallDestinationNotARegularFile") - || control.contains("InstallTargetNotABareBasename") - { - mismatches.push(format!( - "a regular destination was refused by admission: {control}" - )); - } - assert!(mismatches.is_empty(), "{}", mismatches.join("\n")); - - // The refusal is BEFORE the mutation boundary, not a rollback of one: no artifact landed, - // and no authoritative byte moved and came back. - assert!(!stage0.join("Cargo.toml").exists()); - assert!(!stage0.join("cli_run.rs").exists()); - assert!(!stage0.join("nested").exists()); - assert_eq!( - fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), - subject_before - ); + assert!(mismatches.is_empty(), "{}", mismatches.join("\n")); + + // The refusal is BEFORE the mutation boundary, not a rollback of one: no artifact landed, + // and no authoritative byte moved and came back. + assert!(!stage0.join("Cargo.toml").exists()); + assert!(!stage0.join("cli_run.rs").exists()); + assert!(!stage0.join("nested").exists()); + assert_eq!( + fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), + subject_before + ); - // POSITIVE CONTROL. The same entry point, one generated Rust surface, installs — so the - // arms above measure the artifact kind and not a call that refuses everything. - let rows = [("fixture_subject.rs", "fixture.subject", "// new subject\n")]; - let (_, admitted) = fixture_manifest(&candidate, &rows); - install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[rows[0].0.to_string()], - &admitted, - &fixture_modules(&rows), - 1, - RegenConvergenceStageKindReceipt::PromoteGenerationInputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-0", - "generation-input-cut", - &subject, - |_| { - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-1".to_string()), - ) - .unwrap(); - assert_eq!( - fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), - "// new subject\n" - ); + // POSITIVE CONTROL. The same entry point, one generated Rust surface, installs — so the + // arms above measure the artifact kind and not a call that refuses everything. + let rows = [("fixture_subject.rs", "fixture.subject", "// new subject\n")]; + let (_, admitted) = fixture_manifest(&candidate, &rows); + install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[rows[0].0.to_string()], + &admitted, + &fixture_modules(&rows), + 1, + RegenConvergenceStageKindReceipt::PromoteGenerationInputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-0", + "generation-input-cut", + &subject, + |_| { + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-1".to_string()), + ) + .unwrap(); + assert_eq!( + fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), + "// new subject\n" + ); + }); } #[test] @@ -7326,485 +7346,493 @@ mod regen_convergence_host_instrument_tests { /// production. Only the external seed build and executable digest are hermetic callbacks. #[test] fn mutating_transaction_binds_candidates_restores_and_reaches_staged_fixed_point() { - require_measurable_host_budget(); - let roots = fixture_roots(); - let model = RegenConvergenceModel::load(&roots).unwrap(); - - // An emitted non-Rust artifact is admitted from the writer's exact output population, - // not from a basename exception. This is the crate-layout-product shape without naming - // any particular product path in the manifest authority. - let (_root, _, emitted_artifact_candidate, _) = fixture_workspace(); - fs::write( - emitted_artifact_candidate.join("fixture-layout.artifact"), - "emitted layout bytes\n", - ) - .unwrap(); - let emitted_artifact_manifest = produce_candidate_manifest( - &emitted_artifact_candidate, - &[], - &["fixture-layout.artifact".to_string()] - .into_iter() - .collect(), - &HashMap::new(), - "seed-0", - "tree-emitted-artifact", - ) - .unwrap(); - assert_eq!(emitted_artifact_manifest.surfaces.len(), 1); - assert!(matches!( - emitted_artifact_manifest.surfaces[0].role, - RegenCandidateManifestSurfaceRole::GeneratedSurface - )); - - // Relative-path identity is load-bearing: a nested file cannot borrow the generated role - // of an emitted root artifact merely because their basenames collide. - let (_root, _, emitted_collision_candidate, _) = fixture_workspace(); - fs::create_dir_all(emitted_collision_candidate.join("nested")).unwrap(); - fs::write( - emitted_collision_candidate.join("nested/fixture-layout.artifact"), - "nested foreign bytes\n", - ) - .unwrap(); - assert!(produce_candidate_manifest( - &emitted_collision_candidate, - &[], - &["fixture-layout.artifact".to_string()] - .into_iter() - .collect(), - &HashMap::new(), - "seed-0", - "tree-emitted-collision", - ) - .unwrap_err() - .contains("CandidateManifestPopulationMismatch")); - - // Bootstrap-source mirrors inhabit the same immutable candidate artifact as generated - // surfaces. Their role is bound by the manifest, and changing their bytes after - // production refuses before any install journal exists. - let (_root, _, bootstrap_candidate, _) = fixture_workspace(); - fs::create_dir_all(bootstrap_candidate.join("cli_run")).unwrap(); - fs::write( - bootstrap_candidate.join("cli_run/fixture_support.txt"), - "original support bytes\n", - ) - .unwrap(); - let bootstrap_manifest = produce_candidate_manifest( - &bootstrap_candidate, - &[], - &BTreeSet::new(), - &HashMap::new(), - "seed-0", - "tree-bootstrap", - ) - .unwrap(); - assert!(matches!( - bootstrap_manifest.surfaces[0].role, - RegenCandidateManifestSurfaceRole::BootstrapSourceMirror - )); - fs::write( - bootstrap_candidate.join("cli_run/fixture_support.txt"), - "tampered support bytes\n", - ) - .unwrap(); - assert!(admit_candidate_manifest( - &model, - &bootstrap_candidate, - &bootstrap_manifest, - "seed-0" - ) - .unwrap_err() - .contains("CandidateManifestTreeDigestMismatch")); - - // A file with neither a generated-surface row nor a bootstrap-source-mirror row remains - // foreign to the complete artifact and is refused at the population wall. - let (_root, _, foreign_candidate, _) = fixture_workspace(); - let foreign_rows = [( - "fixture_generated.rs", - "fixture.generated", - "// generated\n", - )]; - let (foreign_manifest, _) = fixture_manifest(&foreign_candidate, &foreign_rows); - fs::write(foreign_candidate.join("foreign.bin"), b"foreign bytes\n").unwrap(); - assert!( - admit_candidate_manifest(&model, &foreign_candidate, &foreign_manifest, "seed-0") - .unwrap_err() - .contains("CandidateManifestPopulationMismatch") - ); - - // A candidate changed after its generation manifest is refused before a journal exists. - let (workspace, stage0, candidate, subject) = fixture_workspace(); - let rows = [( - "fixture_producer.rs", - "fixture.producer", - "// new producer\n", - )]; - let (_, admitted) = fixture_manifest(&candidate, &rows); - let stale_manifest = RegenCandidateManifest { - producer_seed_digest: "seed-g0".to_string(), - generation_id: "generation-g0".to_string(), - candidate_tree_id: "tree-g0".to_string(), - candidate_tree_digest: "tree-g0-digest".to_string(), - surfaces: admitted.values().cloned().collect(), - aggregate_digest: String::new(), - }; - let stale_manifest = RegenCandidateManifest { - aggregate_digest: candidate_manifest_aggregate( - &stale_manifest.producer_seed_digest, - &stale_manifest.generation_id, - &stale_manifest.candidate_tree_id, - &stale_manifest.candidate_tree_digest, - &stale_manifest.surfaces, + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let roots = fixture_roots(); + let model = RegenConvergenceModel::load(&roots).unwrap(); + + // An emitted non-Rust artifact is admitted from the writer's exact output population, + // not from a basename exception. This is the crate-layout-product shape without naming + // any particular product path in the manifest authority. + let (_root, _, emitted_artifact_candidate, _) = fixture_workspace(); + fs::write( + emitted_artifact_candidate.join("fixture-layout.artifact"), + "emitted layout bytes\n", ) - .unwrap(), - ..stale_manifest - }; - assert!( - admit_candidate_manifest(&model, &candidate, &stale_manifest, "seed-g1") - .unwrap_err() - .contains("CandidateStaleAfterProducerRebuild") - ); - fs::write(candidate.join(rows[0].0), "// tampered\n").unwrap(); - let tampered = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[rows[0].0.to_string()], - &admitted, - &fixture_modules(&rows), - 1, - RegenConvergenceStageKindReceipt::PromoteGenerationInputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-0", - "generation-input-cut", - &subject, - |_| { - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-1".to_string()), - ) - .unwrap_err(); - assert!(tampered.contains("CandidateManifestSurfaceDigestMismatch")); - assert!(!regen_convergence_journal_path(&workspace).exists()); - fs::write(candidate.join(rows[0].0), "// new producer\n").unwrap(); - let post_build_tamper = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[rows[0].0.to_string()], - &admitted, - &fixture_modules(&rows), - 1, - RegenConvergenceStageKindReceipt::PromoteGenerationInputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-0", - "generation-input-cut", - &subject, - |root| { - fs::write( - root.join("src/v1/stage0/src/fixture_producer.rs"), - "// mutated during build\n", - ) - .unwrap(); - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-1".to_string()), - ) - .unwrap_err(); - assert!(post_build_tamper.contains("InstalledDigestMismatch")); - restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); - - // A failed build crosses the real copy boundary, then the subject-bound journal restores - // the admitted checkpoint. This is the single-pass negative control. - let (workspace, stage0, candidate, subject) = fixture_workspace(); - let rows = [("fixture_subject.rs", "fixture.subject", "// new subject\n")]; - let (_, admitted) = fixture_manifest(&candidate, &rows); - let failed = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[rows[0].0.to_string()], - &admitted, - &fixture_modules(&rows), - 1, - RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, - "seed-0", - "generation-0", - "tree-0", - "manifest-0", - "seed-compatibility-cut", - &subject, - |_| Err("fixture seed rejected partial generation".to_string()), - || Ok("seed-1".to_string()), - ) - .unwrap_err(); - assert!(failed.contains("partial generation")); - restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); - assert_eq!( - fs::read_to_string(stage0.join(rows[0].0)).unwrap(), - "// old subject\n" - ); - - // Promote the producer, then install the complete subject/dependent compatibility cut. - let p_rows = [( - "fixture_producer.rs", - "fixture.producer", - "// new producer\n", - )]; - let (_, p_admitted) = fixture_manifest(&candidate, &p_rows); - install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[p_rows[0].0.to_string()], - &p_admitted, - &fixture_modules(&p_rows), - 1, - RegenConvergenceStageKindReceipt::PromoteGenerationInputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-p", - "generation-input-cut", - &subject, - |_| { - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-1".to_string()), - ) - .unwrap(); - let s_rows = [ - ("fixture_subject.rs", "fixture.subject", "// new subject\n"), - ( - "fixture_dependent.rs", - "fixture.dependent", - "// new dependent\n", - ), - ]; - let (_, s_admitted) = fixture_manifest(&candidate, &s_rows); - let stage = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &s_rows - .iter() - .map(|row| row.0.to_string()) - .collect::>(), - &s_admitted, - &fixture_modules(&s_rows), - 2, - RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, - "seed-1", - "generation-0", - "tree-0", - "manifest-s", - "seed-compatibility-cut", - &subject, - |root| { - let src = root.join("src/v1/stage0/src"); - if fs::read_to_string(src.join("fixture_subject.rs")).unwrap() != "// new subject\n" - || fs::read_to_string(src.join("fixture_dependent.rs")).unwrap() - != "// new dependent\n" - { - return Err("compatibility cut incomplete".to_string()); - } - Ok(CargoBuildObservation { - compiled_crates: 2, - compiled_packages: vec![ - "fixture-producer".to_string(), - "fixture-seed".to_string(), - ], - }) - }, - || Ok("seed-2".to_string()), - ) - .unwrap(); - assert!(stage.surfaces.iter().all( - |surface| surface.standing == RegenSurfaceExecutionStandingReceipt::TerminalPassed - )); - assert_eq!(stage.output_seed_digest, "seed-2"); - assert_eq!(stage.dependency_closure_id, "seed-compatibility-cut"); + .unwrap(); + let emitted_artifact_manifest = produce_candidate_manifest( + &emitted_artifact_candidate, + &[], + &["fixture-layout.artifact".to_string()] + .into_iter() + .collect(), + &HashMap::new(), + "seed-0", + "tree-emitted-artifact", + ) + .unwrap(); + assert_eq!(emitted_artifact_manifest.surfaces.len(), 1); + assert!(matches!( + emitted_artifact_manifest.surfaces[0].role, + RegenCandidateManifestSurfaceRole::GeneratedSurface + )); - // Cross-head and corrupt-backup journals refuse before touching authoritative bytes. - let wrong_subject = RegenConvergenceCheckpointSubject { - starting_commit: "other-head".to_string(), - ..subject.clone() - }; - let before = fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(); - assert!( - restore_regen_convergence_journal_for_subject(&workspace, &wrong_subject) - .unwrap_err() - .contains("CheckpointSubjectMismatch") - ); - assert_eq!( - fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), - before - ); - let journal_root = regen_convergence_journal_path(&workspace); - let backup = fs::read_dir(&journal_root) - .unwrap() - .map(|entry| entry.expect("fixture journal directory entry must remain readable")) - .map(|entry| entry.path()) - .find(|path| path.extension().and_then(|ext| ext.to_str()) == Some("bak")) + // Relative-path identity is load-bearing: a nested file cannot borrow the generated role + // of an emitted root artifact merely because their basenames collide. + let (_root, _, emitted_collision_candidate, _) = fixture_workspace(); + fs::create_dir_all(emitted_collision_candidate.join("nested")).unwrap(); + fs::write( + emitted_collision_candidate.join("nested/fixture-layout.artifact"), + "nested foreign bytes\n", + ) .unwrap(); - fs::write(&backup, "corrupt backup\n").unwrap(); - assert!( - restore_regen_convergence_journal_for_subject(&workspace, &subject) - .unwrap_err() - .contains("CheckpointArtifactDigestMismatch") - ); - assert_eq!( - fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), - before - ); + assert!(produce_candidate_manifest( + &emitted_collision_candidate, + &[], + &["fixture-layout.artifact".to_string()] + .into_iter() + .collect(), + &HashMap::new(), + "seed-0", + "tree-emitted-collision", + ) + .unwrap_err() + .contains("CandidateManifestPopulationMismatch")); + + // Bootstrap-source mirrors inhabit the same immutable candidate artifact as generated + // surfaces. Their role is bound by the manifest, and changing their bytes after + // production refuses before any install journal exists. + let (_root, _, bootstrap_candidate, _) = fixture_workspace(); + fs::create_dir_all(bootstrap_candidate.join("cli_run")).unwrap(); + fs::write( + bootstrap_candidate.join("cli_run/fixture_support.txt"), + "original support bytes\n", + ) + .unwrap(); + let bootstrap_manifest = produce_candidate_manifest( + &bootstrap_candidate, + &[], + &BTreeSet::new(), + &HashMap::new(), + "seed-0", + "tree-bootstrap", + ) + .unwrap(); + assert!(matches!( + bootstrap_manifest.surfaces[0].role, + RegenCandidateManifestSurfaceRole::BootstrapSourceMirror + )); + fs::write( + bootstrap_candidate.join("cli_run/fixture_support.txt"), + "tampered support bytes\n", + ) + .unwrap(); + assert!(admit_candidate_manifest( + &model, + &bootstrap_candidate, + &bootstrap_manifest, + "seed-0" + ) + .unwrap_err() + .contains("CandidateManifestTreeDigestMismatch")); + + // A file with neither a generated-surface row nor a bootstrap-source-mirror row remains + // foreign to the complete artifact and is refused at the population wall. + let (_root, _, foreign_candidate, _) = fixture_workspace(); + let foreign_rows = [( + "fixture_generated.rs", + "fixture.generated", + "// generated\n", + )]; + let (foreign_manifest, _) = fixture_manifest(&foreign_candidate, &foreign_rows); + fs::write(foreign_candidate.join("foreign.bin"), b"foreign bytes\n").unwrap(); + assert!(admit_candidate_manifest( + &model, + &foreign_candidate, + &foreign_manifest, + "seed-0" + ) + .unwrap_err() + .contains("CandidateManifestPopulationMismatch")); - // An unplanned generated mutation is detected after the hermetic build callback and the - // complete-population journal restores it with the planned surface. - let (workspace, stage0, candidate, subject) = fixture_workspace(); - let rows = [( - "fixture_producer.rs", - "fixture.producer", - "// new producer\n", - )]; - let (_, admitted) = fixture_manifest(&candidate, &rows); - let unplanned = install_convergence_stage_with_backend( - &model, - &workspace, - &stage0, - &candidate, - &[rows[0].0.to_string()], - &admitted, - &fixture_modules(&rows), - 1, - RegenConvergenceStageKindReceipt::PromoteGenerationInputs, - "seed-0", - "generation-0", - "tree-0", - "manifest-0", - "generation-input-cut", - &subject, - |root| { - fs::write( - root.join("src/v1/stage0/src/fixture_unplanned.rs"), - "// mutated\n", + // A candidate changed after its generation manifest is refused before a journal exists. + let (workspace, stage0, candidate, subject) = fixture_workspace(); + let rows = [( + "fixture_producer.rs", + "fixture.producer", + "// new producer\n", + )]; + let (_, admitted) = fixture_manifest(&candidate, &rows); + let stale_manifest = RegenCandidateManifest { + producer_seed_digest: "seed-g0".to_string(), + generation_id: "generation-g0".to_string(), + candidate_tree_id: "tree-g0".to_string(), + candidate_tree_digest: "tree-g0-digest".to_string(), + surfaces: admitted.values().cloned().collect(), + aggregate_digest: String::new(), + }; + let stale_manifest = RegenCandidateManifest { + aggregate_digest: candidate_manifest_aggregate( + &stale_manifest.producer_seed_digest, + &stale_manifest.generation_id, + &stale_manifest.candidate_tree_id, + &stale_manifest.candidate_tree_digest, + &stale_manifest.surfaces, ) + .unwrap(), + ..stale_manifest + }; + assert!( + admit_candidate_manifest(&model, &candidate, &stale_manifest, "seed-g1") + .unwrap_err() + .contains("CandidateStaleAfterProducerRebuild") + ); + fs::write(candidate.join(rows[0].0), "// tampered\n").unwrap(); + let tampered = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[rows[0].0.to_string()], + &admitted, + &fixture_modules(&rows), + 1, + RegenConvergenceStageKindReceipt::PromoteGenerationInputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-0", + "generation-input-cut", + &subject, + |_| { + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-1".to_string()), + ) + .unwrap_err(); + assert!(tampered.contains("CandidateManifestSurfaceDigestMismatch")); + assert!(!regen_convergence_journal_path(&workspace).exists()); + fs::write(candidate.join(rows[0].0), "// new producer\n").unwrap(); + let post_build_tamper = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[rows[0].0.to_string()], + &admitted, + &fixture_modules(&rows), + 1, + RegenConvergenceStageKindReceipt::PromoteGenerationInputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-0", + "generation-input-cut", + &subject, + |root| { + fs::write( + root.join("src/v1/stage0/src/fixture_producer.rs"), + "// mutated during build\n", + ) + .unwrap(); + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-1".to_string()), + ) + .unwrap_err(); + assert!(post_build_tamper.contains("InstalledDigestMismatch")); + restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); + + // A failed build crosses the real copy boundary, then the subject-bound journal restores + // the admitted checkpoint. This is the single-pass negative control. + let (workspace, stage0, candidate, subject) = fixture_workspace(); + let rows = [("fixture_subject.rs", "fixture.subject", "// new subject\n")]; + let (_, admitted) = fixture_manifest(&candidate, &rows); + let failed = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[rows[0].0.to_string()], + &admitted, + &fixture_modules(&rows), + 1, + RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, + "seed-0", + "generation-0", + "tree-0", + "manifest-0", + "seed-compatibility-cut", + &subject, + |_| Err("fixture seed rejected partial generation".to_string()), + || Ok("seed-1".to_string()), + ) + .unwrap_err(); + assert!(failed.contains("partial generation")); + restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); + assert_eq!( + fs::read_to_string(stage0.join(rows[0].0)).unwrap(), + "// old subject\n" + ); + + // Promote the producer, then install the complete subject/dependent compatibility cut. + let p_rows = [( + "fixture_producer.rs", + "fixture.producer", + "// new producer\n", + )]; + let (_, p_admitted) = fixture_manifest(&candidate, &p_rows); + install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[p_rows[0].0.to_string()], + &p_admitted, + &fixture_modules(&p_rows), + 1, + RegenConvergenceStageKindReceipt::PromoteGenerationInputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-p", + "generation-input-cut", + &subject, + |_| { + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-1".to_string()), + ) + .unwrap(); + let s_rows = [ + ("fixture_subject.rs", "fixture.subject", "// new subject\n"), + ( + "fixture_dependent.rs", + "fixture.dependent", + "// new dependent\n", + ), + ]; + let (_, s_admitted) = fixture_manifest(&candidate, &s_rows); + let stage = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &s_rows + .iter() + .map(|row| row.0.to_string()) + .collect::>(), + &s_admitted, + &fixture_modules(&s_rows), + 2, + RegenConvergenceStageKindReceipt::InstallSeedCompatibilityCut, + "seed-1", + "generation-0", + "tree-0", + "manifest-s", + "seed-compatibility-cut", + &subject, + |root| { + let src = root.join("src/v1/stage0/src"); + if fs::read_to_string(src.join("fixture_subject.rs")).unwrap() + != "// new subject\n" + || fs::read_to_string(src.join("fixture_dependent.rs")).unwrap() + != "// new dependent\n" + { + return Err("compatibility cut incomplete".to_string()); + } + Ok(CargoBuildObservation { + compiled_crates: 2, + compiled_packages: vec![ + "fixture-producer".to_string(), + "fixture-seed".to_string(), + ], + }) + }, + || Ok("seed-2".to_string()), + ) + .unwrap(); + assert!(stage + .surfaces + .iter() + .all(|surface| surface.standing + == RegenSurfaceExecutionStandingReceipt::TerminalPassed)); + assert_eq!(stage.output_seed_digest, "seed-2"); + assert_eq!(stage.dependency_closure_id, "seed-compatibility-cut"); + + // Cross-head and corrupt-backup journals refuse before touching authoritative bytes. + let wrong_subject = RegenConvergenceCheckpointSubject { + starting_commit: "other-head".to_string(), + ..subject.clone() + }; + let before = fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(); + assert!( + restore_regen_convergence_journal_for_subject(&workspace, &wrong_subject) + .unwrap_err() + .contains("CheckpointSubjectMismatch") + ); + assert_eq!( + fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), + before + ); + let journal_root = regen_convergence_journal_path(&workspace); + let backup = fs::read_dir(&journal_root) + .unwrap() + .map(|entry| entry.expect("fixture journal directory entry must remain readable")) + .map(|entry| entry.path()) + .find(|path| path.extension().and_then(|ext| ext.to_str()) == Some("bak")) .unwrap(); - Ok(CargoBuildObservation { - compiled_crates: 1, - compiled_packages: vec!["fixture-seed".to_string()], - }) - }, - || Ok("seed-1".to_string()), - ) - .unwrap_err(); - assert!(unplanned.contains("UnplannedPathMutated")); - restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); - assert_eq!( - fs::read_to_string(stage0.join("fixture_unplanned.rs")).unwrap(), - "// stable\n" - ); + fs::write(&backup, "corrupt backup\n").unwrap(); + assert!( + restore_regen_convergence_journal_for_subject(&workspace, &subject) + .unwrap_err() + .contains("CheckpointArtifactDigestMismatch") + ); + assert_eq!( + fs::read_to_string(stage0.join("fixture_subject.rs")).unwrap(), + before + ); - // Cycle and bound are reached through the host's production planner over successive - // generation identities, rather than supplied as fixture terminal variants. - let (_, admitted) = fixture_manifest(&candidate, &rows); - let modules = fixture_modules(&rows); - let generation_modules = ["fixture.producer".to_string()] - .into_iter() - .collect::>(); - let seed_members = [rows[0].0.to_string()].into_iter().collect::>(); - let empty = BTreeSet::new(); - let digest = bytes_digest(b"generation-state"); - // Publication is reachable only from an explicit generated-product role. Merely being - // absent from the seed manifest remains unresolved for unclassified surfaces. - let non_seed_roles = [(rows[0].0.to_string(), "NonSeedGeneratedOutput".to_string())] - .into_iter() - .collect::>(); - let (publish_kind, publish_paths, publish_closure_id) = convergence_plan_from_model( - &model, - 1, - "generation-publish", - "tree-publish", - &digest, - &[rows[0].0.to_string()], - &admitted, - &stage0, - &modules, - &empty, - &empty, - &empty, - &empty, - &non_seed_roles, - &RegenEmissionScope::WholePopulation, - &[], - "seed-publish", - ) - .unwrap(); - assert_eq!( - publish_kind, - RegenConvergenceStageKindReceipt::PublishNonSeedOutputs - ); - assert_eq!(publish_paths, vec![rows[0].0.to_string()]); - assert_eq!(publish_closure_id, "non-seed-publish"); + // An unplanned generated mutation is detected after the hermetic build callback and the + // complete-population journal restores it with the planned surface. + let (workspace, stage0, candidate, subject) = fixture_workspace(); + let rows = [( + "fixture_producer.rs", + "fixture.producer", + "// new producer\n", + )]; + let (_, admitted) = fixture_manifest(&candidate, &rows); + let unplanned = install_convergence_stage_with_backend( + &model, + &workspace, + &stage0, + &candidate, + &[rows[0].0.to_string()], + &admitted, + &fixture_modules(&rows), + 1, + RegenConvergenceStageKindReceipt::PromoteGenerationInputs, + "seed-0", + "generation-0", + "tree-0", + "manifest-0", + "generation-input-cut", + &subject, + |root| { + fs::write( + root.join("src/v1/stage0/src/fixture_unplanned.rs"), + "// mutated\n", + ) + .unwrap(); + Ok(CargoBuildObservation { + compiled_crates: 1, + compiled_packages: vec!["fixture-seed".to_string()], + }) + }, + || Ok("seed-1".to_string()), + ) + .unwrap_err(); + assert!(unplanned.contains("UnplannedPathMutated")); + restore_regen_convergence_journal_for_subject(&workspace, &subject).unwrap(); + assert_eq!( + fs::read_to_string(stage0.join("fixture_unplanned.rs")).unwrap(), + "// stable\n" + ); - let cycle = convergence_plan_from_model( - &model, - 2, - "generation-1", - "tree-1", - &digest, - &[rows[0].0.to_string()], - &admitted, - &stage0, - &modules, - &generation_modules, - &empty, - &empty, - &seed_members, - &HashMap::new(), - &RegenEmissionScope::WholePopulation, - &[format!("seed-0:{digest}")], - "seed-0", - ) - .unwrap_err(); - assert!(cycle.contains("CycleRefused"), "{cycle}"); - let bound = convergence_plan_from_model( - &model, - REGEN_CONVERGENCE_BOUND + 1, - "generation-bound", - "tree-bound", - &bytes_digest(b"new-tree"), - &[rows[0].0.to_string()], - &admitted, - &stage0, - &modules, - &generation_modules, - &empty, - &empty, - &seed_members, - &HashMap::new(), - &RegenEmissionScope::WholePopulation, - &[], - "seed-new", - ) - .unwrap_err(); - assert!(bound.contains("BoundRefused"), "{bound}"); + // Cycle and bound are reached through the host's production planner over successive + // generation identities, rather than supplied as fixture terminal variants. + let (_, admitted) = fixture_manifest(&candidate, &rows); + let modules = fixture_modules(&rows); + let generation_modules = ["fixture.producer".to_string()] + .into_iter() + .collect::>(); + let seed_members = [rows[0].0.to_string()].into_iter().collect::>(); + let empty = BTreeSet::new(); + let digest = bytes_digest(b"generation-state"); + // Publication is reachable only from an explicit generated-product role. Merely being + // absent from the seed manifest remains unresolved for unclassified surfaces. + let non_seed_roles = [(rows[0].0.to_string(), "NonSeedGeneratedOutput".to_string())] + .into_iter() + .collect::>(); + let (publish_kind, publish_paths, publish_closure_id) = convergence_plan_from_model( + &model, + 1, + "generation-publish", + "tree-publish", + &digest, + &[rows[0].0.to_string()], + &admitted, + &stage0, + &modules, + &empty, + &empty, + &empty, + &empty, + &non_seed_roles, + &RegenEmissionScope::WholePopulation, + &[], + "seed-publish", + ) + .unwrap(); + assert_eq!( + publish_kind, + RegenConvergenceStageKindReceipt::PublishNonSeedOutputs + ); + assert_eq!(publish_paths, vec![rows[0].0.to_string()]); + assert_eq!(publish_closure_id, "non-seed-publish"); + + let cycle = convergence_plan_from_model( + &model, + 2, + "generation-1", + "tree-1", + &digest, + &[rows[0].0.to_string()], + &admitted, + &stage0, + &modules, + &generation_modules, + &empty, + &empty, + &seed_members, + &HashMap::new(), + &RegenEmissionScope::WholePopulation, + &[format!("seed-0:{digest}")], + "seed-0", + ) + .unwrap_err(); + assert!(cycle.contains("CycleRefused"), "{cycle}"); + let bound = convergence_plan_from_model( + &model, + REGEN_CONVERGENCE_BOUND + 1, + "generation-bound", + "tree-bound", + &bytes_digest(b"new-tree"), + &[rows[0].0.to_string()], + &admitted, + &stage0, + &modules, + &generation_modules, + &empty, + &empty, + &seed_members, + &HashMap::new(), + &RegenEmissionScope::WholePopulation, + &[], + "seed-new", + ) + .unwrap_err(); + assert!(bound.contains("BoundRefused"), "{bound}"); + }); } } @@ -8603,37 +8631,42 @@ mod regen_emission_scope_tests { /// forty-minute round. #[test] fn host_selection_and_model_selection_agree() { - require_measurable_host_budget(); - for scope in [ - RegenEmissionScope::WholePopulation, - RegenEmissionScope::Affected { - members: vec![s("std_b.rs"), s("v1_rt.rs"), s("not_in_the_tree.rs")], - }, - ] { - let host: BTreeSet = scope_selection(&scope, &committed()) - .expect("the host selects") - .into_iter() - .collect(); - let model: BTreeSet = render_scope_selection(&roots(), &scope, &committed()) - .expect("the model selects") - .into_iter() - .collect(); - assert_eq!(host, model, "scope {scope:?}"); - } + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + for scope in [ + RegenEmissionScope::WholePopulation, + RegenEmissionScope::Affected { + members: vec![s("std_b.rs"), s("v1_rt.rs"), s("not_in_the_tree.rs")], + }, + ] { + let host: BTreeSet = scope_selection(&scope, &committed()) + .expect("the host selects") + .into_iter() + .collect(); + let model: BTreeSet = + render_scope_selection(&roots(), &scope, &committed()) + .expect("the model selects") + .into_iter() + .collect(); + assert_eq!(host, model, "scope {scope:?}"); + } + }); } /// The model's refusal arm is REACHABLE and carries no members -- the same fact the host's /// `Err` carries, asserted on the side that owns the vocabulary. #[test] fn the_model_selects_nothing_on_the_unlocatable_arm() { - require_measurable_host_budget(); - let scope = RegenEmissionScope::Unlocatable { - paths: vec![s("dag/std/departed.dag")], - reason: s("regen-affected-set: EditedSetUnlocatable unlocatable=1"), - }; - assert!(render_scope_selection(&roots(), &scope, &committed()) - .expect("the model answers") - .is_empty()); + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let scope = RegenEmissionScope::Unlocatable { + paths: vec![s("dag/std/departed.dag")], + reason: s("regen-affected-set: EditedSetUnlocatable unlocatable=1"), + }; + assert!(render_scope_selection(&roots(), &scope, &committed()) + .expect("the model answers") + .is_empty()); + }); } } @@ -8679,63 +8712,67 @@ mod regen_affected_set_tests { #[test] fn host_walk_and_model_fold_agree_on_the_fixture_graph() { - require_measurable_host_budget(); - let host = regen_reverse_closure_host(&[s("std.a")], &fixture_edges()); - assert_eq!( - host, - ["std.a", "std.b", "gunbc.c", "v1.compiler.emit_rust"] + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let host = regen_reverse_closure_host(&[s("std.a")], &fixture_edges()); + assert_eq!( + host, + ["std.a", "std.b", "gunbc.c", "v1.compiler.emit_rust"] + .iter() + .map(|m| s(m)) + .collect::>() + ); + let bound = render_affected_set_bound( + &roots(), + &[s("std.a")], + &[], + &fixture_edges(), + &fixture_compared(), + ) + .expect("the model answers on the fixture"); + assert_eq!(bound.arm, "AffectedMirrors"); + assert_eq!( + bound.line, + "regen-affected-set: AffectedMirrors edited=1 mirrors=4 bootstrap_products=0" + ); + let members: BTreeSet = bound.members.into_iter().collect(); + assert_eq!( + members, + [ + "std_a.rs", + "std_b.rs", + "gunbc_c.rs", + "v1_compiler_emit_rust.rs" + ] .iter() .map(|m| s(m)) - .collect::>() - ); - let bound = render_affected_set_bound( - &roots(), - &[s("std.a")], - &[], - &fixture_edges(), - &fixture_compared(), - ) - .expect("the model answers on the fixture"); - assert_eq!(bound.arm, "AffectedMirrors"); - assert_eq!( - bound.line, - "regen-affected-set: AffectedMirrors edited=1 mirrors=4 bootstrap_products=0" - ); - let members: BTreeSet = bound.members.into_iter().collect(); - assert_eq!( - members, - [ - "std_a.rs", - "std_b.rs", - "gunbc_c.rs", - "v1_compiler_emit_rust.rs" - ] - .iter() - .map(|m| s(m)) - .collect() - ); + .collect() + ); + }); } /// RED CONTROL: an unlocatable path refuses with no members, and does not widen to the /// population -- the arm is the refusal, and the edited module beside it is not walked. #[test] fn an_unlocatable_edited_path_refuses_and_selects_nothing() { - require_measurable_host_budget(); - let bound = render_affected_set_bound( - &roots(), - &[s("std.a")], - &[s( - "dag/std/gone.dag (departed: no module line remains in the tree)", - )], - &fixture_edges(), - &fixture_compared(), - ) - .expect("the refusal is an arm, not an error"); - assert_eq!(bound.arm, "EditedSetUnlocatable"); - assert!(bound.members.is_empty()); - assert!(bound - .line - .starts_with("regen-affected-set: EditedSetUnlocatable unlocatable=1 reason=")); + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let bound = render_affected_set_bound( + &roots(), + &[s("std.a")], + &[s( + "dag/std/gone.dag (departed: no module line remains in the tree)", + )], + &fixture_edges(), + &fixture_compared(), + ) + .expect("the refusal is an arm, not an error"); + assert_eq!(bound.arm, "EditedSetUnlocatable"); + assert!(bound.members.is_empty()); + assert!(bound + .line + .starts_with("regen-affected-set: EditedSetUnlocatable unlocatable=1 reason=")); + }); } /// The edit reader on a synthetic diff: an existing module is named, a departed `.dag` and an @@ -8791,65 +8828,73 @@ diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs /// edit is the whole population. #[test] fn live_tree_controls_land_on_the_measured_arms() { - require_measurable_host_budget(); - let workspace = workspace_root(); - let (edges, modules) = regen_module_edges(&workspace).expect("the closure edge index maps"); - let compared = compared_mirror_rows(&workspace.join("src/v1/stage0/src"), &modules) - .expect("committed population"); - assert!(compared.iter().any(|(m, _)| m == "std.content_hash")); - - let leaf = - render_affected_set_bound(&roots(), &[s("std.content_hash")], &[], &edges, &compared) - .expect("leaf edit answers"); - assert_eq!(leaf.arm, "AffectedMirrors", "{}", leaf.line); - assert!( - leaf.members.iter().any(|m| m == "std_content_hash.rs"), - "{:?}", - leaf.members - ); - assert!( - !leaf.members.iter().any(|m| m == "v1_rt.rs"), - "{:?}", - leaf.members - ); - assert!( - leaf.members.len() < compared.len(), - "the leaf bound is a proper subset" - ); + crate::cli_run::on_live_pool_thread(|| { + require_measurable_host_budget(); + let workspace = workspace_root(); + let (edges, modules) = + regen_module_edges(&workspace).expect("the closure edge index maps"); + let compared = compared_mirror_rows(&workspace.join("src/v1/stage0/src"), &modules) + .expect("committed population"); + assert!(compared.iter().any(|(m, _)| m == "std.content_hash")); + + let leaf = render_affected_set_bound( + &roots(), + &[s("std.content_hash")], + &[], + &edges, + &compared, + ) + .expect("leaf edit answers"); + assert_eq!(leaf.arm, "AffectedMirrors", "{}", leaf.line); + assert!( + leaf.members.iter().any(|m| m == "std_content_hash.rs"), + "{:?}", + leaf.members + ); + assert!( + !leaf.members.iter().any(|m| m == "v1_rt.rs"), + "{:?}", + leaf.members + ); + assert!( + leaf.members.len() < compared.len(), + "the leaf bound is a proper subset" + ); - let template = render_affected_set_bound( - &roots(), - &[s("v1.compiler.runtime_rust")], - &[], - &edges, - &compared, - ) - .expect("template edit answers"); - assert_eq!(template.arm, "AffectedMirrors", "{}", template.line); - assert!( - template - .members - .iter() - .any(|m| m == "v1_compiler_runtime_rust.rs"), - "{:?}", - template.members - ); - assert!( - template.members.iter().any(|m| m == "v1_rt.rs"), - "{:?}", - template.members - ); + let template = render_affected_set_bound( + &roots(), + &[s("v1.compiler.runtime_rust")], + &[], + &edges, + &compared, + ) + .expect("template edit answers"); + assert_eq!(template.arm, "AffectedMirrors", "{}", template.line); + assert!( + template + .members + .iter() + .any(|m| m == "v1_compiler_runtime_rust.rs"), + "{:?}", + template.members + ); + assert!( + template.members.iter().any(|m| m == "v1_rt.rs"), + "{:?}", + template.members + ); - let emitter = render_affected_set_bound( - &roots(), - &[s("v1.compiler.emit_rust")], - &[], - &edges, - &compared, - ) - .expect("emitter edit answers"); - assert_eq!(emitter.arm, "WholePopulation", "{}", emitter.line); - assert!(emitter.members.is_empty()); + let emitter = render_affected_set_bound( + &roots(), + &[s("v1.compiler.emit_rust")], + &[], + &edges, + &compared, + ) + .expect("emitter edit answers"); + assert_eq!(emitter.arm, "WholePopulation", "{}", emitter.line); + assert!(emitter.members.is_empty()); + }); } } From 3905160aa8427375a826f8ac5fe7ec12bb872eb1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 28 Sep 2026 22:54:01 +0000 Subject: [PATCH 2/4] Live-pool thread: fixture under target/, and drop per-entry graphs between claims The route control's fixture root sat under the system temp dir, which the module-graph facts refuse (repo-relative normalization); it now lives under the workspace target/ like the other pool fixtures. The thread kept every claim's resolved entry graphs, so its resident set grew claim by claim: 39 live-pool claims peaked at 10.6 GiB (main: 7.1 GiB, index per test) against the hosted runner's 12 GiB memory.max, and the CI unit step stalled there. Per-entry graphs are not shared (no later claim asks for the same entry) and are now dropped after each claim; the pool facts, censuses and the capped typed cache stay. Peak 8.8 GiB, wall 387s vs 1789s on main for the same claims (24 GB BuildBuddy runner). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run/entry_resolve.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 2b10e35a810..6237a2f8a8d 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -741,6 +741,7 @@ pub(crate) fn on_live_pool_thread( sender .send(Box::new(move || { let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)); + release_per_entry_graphs_on_live_pool_thread(); let _ = done_tx.send(outcome); })) .expect("the live-pool thread accepts work"); @@ -750,6 +751,22 @@ pub(crate) fn on_live_pool_thread( } } +/// What the live-pool thread keeps between claims, and what it drops. The shared FACT is the +/// pool: its module index, graph facts, heads parse, censuses and the capped typed-module cache, +/// which every claim's entry reuses. A claim's own resolved entry graphs are not shared -- no +/// later claim asks for the same entry -- so holding them would only grow the thread's resident +/// set claim by claim (measured: 39 live-pool claims peaked at 10.6 GiB with them held, against a +/// 12 GiB `memory.max` on the hosted runner). They are dropped after every claim. +#[cfg(test)] +fn release_per_entry_graphs_on_live_pool_thread() { + PROCESS_RESOLVE_STORE.with(|s| s.borrow_mut().clear()); + PROCESS_RESOLVE_INDEX.with(|slots| { + for (_, index) in slots.borrow().iter().flatten() { + clear_resolved_graph_memo_for_test(index); + } + }); +} + /// Fallible twin of `process_shared_index`. The MEMO IS ONLY WRITTEN ON SUCCESS -- a failed /// discovery must not install a partial index that every later caller in the process would /// then read as complete. @@ -2842,7 +2859,9 @@ mod live_pool_thread_tests { use super::*; fn one_module_pool(tag: &str) -> (PathBuf, Vec) { - let root = std::env::temp_dir().join(format!( + // Under the workspace `target/` (gitignored): the module-graph facts normalize every pool + // path repo-relative and refuse one outside the workspace. + let root = process_workspace_root().join("target").join(format!( "gunbc-live-pool-{tag}-{}-{}", std::process::id(), std::time::SystemTime::now() From b29cf8d4307a8cd8cb00c583c97ff7643ffe99dd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Tue, 29 Sep 2026 03:40:05 +0000 Subject: [PATCH 3/4] Live-pool thread: trim retained heap before each claim, release the pool when its block ends Reproduced the CI unit-step stall under the hosted runner's exact bind (a 12 GiB memory.max cgroup leaf, 24 GB BuildBuddy runner): - 7.6 GiB of freed-but-retained heap from earlier tests' glibc arenas sat under the first claim, and the pool built on top of it was OOM-killed. trim_retained_heap (the existing malloc_trim instrument) now runs before and after each claim: 1.4 GiB under the first claim. - The ~5.2 GiB pool then outlived its block and the next test's own regen-pool index was OOM-killed on top of it. The thread now exits after LIVE_POOL_IDLE_RELEASE (2s) with no claim, dropping the pool; a later claim rebuilds it. Send and exit share one lock, so no claim is lost to the exit. New control: the_live_pool_is_released_when_its_block_of_claims_ends. Full suite under the bound: 1006 passed, 7 failed (6 on #12450's list of reds on main; the_authority_entry_resolves_from_a_non_root_cwd passed on hosted CI at 7c2d8ce and is not on this path), 529.6s, memory.events max=0 oom=0. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run/entry_resolve.rs | 133 ++++++++++++++++----- 1 file changed, 106 insertions(+), 27 deletions(-) diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 1944a21d0f2..a1b17285340 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -719,37 +719,100 @@ pub fn process_shared_index(source_roots: &[String]) -> Rc { /// calling test's thread with its original payload, so a failing claim reds exactly as before and /// the worker survives for the next one. #[cfg(test)] +type LivePoolJob = Box; + +/// The live-pool thread's inbox, `None` while no thread is alive. Senders hold this lock while they +/// send, and the thread takes it before it exits, so a claim can never be sent to a thread that is +/// already leaving. +#[cfg(test)] +static LIVE_POOL_THREAD: Mutex>> = Mutex::new(None); + +/// How long the live-pool thread waits for the next claim before it releases the pool. Claims in +/// one block of the suite arrive back to back, milliseconds apart; a test after the block that +/// builds a pool of its own must not do it on top of this one (measured under the hosted runner's +/// 12 GiB `memory.max`: the ~5.2 GiB pool held past the block, plus the regen-pool index of the next +/// test, was OOM-killed at the bound). A gap this long ends the block. It decides only how often +/// the pool is built, never what a claim answers: a claim after a release rebuilds it. +#[cfg(test)] +const LIVE_POOL_IDLE_RELEASE: std::time::Duration = std::time::Duration::from_secs(2); + +#[cfg(test)] +fn spawn_live_pool_thread() -> std::sync::mpsc::Sender { + use std::sync::mpsc::RecvTimeoutError; + let (tx, rx) = std::sync::mpsc::channel::(); + std::thread::Builder::new() + .name("live-pool".to_string()) + // The largest stack any live-pool claim spawned for itself before it ran here. + .stack_size(64 * 1024 * 1024) + .spawn(move || { + loop { + match rx.recv_timeout(LIVE_POOL_IDLE_RELEASE) { + Ok(job) => job(), + Err(RecvTimeoutError::Timeout) => { + let mut slot = LIVE_POOL_THREAD + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Ok(job) = rx.try_recv() { + drop(slot); + job(); + continue; + } + *slot = None; + break; + } + Err(RecvTimeoutError::Disconnected) => break, + } + } + // The pool leaves with the thread: drop the index slots now and hand the freed heap + // back, rather than leave it for the thread-local destructors after the trim could run. + reset_process_shared_index_for_test(); + trim_retained_heap(); + }) + .expect("the live-pool thread starts"); + tx +} + +/// THE LIVE POOL'S ONE HOLDER UNDER TEST. `process_shared_index` is per-thread because the index +/// is `Rc`-based, and libtest runs every test on a fresh thread -- so every claim that resolves an +/// entry over the live `[dag, src/v2]` pool rebuilt the whole-pool index and re-typechecked the +/// shared prefix, the same fact once per claim with its least common ancestor at the PROCESS +/// (DESIGN §2 demand minimization; gunbc#12450 measured ~18 such claims at ~90s each). The +/// entry-scoped loader cannot stand in: bare references resolve through a census of the whole pool +/// (`admit_pool_bare_references`), so no entry resolves without it. +/// +/// So the fact moves to its ancestor: one thread holds the index in ITS thread-local slot for as +/// long as claims keep arriving, and a live-pool claim runs its body there. Nothing about +/// resolution changes -- the same `process_shared_index` builds the same pool on first demand -- +/// only how many times. `RUST_TEST_THREADS=1` (`.cargo/config.toml`) already serializes the suite, +/// so the queue costs no parallelism. The thread releases the pool when the block of claims ends +/// (`LIVE_POOL_IDLE_RELEASE`). A panic in the body is caught on the thread and re-raised on the +/// calling test's thread with its original payload, so a failing claim reds exactly as before and +/// the thread survives for the next one. +#[cfg(test)] pub(crate) fn on_live_pool_thread( body: impl FnOnce() -> T + Send + 'static, ) -> T { - type Job = Box; - static WORKER: OnceLock>> = OnceLock::new(); - let sender = WORKER - .get_or_init(|| { - let (tx, rx) = std::sync::mpsc::channel::(); - std::thread::Builder::new() - .name("live-pool".to_string()) - // The largest stack any live-pool claim spawned for itself before it ran here. - .stack_size(64 * 1024 * 1024) - .spawn(move || { - for job in rx { - job(); - } - }) - .expect("the live-pool thread starts"); - Mutex::new(tx) - }) - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .clone(); let (done_tx, done_rx) = std::sync::mpsc::channel(); - sender - .send(Box::new(move || { - let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)); - release_per_entry_graphs_on_live_pool_thread(); - let _ = done_tx.send(outcome); - })) - .expect("the live-pool thread accepts work"); + let job: LivePoolJob = Box::new(move || { + // Freed-but-retained heap from the tests that ran on their own threads before this claim + // is returned first: those threads' glibc arenas are not the one this long-lived thread + // allocates from, so without the trim the pool is built ON TOP of their residue (measured + // under the hosted runner's 12 GiB `memory.max`: 7.6 GiB retained before the first claim, + // OOM-killed at the bound as the pool was built; 1.4 GiB with the trim). + trim_retained_heap(); + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)); + release_per_entry_graphs_on_live_pool_thread(); + trim_retained_heap(); + let _ = done_tx.send(outcome); + }); + { + let mut slot = LIVE_POOL_THREAD + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + slot.get_or_insert_with(spawn_live_pool_thread) + .send(job) + .expect("the live-pool thread accepts work"); + } match done_rx.recv().expect("the live-pool thread answers") { Ok(value) => value, Err(payload) => std::panic::resume_unwind(payload), @@ -3154,6 +3217,22 @@ mod live_pool_thread_tests { assert_ne!(first, elsewhere, "a fresh thread builds its own"); } + /// THE POOL DOES NOT OUTLIVE ITS BLOCK. A claim after an idle gap longer than + /// `LIVE_POOL_IDLE_RELEASE` meets a rebuilt index: the red this discriminates is a thread that + /// holds the pool for the rest of the suite, under every later test's own pool. + #[test] + fn the_live_pool_is_released_when_its_block_of_claims_ends() { + let (root, roots) = one_module_pool("release"); + let generation = |roots: Vec| { + on_live_pool_thread(move || process_shared_index(&roots).generation) + }; + let first = generation(roots.clone()); + std::thread::sleep(LIVE_POOL_IDLE_RELEASE + std::time::Duration::from_secs(1)); + let after_idle = generation(roots); + let _ = std::fs::remove_dir_all(root); + assert_ne!(first, after_idle, "the pool outlived an idle gap"); + } + /// A claim that fails on the live-pool thread reds its own test with its own message, and the /// thread survives to serve the next claim. #[test] From 101130af1f0bf5b9e415fe4688b7eb1acd11a4fd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Tue, 29 Sep 2026 04:20:48 +0000 Subject: [PATCH 4/4] Live-pool thread: release the pool on the conflicting build, not on a timer Per neat-boar-16: a 2s idle exit made the memory bound depend on scheduling. The release is now tied to the demand that conflicts with the pool: every whole-pool index build (try_process_shared_index_for_pool, build_multi_entry_index, the primary-precedence builder) first calls yield_live_pool_before_building_another, which ends the live-pool thread (dropping its index and every thread-local cache over it), joins it, and trims. A claim's own nested builds are exempt (LIVE_POOL_BUSY). The idle timer is kept only as a 10s leak backstop for tests that build no pool. Control: another_threads_pool_build_releases_the_live_pool (back-to-back claims share one generation; after another thread's build, the next claim's differs). Full suite under a 12 GiB memory.max leaf (24 GB BuildBuddy runner): 521.27s, memory.events oom=0, sampled peak 10.1 GiB. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run.rs | 4 + src/v1/stage0/src/cli_run/entry_resolve.rs | 129 ++++++++++++++------- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index dddf9c94ea6..3b37129aeb3 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -12982,6 +12982,8 @@ pub use shared_typecheck_store::{ }; pub fn build_multi_entry_index(source_roots: &[String]) -> MultiEntryIndex { + #[cfg(test)] + yield_live_pool_before_building_another(); new_multi_entry_index_shell(build_module_index(source_roots), source_roots, None) } @@ -12992,6 +12994,8 @@ pub fn build_multi_entry_index(source_roots: &[String]) -> MultiEntryIndex { /// requires the `MultiEntryIndex` for its per-tree bare census), dissolving the §3 /// closure-authority fork the two loaders' doc-comments each falsely claimed to be single. fn build_multi_entry_index_primary_precedence(source_roots: &[String]) -> MultiEntryIndex { + #[cfg(test)] + yield_live_pool_before_building_another(); new_multi_entry_index_shell( build_module_index_primary_precedence(source_roots), source_roots, diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index a1b17285340..19df38075a9 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -703,73 +703,102 @@ pub fn process_shared_index(source_roots: &[String]) -> Rc { try_process_shared_index(source_roots).unwrap_or_else(|e| panic!("{e}")) } -/// THE LIVE POOL'S ONE HOLDER UNDER TEST. `process_shared_index` is per-thread because the index -/// is `Rc`-based, and libtest runs every test on a fresh thread -- so every claim that resolves an -/// entry over the live `[dag, src/v2]` pool rebuilt the whole-pool index and re-typechecked the -/// shared prefix, the same fact once per claim with its least common ancestor at the PROCESS -/// (DESIGN §2 demand minimization; gunbc#12450 measured ~18 such claims at ~90s each). The -/// entry-scoped loader cannot stand in: bare references resolve through a census of the whole pool -/// (`admit_pool_bare_references`), so no entry resolves without it. -/// -/// So the fact moves to its ancestor: one thread lives for the test process, the index lives in -/// ITS thread-local slot, and a live-pool claim runs its body there. Nothing about resolution -/// changes -- the same `process_shared_index` builds the same pool on first demand -- only how -/// many times. `RUST_TEST_THREADS=1` (`.cargo/config.toml`) already serializes the suite, so the -/// queue costs no parallelism. A panic in the body is caught on the worker and re-raised on the -/// calling test's thread with its original payload, so a failing claim reds exactly as before and -/// the worker survives for the next one. #[cfg(test)] type LivePoolJob = Box; -/// The live-pool thread's inbox, `None` while no thread is alive. Senders hold this lock while they -/// send, and the thread takes it before it exits, so a claim can never be sent to a thread that is -/// already leaving. +/// The live-pool thread's inbox and handle, `None` while no thread is alive. Senders hold this lock +/// while they send, and every path that ends the thread takes it first, so a claim is never sent to +/// a thread that is already leaving. #[cfg(test)] -static LIVE_POOL_THREAD: Mutex>> = Mutex::new(None); - -/// How long the live-pool thread waits for the next claim before it releases the pool. Claims in -/// one block of the suite arrive back to back, milliseconds apart; a test after the block that -/// builds a pool of its own must not do it on top of this one (measured under the hosted runner's -/// 12 GiB `memory.max`: the ~5.2 GiB pool held past the block, plus the regen-pool index of the next -/// test, was OOM-killed at the bound). A gap this long ends the block. It decides only how often -/// the pool is built, never what a claim answers: a claim after a release rebuilds it. +static LIVE_POOL_THREAD: Mutex< + Option<( + std::sync::mpsc::Sender, + std::thread::JoinHandle<()>, + )>, +> = Mutex::new(None); + +/// Set while the live-pool thread runs a claim. A pool built by a thread the claim itself spawned +/// is that claim's business and must not wait on the thread that is running it. #[cfg(test)] -const LIVE_POOL_IDLE_RELEASE: std::time::Duration = std::time::Duration::from_secs(2); +static LIVE_POOL_BUSY: AtomicBool = AtomicBool::new(false); +/// A LEAK BACKSTOP, not the release. The pool is released by the demand that conflicts with it +/// (`yield_live_pool_before_building_another`). This timer only bounds how long it can outlive its +/// block when no later test builds a pool of its own, so it never decides what a claim answers and +/// the bound does not depend on it for any test that builds one. #[cfg(test)] -fn spawn_live_pool_thread() -> std::sync::mpsc::Sender { +const LIVE_POOL_IDLE_BACKSTOP: std::time::Duration = std::time::Duration::from_secs(10); + +#[cfg(test)] +fn spawn_live_pool_thread() -> ( + std::sync::mpsc::Sender, + std::thread::JoinHandle<()>, +) { use std::sync::mpsc::RecvTimeoutError; let (tx, rx) = std::sync::mpsc::channel::(); - std::thread::Builder::new() + let handle = std::thread::Builder::new() .name("live-pool".to_string()) // The largest stack any live-pool claim spawned for itself before it ran here. .stack_size(64 * 1024 * 1024) .spawn(move || { loop { - match rx.recv_timeout(LIVE_POOL_IDLE_RELEASE) { - Ok(job) => job(), + match rx.recv_timeout(LIVE_POOL_IDLE_BACKSTOP) { + Ok(job) => { + LIVE_POOL_BUSY.store(true, Ordering::SeqCst); + job(); + LIVE_POOL_BUSY.store(false, Ordering::SeqCst); + } Err(RecvTimeoutError::Timeout) => { let mut slot = LIVE_POOL_THREAD .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); if let Ok(job) = rx.try_recv() { drop(slot); + LIVE_POOL_BUSY.store(true, Ordering::SeqCst); job(); + LIVE_POOL_BUSY.store(false, Ordering::SeqCst); continue; } + // Detach: nobody joins a thread that leaves on its own. *slot = None; break; } + // The inbox was taken by `yield_live_pool_before_building_another`, which joins. Err(RecvTimeoutError::Disconnected) => break, } } - // The pool leaves with the thread: drop the index slots now and hand the freed heap - // back, rather than leave it for the thread-local destructors after the trim could run. + // Drop the pool now and hand the freed heap back, rather than leave it for the + // thread-local destructors after the trim could run. reset_process_shared_index_for_test(); trim_retained_heap(); }) .expect("the live-pool thread starts"); - tx + (tx, handle) +} + +/// THE PROCESS HOLDS ONE LIVE WHOLE-POOL INDEX AT A TIME. Called by every whole-pool index build +/// (`try_process_shared_index_for_pool`, `build_multi_entry_index`): before any other thread builds +/// one, the live-pool thread's pool is released -- the thread exits, which drops its index and +/// every thread-local cache built over it, and the freed heap is handed back. This is the release, +/// tied to the demand that conflicts with the pool rather than to a clock (measured under the +/// hosted runner's 12 GiB `memory.max`: the ~5.2 GiB pool held past its block, plus the next test's +/// own index, was OOM-killed at the bound). A claim's own nested builds are exempt: the live-pool +/// thread itself, and any thread a running claim spawned (`LIVE_POOL_BUSY`). +#[cfg(test)] +pub(crate) fn yield_live_pool_before_building_another() { + if std::thread::current().name() == Some("live-pool") || LIVE_POOL_BUSY.load(Ordering::SeqCst) { + return; + } + let taken = LIVE_POOL_THREAD + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take(); + let Some((sender, handle)) = taken else { + return; + }; + drop(sender); + handle.join().expect("the live-pool thread leaves cleanly"); + trim_retained_heap(); } /// THE LIVE POOL'S ONE HOLDER UNDER TEST. `process_shared_index` is per-thread because the index @@ -784,8 +813,8 @@ fn spawn_live_pool_thread() -> std::sync::mpsc::Sender { /// long as claims keep arriving, and a live-pool claim runs its body there. Nothing about /// resolution changes -- the same `process_shared_index` builds the same pool on first demand -- /// only how many times. `RUST_TEST_THREADS=1` (`.cargo/config.toml`) already serializes the suite, -/// so the queue costs no parallelism. The thread releases the pool when the block of claims ends -/// (`LIVE_POOL_IDLE_RELEASE`). A panic in the body is caught on the thread and re-raised on the +/// so the queue costs no parallelism. The pool is released when another thread builds one +/// (`yield_live_pool_before_building_another`). A panic in the body is caught on the thread and re-raised on the /// calling test's thread with its original payload, so a failing claim reds exactly as before and /// the thread survives for the next one. #[cfg(test)] @@ -810,6 +839,7 @@ pub(crate) fn on_live_pool_thread( .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); slot.get_or_insert_with(spawn_live_pool_thread) + .0 .send(job) .expect("the live-pool thread accepts work"); } @@ -899,6 +929,8 @@ pub fn try_process_shared_index_for_pool( if let Some(idx) = existing { return Ok(idx); } + #[cfg(test)] + yield_live_pool_before_building_another(); let build_started = std::time::Instant::now(); let walk_started = std::time::Instant::now(); let module_index = if primary_precedence { @@ -3217,20 +3249,31 @@ mod live_pool_thread_tests { assert_ne!(first, elsewhere, "a fresh thread builds its own"); } - /// THE POOL DOES NOT OUTLIVE ITS BLOCK. A claim after an idle gap longer than - /// `LIVE_POOL_IDLE_RELEASE` meets a rebuilt index: the red this discriminates is a thread that - /// holds the pool for the rest of the suite, under every later test's own pool. + /// THE POOL DOES NOT OUTLIVE ITS BLOCK. When another thread builds a pool of its own, the + /// live-pool thread's pool is released first, so the next claim meets a rebuilt index. The red + /// this discriminates is a thread that holds its pool under every later test's own pool. #[test] - fn the_live_pool_is_released_when_its_block_of_claims_ends() { + fn another_threads_pool_build_releases_the_live_pool() { let (root, roots) = one_module_pool("release"); + let (other_root, other_roots) = one_module_pool("release-other"); let generation = |roots: Vec| { on_live_pool_thread(move || process_shared_index(&roots).generation) }; let first = generation(roots.clone()); - std::thread::sleep(LIVE_POOL_IDLE_RELEASE + std::time::Duration::from_secs(1)); - let after_idle = generation(roots); + let held = generation(roots.clone()); + std::thread::spawn(move || { + process_shared_index(&other_roots); + }) + .join() + .unwrap(); + let after_release = generation(roots); let _ = std::fs::remove_dir_all(root); - assert_ne!(first, after_idle, "the pool outlived an idle gap"); + let _ = std::fs::remove_dir_all(other_root); + assert_eq!(first, held, "back-to-back claims share the pool"); + assert_ne!( + first, after_release, + "the pool outlived another thread's build" + ); } /// A claim that fails on the live-pool thread reds its own test with its own message, and the