From bc31d1196f7e787eab53d8f918be7983ad912357 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 16 Sep 2026 17:51:31 +0200 Subject: [PATCH] Put the elaboration call site and the quarantine stop under test (L1 Cut 4) - Dungeon: the body of elaborate_world's loop becomes run_elaboration_sweep(&state). It checks quarantine, runs the sweep on state.elaboration_permits, and logs any error at debug, exactly as before. The interval loop is unchanged. - saturating_one_pool_never_delays_the_other now runs its second half through run_elaboration_sweep. The production call site is therefore the one tested: passing controller_permits there fails the test, which was not true when the test called sweep directly. - Library: no_session_starts_after_a_quarantine_error pins the sweep's stop rule. The pool holds one permit and the first session faults with an integrity violation, so the second entry never reaches the provider. This commit builds. Library 457 + 1 ignored, Dungeon 53 + 1 ignored. Co-Authored-By: Claude Opus 5 --- crates/ghostlight-dungeon/src/runtime.rs | 44 ++++++++++++------------ crates/ghostlight/src/controllers.rs | 29 ++++++++++++++++ 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/crates/ghostlight-dungeon/src/runtime.rs b/crates/ghostlight-dungeon/src/runtime.rs index 6d8a534..7a7da9a 100644 --- a/crates/ghostlight-dungeon/src/runtime.rs +++ b/crates/ghostlight-dungeon/src/runtime.rs @@ -1829,26 +1829,31 @@ const ELABORATION_SWEEP_INTERVAL: Duration = Duration::from_secs(300); /// The authoring lane's only driver. It runs when the cognition organ opened at /// all, which is the config gate the runtime already has: no mode flag joins it. -/// Each sweep runs its sessions under the elaboration pool and never touches the -/// simulation pool. Sweeps never overlap, because this loop awaits one before it -/// ticks the next, so one sweep's claim set is the only set of sessions alive. +/// Sweeps never overlap, because this loop awaits one before it ticks the next, +/// so one sweep's claim set is the only set of sessions alive. async fn elaborate_world(state: AppState) { let mut interval = tokio::time::interval(ELABORATION_SWEEP_INTERVAL); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); interval.tick().await; loop { interval.tick().await; - let Some(runner) = state - .controllers - .as_deref() - .filter(|_| !state.controller_quarantined.load(Ordering::SeqCst)) - .map(ControllerRunner::elaborator) - else { - continue; - }; - if let Err(error) = runner.sweep(state.elaboration_permits.clone()).await { - tracing::debug!(%error, "elaboration sweep did not complete"); - } + run_elaboration_sweep(&state).await; + } +} + +/// One sweep, apart from the wall clock that decides when to run it. Its +/// sessions run under the elaboration pool and never touch the simulation pool. +async fn run_elaboration_sweep(state: &AppState) { + let Some(runner) = state + .controllers + .as_deref() + .filter(|_| !state.controller_quarantined.load(Ordering::SeqCst)) + .map(ControllerRunner::elaborator) + else { + return; + }; + if let Err(error) = runner.sweep(state.elaboration_permits.clone()).await { + tracing::debug!(%error, "elaboration sweep did not complete"); } } @@ -3398,14 +3403,9 @@ mod tests { .clone() .try_acquire_many_owned(u32::try_from(TEST_CONTROLLER_CONCURRENCY).expect("a small pool")) .expect("the simulation pool is free"); - let runner = state.controllers.clone().expect("the runner"); - tokio::time::timeout( - Duration::from_secs(30), - runner.elaborator().sweep(state.elaboration_permits.clone()), - ) - .await - .expect("the sweep waited on the simulation pool") - .expect("the sweep completed"); + tokio::time::timeout(Duration::from_secs(30), run_elaboration_sweep(&state)) + .await + .expect("the sweep waited on the simulation pool"); assert!( port.calls.load(Ordering::SeqCst) > before, "no elaboration inference completed" diff --git a/crates/ghostlight/src/controllers.rs b/crates/ghostlight/src/controllers.rs index 23214d4..7242314 100644 --- a/crates/ghostlight/src/controllers.rs +++ b/crates/ghostlight/src/controllers.rs @@ -11291,6 +11291,35 @@ mod tests { task.await.unwrap(); } + /// No session starts after one has returned a quarantine-class error. Under + /// a pool of one the second entry waits for the first session's permit; the + /// first session faults with an integrity violation, and the second never + /// reaches the provider boundary. + #[tokio::test] + async fn no_session_starts_after_a_quarantine_error() { + let (_directory, mailbox, task, _commons, _roads) = dead_end_world( + crate::tests::stock_weights(), + &["The Unwalked Road", "The Far Road"], + Roots::Commons, + 0, + ) + .await; + let port = Arc::new(SweepPort::new(|_, _| { + Err(InferenceFault::integrity_violation( + "the fixture port disputes every receipt", + )) + })); + let runner = sweep_runner(&mailbox, port.clone(), fresh_store()); + let Err(error) = runner.sweep(pool(1)).await else { + panic!("a quarantine-class fault did not end the sweep with an error"); + }; + assert!(error.requires_quarantine(), "{error:?}"); + assert_eq!(port.calls(), 1, "a session started after a quarantine-class error"); + drop(runner); + drop(mailbox); + task.await.unwrap(); + } + /// Probe 3's deficit half, through the sweep. Sweep one's deficit session /// is refused; a clock tick moves the ancestry, and with it the id a fresh /// derivation would give the deficit. Sweep two finds the refused session by