commit 5e7803ba2e35078a87ac3b8376c8e30d0edb6a73 Author: Reavo End AuthorDate: Sat Aug 1 22:33:37 2026 -0700 Commit: Reavo End CommitDate: Sat Aug 1 22:33:37 2026 -0700 test(dispatch): close the census tail-index residual from the enum side The shipped census bijection derives BOTH sides it compares from StreamFamily::ALL, so one shape stayed invisible to it: a variant that exists in the enum, carries a census arm claiming the TAIL index, and never reached ALL. ALL.len() is unchanged, every surviving entry still matches its own arm, and claimed == expected still holds -- the variant is simply never visited. Same guard-derives-from-the-thing-it-guards class as instrument-soundness guard 12. Iterating the enum supplies the independent side. strum::EnumIter rides behind cfg_attr(test, ...) so production carries no derive; strum 0.28.0 and strum_macros 0.28.0 were already in Cargo.lock via iroh -> spt-net, so the dev-dep adds one lock EDGE and no new crate (verified by lock diff: a single "strum 0.28.0" line under spt-daemon's dependencies). It also fixes the naming direction. An index-equality failure names the entry that landed at the wrong index -- the absentee's NEIGHBOUR. This row names the absentee. MUTATION-PROVED, two arms, with the mutant shaped exactly as a real commit would leave it (variant added, every wildcard-free match given an arm including census_index => 17, omitted from ALL only): - with this row: RED, naming MutantProbe itself - row neutralised: GREEN, mutant still in place So the residual was real and this row is what closes it. No count row is written beside it: iter().count() == ALL.len() cannot fail unless this loop or the set equality already has, and a row that cannot fail on its own is the vacuous kind this file has deleted before. Gate: dispatch module 26/26 under nextest; clippy --workspace --all-targets -D warnings exit 0; traceable-reqs check exit 0. Co-authored by: hertz diff --git a/Cargo.lock b/Cargo.lock index 2a36d45..4b4c7b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4050,4 +4050,5 @@ dependencies = [ "spt-term", "spt-test-support", + "strum 0.28.0", "tempfile", "tokio", diff --git a/crates/spt-daemon/Cargo.toml b/crates/spt-daemon/Cargo.toml index 047c958..d24095a 100644 --- a/crates/spt-daemon/Cargo.toml +++ b/crates/spt-daemon/Cargo.toml @@ -97,4 +97,12 @@ spt-store = { path = "../spt-store" } # The D4a net-broker int-test constructs identities + relay policies directly. spt-proto = { path = "../spt-proto" } +# TEST-ONLY variant iteration for the StreamFamily census bijection. The census +# check that ships derives BOTH of its sides from `ALL`, so a variant present in +# the enum and absent from `ALL` is invisible to it; iterating the ENUM supplies +# the independent side. Already transitive at this exact version through +# iroh -> spt-net (strum 0.28.0 + strum_macros 0.28.0 are both in Cargo.lock +# before this edit), so the derive feature adds a dev-dep EDGE and no new crate. +# Production stays bare — the derive is behind `cfg_attr(test, ...)`. +strum = { version = "0.28", features = ["derive"] } spt-net = { path = "../spt-net" } # The D5a wanmsg int-test asserts spool formatting through the spt-msg surface. diff --git a/crates/spt-daemon/src/dispatch.rs b/crates/spt-daemon/src/dispatch.rs index 3fff0ca..ffd2be9 100644 --- a/crates/spt-daemon/src/dispatch.rs +++ b/crates/spt-daemon/src/dispatch.rs @@ -90,5 +90,11 @@ pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100); /// What protocol a peer-initiated stream speaks, by its first record's shape. +/// +/// The test-only `EnumIter` derive is what lets the census bijection assert +/// itself from the ENUM side. Without it both halves of that check read `ALL`, +/// and a variant that never reached `ALL` is unreachable by the test that exists +/// to find exactly that. Production carries no strum derive. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(strum::EnumIter))] pub enum StreamFamily { /// A context sync pull (`SyncRecord::Request`). @@ -2073,4 +2079,7 @@ mod tests { use super::*; use serde_json::json; + // Brings `StreamFamily::iter()` into scope for the census bijection's + // enum-side row. Test-only, like the derive it pairs with. + use strum::IntoEnumIterator; // [unit->REQ-NET-1] the demux table: every family's first record @@ -2413,4 +2422,31 @@ mod tests { ); } + // THE RESIDUAL THE TWO CHECKS BELOW CANNOT SEE, closed from the enum + // side. Both the index loop above and the claimed/expected sets below + // derive EVERY value they compare from `ALL` — so a variant that is in + // the enum, carries a census arm claiming the TAIL index, and never + // reached `ALL` leaves `ALL.len()` unchanged, every surviving entry + // still matching its own arm, and `claimed == expected` still holding. + // The variant is simply never visited. Iterating `StreamFamily` is the + // independent side, and it is the only row here that fails on that + // shape. + // + // It also fixes the naming direction: an index-equality failure names + // the entry that landed at the wrong index (the absentee's NEIGHBOUR), + // while this one names the absentee itself. + // + // No count row is written beside it deliberately — `iter().count() == + // ALL.len()` cannot fail unless this loop or the set equality already + // has, and a row that cannot fail on its own is the vacuous kind this + // file has had to delete before. + for f in StreamFamily::iter() { + assert!( + StreamFamily::ALL.contains(&f), + "{f:?} is in the enum and missing from the census ALL — every \ + family walk skips it in silence, so nothing that walks the \ + census can assert anything about it" + ); + } + let claimed: std::collections::BTreeSet = StreamFamily::ALL.iter().map(|f| census_index(*f)).collect(); commit 6e878ec5472d65ec2178e8cfbed82d726ffcac7f Author: Reavo End AuthorDate: Tue Aug 4 13:55:10 2026 -0700 Commit: Reavo End CommitDate: Tue Aug 4 13:55:10 2026 -0700 test(live-resolve): name the exit code and both streams on every failure arm Every failure arm in `live_resolve_e2e` gated on `status` but printed only `stderr`, so a red said nothing about WHICH way the child died: `Some(101)` (child panic), `Some(1)` (clean refusal) and `None` (terminated by signal/job) all read identically. The golden victim red on this row could not be diagnosed for exactly that reason. One `diag()` helper now formats the exit code (Debug-formatted `Option`, so the three cases stay distinguishable), stdout and stderr; all six positive arms and both negative-arm asserts carry it — including the `ADAPTER_UNRESOLVED` grep, which printed stderr but never the code, and the wrong-way-success arm, which printed nothing at all. `output_bounded`'s timeout arm now names the call it was waiting on: the argv is read off the `Command` before it moves into the worker thread, since that arm discards everything captured so far. Diagnostic surface only: no assertion changes, every pin stays pinned to contract. A green run prints none of it. Co-authored by: hertz diff --git a/crates/spt/tests/live_resolve_e2e.rs b/crates/spt/tests/live_resolve_e2e.rs index 0e1e5d2..5cb01fb 100644 --- a/crates/spt/tests/live_resolve_e2e.rs +++ b/crates/spt/tests/live_resolve_e2e.rs @@ -50,4 +50,12 @@ fn start_inproc_daemon() { fn output_bounded(mut cmd: Command, deadline: Duration) -> Output { + // The timeout arm discards everything captured so far — nothing has been read off + // the child yet — so the least it can do is NAME the call that hung. Read the argv + // before the `Command` moves into the worker thread. + let argv: Vec = cmd + .get_args() + .map(|a| a.to_string_lossy().into_owned()) + .collect(); + let argv = argv.join(" "); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { @@ -55,6 +63,24 @@ fn output_bounded(mut cmd: Command, deadline: Duration) -> Output { }); rx.recv_timeout(deadline) - .expect("captured spt call must complete") - .expect("run spt") + .unwrap_or_else(|e| panic!("captured `spt {argv}` did not complete within {deadline:?}: {e}")) + .unwrap_or_else(|e| panic!("running `spt {argv}` failed before it produced output: {e}")) +} + +/// Every failure arm's diagnosis, in ONE place. The recorded gap this closes: the arms +/// below gate on `status` but printed only `stderr`, so a red said nothing about WHICH +/// way the child died. The exit code is the discriminator — `Some(101)` is a child +/// panic, `Some(1)` a clean refusal, `None` terminated by signal/job — and those three +/// read identically through a stderr-only message. stdout is printed for the same +/// reason: a child that failed while writing its answer left the evidence there. +/// +/// Diagnostic surface only: no arm's assertion changes, every pin stays pinned to the +/// contract. A green run prints none of this. +fn diag(label: &str, out: &Output) -> String { + format!( + "{label}: exit={:?} stdout={:?} stderr={:?}", + out.status.code(), + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ) } @@ -98,11 +124,11 @@ fn listen_without_adapter_resolves_from_host_binaries() { // The seed itself carries NO --adapter (adapter-agnostic, REQ-START-5). let out = spt(&["api", "seed", "--pid", &pid, "--session-id", "sid-1"]); - assert!(out.status.success(), "agnostic seed: {}", String::from_utf8_lossy(&out.stderr)); + assert!(out.status.success(), "{}", diag("agnostic seed", &out)); let out = spt(&["api", "listen", "agent-a", "--parent-pid", &pid, "--once"]); assert!( out.status.success(), - "no-adapter listen resolves + binds: {}", - String::from_utf8_lossy(&out.stderr) + "{}", + diag("no-adapter listen resolves + binds", &out) ); let perch_a = perch::resolve_perch_path("agent-a", ParentHint::Infer); @@ -121,10 +147,10 @@ fn listen_without_adapter_resolves_from_host_binaries() { // ── Phase 2: the active-profile pointer overrides the freshest fallback. let out = spt(&["adapter", "use", "older-spt"]); - assert!(out.status.success(), "adapter use: {}", String::from_utf8_lossy(&out.stderr)); + assert!(out.status.success(), "{}", diag("adapter use", &out)); let out = spt(&["api", "seed", "--pid", &pid, "--session-id", "sid-2"]); - assert!(out.status.success(), "re-seed: {}", String::from_utf8_lossy(&out.stderr)); + assert!(out.status.success(), "{}", diag("re-seed (sid-2)", &out)); let out = spt(&["api", "listen", "agent-b", "--parent-pid", &pid, "--once"]); - assert!(out.status.success(), "pointer listen: {}", String::from_utf8_lossy(&out.stderr)); + assert!(out.status.success(), "{}", diag("pointer listen", &out)); let perch_b = perch::resolve_perch_path("agent-b", ParentHint::Infer); assert_eq!( @@ -141,11 +167,22 @@ fn listen_without_adapter_resolves_from_host_binaries() { let out = spt(&["api", "seed", "--pid", &pid, "--session-id", "sid-3"]); - assert!(out.status.success(), "re-seed: {}", String::from_utf8_lossy(&out.stderr)); + assert!(out.status.success(), "{}", diag("re-seed (sid-3)", &out)); let out = spt(&["api", "listen", "agent-c", "--parent-pid", &pid, "--once"]); - assert!(!out.status.success(), "a zero-match resolution must refuse"); + // The WRONG-way arm: a listen that succeeds here printed nothing at all about what + // the child actually did, which is the same blindness as the positive arms. + assert!( + !out.status.success(), + "{}", + diag("a zero-match resolution must refuse", &out) + ); let stderr = String::from_utf8_lossy(&out.stderr); assert!( stderr.contains("ADAPTER_UNRESOLVED") && stderr.contains(&self_bn), - "the friendly refusal names the unhosted binary: {stderr}" + "{}", + diag( + "the friendly refusal names the unhosted binary (expected ADAPTER_UNRESOLVED \ + and the binary basename in stderr)", + &out + ) ); assert!( commit 0d1f3e4e5f33f2f3677ce759725a382d71291282 Author: Reavo End AuthorDate: Tue Aug 4 21:12:47 2026 -0700 Commit: Reavo End CommitDate: Tue Aug 4 21:12:47 2026 -0700 docs: retire the owlery noun from user-facing copy `owlery` is an internal on-disk directory name. Three strings a user reads named it, where the reader has no such noun. Swap the noun only — sentences otherwise unchanged — and regen the CLI reference. - crates/spt/src/cli.rs:572 `endpoint gc` clap doc; renders to docs-site/src/cli/reference.md L2310 + L2675 - crates/spt/src/cli.rs:21997 `endpoint gc` PERCH-GC root-unreadable eprintln - crates/spt-daemon/src/daemon.rs:176 PERCH_CENSUS_SKIPPED eprintln The cli.rs:21997 site sat outside the originally-ruled two-site scope and was ruled in: without it `spt endpoint gc` disagreed with itself — its help would read "perch tree" while its own error still read "the owlery root". Same file, same subcommand, and the direct CLI-side sibling of the daemon.rs:176 line. Noun sourced from CONTEXT.md ("perch directory tree"), not invented. CLASS B untouched and still binding: `owlery` remains the real on-disk directory (`perch::owlery_dir()`, REQ-HAZARD-SINGLE-PATH-SOURCE) — renaming that is a storage migration with a compatibility story, not a copy lane. Co-authored by: hertz diff --git a/crates/spt-daemon/src/daemon.rs b/crates/spt-daemon/src/daemon.rs index a1f4a3d..4b7a777 100644 --- a/crates/spt-daemon/src/daemon.rs +++ b/crates/spt-daemon/src/daemon.rs @@ -174,5 +174,5 @@ impl Daemon { let report = perchgc::sweep(&owlery, false); if !report.root_readable { - eprintln!("PERCH_CENSUS_SKIPPED: owlery unreadable — nothing classified"); + eprintln!("PERCH_CENSUS_SKIPPED: perch tree unreadable — nothing classified"); return; } diff --git a/crates/spt/src/cli.rs b/crates/spt/src/cli.rs index b6f3458..bd0363d 100644 --- a/crates/spt/src/cli.rs +++ b/crates/spt/src/cli.rs @@ -570,5 +570,5 @@ enum EndpointCmd { new_id: String, }, - /// Census the owlery for perch directories that outlived their endpoint. + /// Census the perch tree for perch directories that outlived their endpoint. /// /// REPORTS BY DEFAULT AND DELETES NOTHING. A perch directory is residue only @@ -21995,5 +21995,5 @@ pub(crate) fn cmd_endpoint_gc(reap: bool, json: bool) -> i32 { // Distinct from an empty owlery, which is a legitimate zero. eprintln!( - "PERCH-GC: the owlery root could not be read ({}) — nothing was classified and \ + "PERCH-GC: the perch tree root could not be read ({}) — nothing was classified and \ nothing was removed.", owlery.display() diff --git a/docs-site/src/cli/reference.md b/docs-site/src/cli/reference.md index 89f10a9..a5bf7d3 100644 --- a/docs-site/src/cli/reference.md +++ b/docs-site/src/cli/reference.md @@ -2308,5 +2308,5 @@ Commands: stop Stop an endpoint outright (spool and history preserved) rename Rename an endpoint's logical id across its on-disk state - gc Census the owlery for perch directories that outlived their endpoint + gc Census the perch tree for perch directories that outlived their endpoint purge Permanently remove an endpoint and every record keyed on it digest Show a session's live activity buffer (session digest) @@ -2673,5 +2673,5 @@ Options: ```text -Census the owlery for perch directories that outlived their endpoint. +Census the perch tree for perch directories that outlived their endpoint. REPORTS BY DEFAULT AND DELETES NOTHING. A perch directory is residue only when it carries NO commit 6da6e7e08bc33faab5322459f3ff19e4b640f125 Author: Reavo End AuthorDate: Tue Aug 4 13:55:10 2026 -0700 Commit: Reavo End CommitDate: Tue Aug 4 13:55:10 2026 -0700 test(psyche): per-leg bound + direct-child summarizer for the soft-budget int `psyche_real_bound_kill_soft_budget_e2e` carried two independent defects on the hfenduleam Windows legs; this addresses both, test-side only. 1. assert-FAIL (1 of 7 recent golden Windows legs: expected `Some(Ok)`, got `Timeout 1s`). The positive control fired the fast summarizer through the SAME 1s bound that exists to make ten real kills cheap, so its pass condition was silently "spawn, run and exit inside 1s on a loaded CI runner" — the runner's deadline starts after `spawn()` returns, so the child's whole startup sits inside the window. The control leg now swaps in its own generous bound through the production `refresh_manifest` seam, on the SAME host: budgets are host state, not manifest state, so the standing soft latch survives the swap and the closing `stamp() == None` row still proves a real success clears a real latch. A second `BrainLifecycle` would start with fresh budgets and make that row vacuous; the header says so, at the site. 2. nextest LEAK (7 of 7 recent golden Windows legs). The slow arm was a `.bat`/`.sh` running `ping`/`sleep`, making the sleeper a GRANDCHILD; the bounded runner kills the direct child only, so each of the ten kills left a sleeper holding inherited stdio. The summarizer is now a fixture binary that sleeps in its own process. The shallow production kill is unchanged and filed separately for triage — this removes the row's DEPENDENCE on kill depth rather than papering over it: the sleep arm still outlives its bound ~6x, so a runner that stopped killing still fails the per-fire Timeout rows. Co-authored by: hertz diff --git a/crates/spt-daemon/Cargo.toml b/crates/spt-daemon/Cargo.toml index 047c958..bd80c27 100644 --- a/crates/spt-daemon/Cargo.toml +++ b/crates/spt-daemon/Cargo.toml @@ -30,4 +30,16 @@ name = "service_fixture" path = "tests/fixtures/service_fixture.rs" +# The `echo_commune` summarizer every REQ-PSYCHE-SOFT-TIMEOUT-BUDGET int fire +# spawns: its slow arm sleeps in its OWN process, so the bounded runner's kill +# (which reaches the DIRECT child only) leaves no sleeping grandchild holding +# inherited stdio past the test — the nextest LEAK the `.bat`/`ping` shape +# produced on every run. Same tests/-homed `[[bin]]` pattern as the fixtures +# above (excluded from the release `--bin spt` build). The name is unique across +# the WORKSPACE (shared target dir ⇒ a duplicate clobbers) and free of +# update/setup/install/patch (the Windows installer-detection UAC-740 class). +[[bin]] +name = "summarizer_fixture" +path = "tests/fixtures/summarizer_fixture.rs" + [dependencies] # Layer-below set (R-ARCH-1 acyclic): …→spt-live→spt-daemon→spt. The broker diff --git a/crates/spt-daemon/tests/fixtures/summarizer_fixture.rs b/crates/spt-daemon/tests/fixtures/summarizer_fixture.rs new file mode 100644 index 0000000..5f89b54 --- /dev/null +++ b/crates/spt-daemon/tests/fixtures/summarizer_fixture.rs @@ -0,0 +1,68 @@ +//! Test-only `echo_commune` summarizer fixture for +//! `tests/psyche_real_bound_kill_soft_budget_e2e.rs`: a summarizer whose slow arm +//! is the DIRECT child of the bounded runner, so the runner's kill reaches the +//! thing that is sleeping. +//! +//! WHY A BINARY AND NOT A SCRIPT. The row's slow arm used to be a `.bat`/`.sh` +//! running `ping -n 6` / `sleep 6`. That makes the sleeper a GRANDCHILD: +//! `wait_bounded` (spt-runtime) kills the direct child only, so every one of the +//! row's ten real bound kills left a sleeper alive for the remainder of its 6s +//! holding inherited stdio handles — which nextest reports as a LEAK. A binary +//! that sleeps in its OWN process has no grandchild to outlive it. The shallow +//! prod kill is unchanged and deliberate (filed separately for triage); this +//! fixture removes the row's DEPENDENCE on kill depth, and does not paper over it: +//! the sleep arm still sleeps well past the bound, so a runner that stopped +//! killing at all still fails the per-fire Timeout rows. +//! +//! Args: ``. The control file is the test's slow → fast SWITCH, +//! rewritten in place between legs (shell/behaviour logic in a file, never inline +//! — REQ-HAZARD-TEMPLATE-ARGV-FILL). One line: +//! +//! - `sleep ` — sleep, then print. Under a bound smaller than `` the +//! print is unreachable, so stdout arriving at all is itself evidence the kill +//! never came. +//! - `emit ` — print `` and exit 0: an honest, fast success whose +//! stdout becomes the commune drop. +//! +//! Anything else exits 2 LOUDLY rather than hanging: a mis-authored control file +//! must not reach the test as a timeout, which is the very class under test. +//! Never ships (tests/-homed `[[bin]]`, excluded from the release build by name). + +use std::time::Duration; + +fn main() { + let control = std::env::args() + .nth(1) + .expect("usage: summarizer_fixture "); + let body = match std::fs::read_to_string(&control) { + Ok(b) => b, + Err(e) => { + eprintln!("SUMMARIZER_FIXTURE_NOCONTROL: {control}: {e}"); + std::process::exit(2); + } + }; + let trimmed = body.trim(); + let (verb, rest) = match trimmed.split_once(char::is_whitespace) { + Some((v, r)) => (v, r.trim()), + None => (trimmed, ""), + }; + match verb { + "sleep" => match rest.parse::() { + Ok(secs) => { + std::thread::sleep(Duration::from_secs(secs)); + // Only reachable if the bound never fired — the test's per-fire + // rows read this as the success it would then be. + println!("SUMMARIZER_FIXTURE_SLEPT {secs}"); + } + Err(e) => { + eprintln!("SUMMARIZER_FIXTURE_BADSECS: {rest:?}: {e}"); + std::process::exit(2); + } + }, + "emit" => println!("{rest}"), + other => { + eprintln!("SUMMARIZER_FIXTURE_BADCONTROL: verb {other:?} in {control}"); + std::process::exit(2); + } + } +} diff --git a/crates/spt-daemon/tests/psyche_real_bound_kill_soft_budget_e2e.rs b/crates/spt-daemon/tests/psyche_real_bound_kill_soft_budget_e2e.rs index 10b9137..fe10606 100644 --- a/crates/spt-daemon/tests/psyche_real_bound_kill_soft_budget_e2e.rs +++ b/crates/spt-daemon/tests/psyche_real_bound_kill_soft_budget_e2e.rs @@ -33,14 +33,43 @@ //! either alone passes a build the other catches. //! -//! POSITIVE CONTROL: the same summarizer script is then rewritten to a fast success -//! and one more real fire runs — the outcome is `Ok`, the drop file is really +//! POSITIVE CONTROL: the same summarizer control file is then rewritten to a fast +//! success and one more real fire runs — the outcome is `Ok`, the drop file is really //! written, and the standing soft latch clears. Without it, an implementation that //! stamped and never cleared would pass everything above. //! +//! THE CONTROL GETS ITS OWN BOUND, AND WHY THAT IS NOT A WEAKENING. The kill run's +//! bound is 1s because ten REAL kills must be cheap; the control needs the opposite — +//! room for an honest success. Run under the kill bound, the control's pass condition +//! silently became "spawn, run and exit inside 1s **on a loaded CI runner**": the +//! runner's deadline starts after `spawn()` returns, so the child's whole startup is +//! inside the window, and this row is where that showed up (one assert-FAIL in seven +//! recent golden Windows legs — `expected Some(Ok), got Timeout 1s`). Nothing the test +//! proves needed that race. So the control leg swaps in a manifest declaring +//! [`CONTROL_BUDGET_SECS`] through the production `refresh_manifest` seam, on the SAME +//! host — and `fire_echo` re-reads both the budget and the runtime from that cell on +//! every fire, so the swap lands on the next fire and nowhere else. +//! +//! The same host is load-bearing, not a convenience: the budgets are HOST state +//! (`BrainLifecycle::budgets`), not manifest state, and `refresh_manifest` swaps only +//! the manifest and its runtime. The standing soft latch and its counter therefore +//! survive into the control leg, which is the only reason the final `stamp() == None` +//! row proves a real success CLEARS a real latch. A SECOND `BrainLifecycle` would +//! start with fresh budgets and make that row vacuous — it must not be "simplified" +//! into one. +//! //! ROLE-DECLARED BOUND, NOT THE DEFAULT: each kill's reason is asserted to name //! `after 1s`, so the `invocation_budget_secs = 1` knob on the `echo_commune` role is //! what bounded the spawn. Under the 90s default this test would not merely fail, it //! would take fifteen minutes — the assertion makes the reason explicit rather than -//! leaving it to wall-clock inference. +//! leaving it to wall-clock inference. The control's own bound is never SPENT (a fast +//! success returns immediately); it only stops the control racing the kill bound. +//! +//! NO GRANDCHILD, BY CONSTRUCTION: the summarizer is a fixture BINARY that sleeps in +//! its own process, not a `.bat`/`.sh` running `ping`/`sleep`. The bounded runner kills +//! the DIRECT child only, so the script shape left a sleeper alive past every one of +//! the ten kills, holding inherited stdio — a nextest LEAK on every run of this row. +//! The shallow prod kill is unchanged and filed separately; this row simply no longer +//! DEPENDS on kill depth. It is not papered over: the sleep arm still outlives the +//! bound ~6x, so a runner that stopped killing fails the per-fire rows above. //! //! RED-FIRST — RUN, not asserted: `EchoError::is_timeout` was made to return `false` @@ -70,8 +99,16 @@ use spt_test_support::TestHome; const ID: &str = "barometer"; -/// The bound the `echo_commune` ROLE declares. Small on purpose: the point is a real -/// kill, and the manifest knob is what makes a real kill cheap to stage. +/// The bound the `echo_commune` ROLE declares for the KILL RUN. Small on purpose: the +/// point is a real kill, and the manifest knob is what makes a real kill cheap to +/// stage. Ten fires ⇒ ~10s of wall clock, so this number is also the row's cost. const BUDGET_SECS: u64 = 1; +/// The bound the same role declares for the POSITIVE CONTROL leg, swapped in through +/// `refresh_manifest`. Generous because the control must not race a bound at all: a +/// bound-killed control is indistinguishable from a build that never recovers, which +/// is precisely the false red this number retires. Never SPENT — a fast success +/// returns in milliseconds — so it costs the row nothing. +const CONTROL_BUDGET_SECS: u64 = 30; + /// The PRODUCTION hard-failure budget (`PSYCHE_TURN_STRIKE_BUDGET`) and the /// PRODUCTION soft timeout budget (`PSYCHE_TIMEOUT_STRIKE_BUDGET`), duplicated here @@ -99,40 +136,32 @@ const TURN_CMD: &str = r#"cmd /C "echo TURN_RAN & findstr x*""#; const TURN_CMD: &str = r#"sh -c "echo TURN_RAN; cat""#; -// The summarizer body that outlives its bound by ~5x — a REAL child the REAL bounded -// runner must kill. `ping -n`/`sleep` is the established sleeper across this suite. -#[cfg(windows)] -const SLOW_BODY: &str = "@ping 127.0.0.1 -n 6 >NUL\r\n"; -#[cfg(unix)] +// The summarizer control body that outlives its bound by ~6x — the fixture binary +// sleeps in its OWN process, so the REAL bounded runner's kill of the DIRECT child is +// the kill of the sleeper (no grandchild survives the row; see the header). const SLOW_BODY: &str = "sleep 6\n"; -// The same script, rewritten for the positive control: a fast, honestly-succeeding -// summarizer whose stdout becomes the commune drop. -#[cfg(windows)] -const FAST_BODY: &str = "@echo COMMUNE_OK\r\n"; -#[cfg(unix)] -const FAST_BODY: &str = "echo COMMUNE_OK\n"; +// The same control file, rewritten for the positive control: a fast, honestly- +// succeeding summarizer whose stdout becomes the commune drop. +const FAST_BODY: &str = "emit COMMUNE_OK\n"; -/// The summarizer's script FILE and the role command that runs it. Shell logic lives -/// in a file, never inline (REQ-HAZARD-TEMPLATE-ARGV-FILL, the F-030 lesson) — and the -/// file is also the switch: the test rewrites its body to flip slow → fast without -/// touching the manifest. +/// The summarizer's CONTROL FILE and the role command that runs the fixture binary +/// against it. Behaviour lives in a file, never inline (REQ-HAZARD-TEMPLATE-ARGV-FILL, +/// the F-030 lesson) — and the file is also the switch: the test rewrites its body to +/// flip slow → fast without touching the manifest. Both tokens are quoted for +/// `spt_runtime::tokenize`, so a target dir or home with a space in it stays one token. fn summarizer_script(home: &Path) -> (PathBuf, String) { - #[cfg(windows)] - { - let path = home.join("summarizer.bat"); - let cmd = format!("cmd /C \"{}\"", path.display()); - (path, cmd) - } - #[cfg(unix)] - { - let path = home.join("summarizer.sh"); - let cmd = format!("sh {}", path.display()); - (path, cmd) - } + let path = home.join("summarizer.control"); + let cmd = format!( + "\"{}\" \"{}\"", + env!("CARGO_BIN_EXE_summarizer_fixture"), + path.display() + ); + (path, cmd) } /// A live manifest whose `echo_commune` role declares its OWN kill bound — the W1 -/// per-role knob, keyed on the field and never on which role it is. -fn budgeted_manifest(commune_dir: &Path, summarizer: &str) -> Manifest { +/// per-role knob, keyed on the field and never on which role it is. `budget_secs` is a +/// parameter because the two legs need different bounds off the same role (header). +fn budgeted_manifest(commune_dir: &Path, summarizer: &str, budget_secs: u64) -> Manifest { let dir = commune_dir.to_string_lossy().replace('\\', "/"); let toml = format!( @@ -141,5 +170,5 @@ fn budgeted_manifest(commune_dir: &Path, summarizer: &str) -> Manifest { [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\ [session.psyche_resume]\ncommand='{TURN_CMD}'\nkeys=[]\n\n\ - [session.echo_commune]\ncommand='{summarizer}'\ninvocation_budget_secs={BUDGET_SECS}\n" + [session.echo_commune]\ncommand='{summarizer}'\ninvocation_budget_secs={budget_secs}\n" ); Manifest::from_toml_str(&toml).expect("the budgeted manifest parses") @@ -158,5 +187,5 @@ fn real_bound_kill_spends_the_soft_budget_and_never_the_hard_one() { std::fs::write(&script, SLOW_BODY).expect("the slow summarizer"); - let manifest = budgeted_manifest(&commune_dir, &summarizer_cmd); + let manifest = budgeted_manifest(&commune_dir, &summarizer_cmd, BUDGET_SECS); let cfg = DaemonConfig { // Back-to-back ticks: the wall clock of this test is the ten real kills, not @@ -291,4 +320,12 @@ fn real_bound_kill_spends_the_soft_budget_and_never_the_hard_one() { // ---- positive control: a real, fast, honest success clears it ----------------- std::fs::write(&script, FAST_BODY).expect("the fast summarizer"); + // Same host (the latch and its counter are host state and must survive into this + // leg — header), same role, same commune dir; only the BOUND changes, through the + // production refresh seam. `fire_echo` re-reads budget + runtime from the cell per + // fire, so this lands on the next fire. + host.refresh_manifest( + budgeted_manifest(&commune_dir, &summarizer_cmd, CONTROL_BUDGET_SECS), + None, + ); let stop = AtomicBool::new(false); let mut recovered: Option = None; commit 579e0b35600b0017d8b319eae3fc9c3ab552685c Author: Reavo End AuthorDate: Tue Aug 4 14:33:37 2026 -0700 Commit: Reavo End CommitDate: Tue Aug 4 14:33:37 2026 -0700 test(teardown): keep the unit tag adjacent to the row it tags The settle-budget const landed BETWEEN the `[unit->REQ-RESIDENT-SERVICE]` comment block and the `#[test]` fn it describes, so the tag no longer sat immediately above its evidence (AGENTS.md traceability rule 1). Move the const above the tag block instead; no behaviour change, no bound change. Co-authored by: hertz diff --git a/crates/spt-daemon/src/daemon.rs b/crates/spt-daemon/src/daemon.rs index 82ece49..7d1ff21 100644 --- a/crates/spt-daemon/src/daemon.rs +++ b/crates/spt-daemon/src/daemon.rs @@ -2586,4 +2586,9 @@ mod tests { } + /// How long the tree-teardown row waits for an asynchronous termination to land + /// before it calls the survivor a leak. See the poll site for why a generous bound + /// is free on a green run and why ten seconds was not enough on a loaded runner. + const TREE_TEARDOWN_SETTLE_BUDGET: Duration = Duration::from_secs(60); + // [unit->REQ-RESIDENT-SERVICE] the force-kill promise is UNCONDITIONAL, and a // descendant that outlives it makes it conditional. A real grandchild is @@ -2593,9 +2598,4 @@ mod tests { // that grandchild reachable by nothing afterwards: we hold no handle to it and // the next daemon's orphan sweep knows one parked pid and nothing below it. - /// How long the tree-teardown row waits for an asynchronous termination to land - /// before it calls the survivor a leak. See the poll site for why a generous bound - /// is free on a green run and why ten seconds was not enough on a loaded runner. - const TREE_TEARDOWN_SETTLE_BUDGET: Duration = Duration::from_secs(60); - #[test] fn a_tree_teardown_reaches_a_grandchild_the_service_spawned() { commit 061ddad18f8c100e10e0fe1a408d72023a11cea2 Author: Reavo End AuthorDate: Mon Aug 3 17:12:25 2026 -0700 Commit: Reavo End CommitDate: Mon Aug 3 17:12:25 2026 -0700 docs(instruments): name the xtask port as the instrument of record STATUS.md presented the python prototypes as the instruments. They are superseded: the maintained instruments are `xtask binedge-check` and `xtask perjob-map`, which ship with the workspace, build in CI, carry unit rows and a committed burn-down baseline, and can be run by whoever is gating rather than only by an author with a python on PATH. The prototype FILES stay, and the reason is evidence rather than sentiment: the port's acceptance rests on a row-for-row diff against them — 185 site tuples and 7 per-job rows identical, not four summary integers agreeing by coincidence — so the reference arm has to remain reachable for that claim to remain checkable. Their status column now reads SPECIMEN and a section says how to re-derive the cross-validation, including the precondition that both runs must see an identical `.rs` population. Two sentences that had gone stale are corrected by replacement rather than annotated. The per-job limit no longer says the map "cannot see" narrow invocations inside PowerShell scripts: the port scans them with the same detector and prints them as UNMODELLED with a measured count, so the hole is stated in the tool's own output instead of living only here, where whoever runs the command never reads it. Register citations are deliberately untouched; they ride the register lane so the sha story stays coherent. Co-authored by: todlando diff --git a/docs/instruments/ir21/STATUS.md b/docs/instruments/ir21/STATUS.md index 7e95fb8..a79e25f 100644 --- a/docs/instruments/ir21/STATUS.md +++ b/docs/instruments/ir21/STATUS.md @@ -5,4 +5,20 @@ the tree so a reviewer can re-run what the entry cites instead of taking its num trust. Measured on `hfenduleam`, cargo 1.93.0, 2026-08-03. +## The instrument of record is the xtask port + +**Run `cargo run -p xtask -- binedge-check` and `cargo run -p xtask -- perjob-map`.** They are +the maintained instruments: they ship with the workspace, build in CI, carry unit rows and a +committed burn-down baseline, and are runnable by whoever is gating rather than only by an +author with a python on PATH. They arrive with the xtask instruments lane. + +**The python scripts in this directory are retained deliberately, as the cross-validation +specimens the port was diffed against.** That diff is part of the port's acceptance evidence — +185 site tuples identical, and 7 per-job rows identical, not merely four summary integers +agreeing — so the reference arm has to stay reachable for the claim to stay checkable. Deleting +them would orphan the evidence that the port measures the same tree the same way. + +Read them as specimens, not as the tool: a number quoted from this page is quotable because the +port reproduces it, and the port is what a reader should run today. + **Read the status column before quoting any number from these.** @@ -10,6 +26,6 @@ trust. Measured on `hfenduleam`, cargo 1.93.0, 2026-08-03. |---|---|---| | `bindeps-probe/` | **COMPLETE** | artifact dependencies (remedy (a)) work, but only on nightly | -| `binedge_check.py` | **COMPLETE population** | 185 sites, 146 guaranteed, 39 need an explicit build, 11 red | -| `perjob_map.py` | **COMPLETE population** | 7 narrow invocations consume cross-package bins; **0 unguaranteed in CI** | +| `binedge_check.py` | **SPECIMEN** (superseded by `xtask binedge-check`) | 185 sites, 146 guaranteed, 39 need an explicit build, 11 red | +| `perjob_map.py` | **SPECIMEN** (superseded by `xtask perjob-map`) | 7 narrow invocations consume cross-package bins; **0 unguaranteed in CI** | ## `bindeps-probe/` — complete @@ -39,6 +55,7 @@ adopting this rewrites the consumer path contract rather than swapping one expre The first version of these scripts detected **two** ways a test names a fixture bin and reported **33** needing an explicit build with **7** red. That was drawn from an incomplete -population. `binedge_check.py` now detects **four**, and the figures are **re-derived and -re-stated**, not patched upward: +population. `binedge_check.py` was corrected to detect **four**, and the figures were +**re-derived and re-stated**, not patched upward. The port reproduces the corrected figures +exactly, which is what makes the specimen worth keeping: | | two-syntax (superseded) | four-syntax (current) | @@ -56,5 +73,5 @@ The seven added sites, each accounted for rather than absorbed: four shared-reso ## The per-job verdict — the 11 reds are NOT CI defects -`perjob_map.py` now uses the same four-syntax detector and answers the question the repo-wide +The per-job map uses the same four-syntax detector and answers the question the repo-wide allowlist could not: *does the job that RUNS a test build the bin it consumes?* @@ -83,9 +100,13 @@ appears and verdicts PREBUILD, which is what `ci.yml:105-106` actually does. ### Limits of the map, unfixed -- It parses `run:` blocks in workflow YAML only. **Narrow cargo invocations also live inside - PowerShell scripts** — `.github/ci/g6-curve.ps1:83` and `.github/ci/g6-postbounce.ps1:43` both - run `cargo nextest run -p spt-daemon --test inject_control_wedge`, and the map cannot see them. - Checked by hand: that target's needs are all same-package, so nothing is hidden *here*, but the - hole is real and a future script could sit in it. +- Guarantee is modelled from `run:` blocks in workflow YAML. **Narrow cargo invocations also live + inside PowerShell scripts** — `.github/ci/g6-curve.ps1:83` and `.github/ci/g6-postbounce.ps1:43` + both run `cargo nextest run -p spt-daemon --test inject_control_wedge`, and no model that reads + workflow YAML can know what a job built before invoking a script. + **The port states this hole rather than leaving it to a footnote:** it scans those scripts with + the same detector and reports the invocations as `UNMODELLED` with a measured count (2 today, of + which 0 consume cross-package bins), so the limit is printed in the tool's own output next to the + verdict. A verdict nobody can compute must not read green, and a limit that lives only in a doc + is invisible to whoever runs the command. - `uses:` steps are not followed. - Guarantee is tracked per job in step order; it does not model artifact reuse across jobs on a @@ -131,10 +152,12 @@ documents. That case is handled; it is recorded here because the first version o wrong and produced a green over the known-real gap. -## Running them +## Running the instrument ``` -python docs/instruments/ir21/binedge_check.py # exit 1 if any consumer is unguaranteed -python docs/instruments/ir21/binedge_check.py --no-prebuilds # negative control: empties the allowlist -python docs/instruments/ir21/perjob_map.py # per-job guarantee mapping +cargo run -p xtask -- binedge-check # exit 1 if any consumer is unguaranteed +cargo run -p xtask -- binedge-check --no-prebuilds # negative control: empties the allowlist +cargo run -p xtask -- binedge-check --baseline # reds only on a site outside the burn-down set +cargo run -p xtask -- binedge-check --sites # the full 185-row population, one line per site +cargo run -p xtask -- perjob-map # per-job guarantee mapping ``` @@ -143,4 +166,23 @@ sites stay green, and it must **not** reach the 11 same-package sites, whose gua cargo rather than from any allowlist. A green without that arm carries no information. -Neither script compiles anything — `cargo metadata --no-deps` plus a tracked-file scan. Worst -observed wall time 1.41s, of which `cargo metadata` is ~0.09s. +The port adds what a maintained instrument needs and a prototype did not have: a committed +burn-down baseline (`--baseline`), a false-positive arm that **refuses** rather than reporting +"0 wrongly flagged" once it can no longer recognize its own fixtures, and a derived `fix:` line +on every red naming the exact `cargo build -p --bin ` that clears it. + +## Re-running the specimens + +``` +python docs/instruments/ir21/binedge_check.py # the cross-validation reference arm +python docs/instruments/ir21/binedge_check.py --no-prebuilds +python docs/instruments/ir21/perjob_map.py +``` + +Use these to re-derive the cross-validation: diff `xtask binedge-check --sites` against +`binedge_check.py`'s site listing and the two must agree row for row, on a tree whose `.rs` +population is identical for both runs. Two populations wearing one measurement is the failure +this comparison exists to exclude, so establish the tree first and the agreement second. + +Nothing here compiles the workspace — `cargo metadata --no-deps` plus a tracked-file scan. Worst +observed wall time 1.41s for a script, of which `cargo metadata` is ~0.09s; the xtask commands run +in ~2s warm, plus a build the first time in a cold pool. commit 6fbea74bf5e5ef8c3d1f6f46a889553e2ebb2302 Author: Reavo End AuthorDate: Mon Aug 3 09:15:25 2026 -0700 Commit: Reavo End CommitDate: Mon Aug 3 15:54:28 2026 -0700 test(spt): resolve same-package fixtures via CARGO_BIN_EXE_ instead of sibling_bin The 11 same-package `sibling_bin("…")` call sites in `crates/spt/tests/` now use `PathBuf::from(env!("CARGO_BIN_EXE_"))` — the path cargo actually emitted for the target, rather than a string-join of the `CARGO_BIN_EXE_spt` directory anchor and `EXE_SUFFIX`. Five files' `sibling_bin` copies became dead and are deleted (29 resolvers -> 24); `idle_edge_drain_e2e` and `live_adapt_translation_swap_e2e` keep theirs because they also resolve the cross-package `mock-session`. This is a resolution and subtraction change. It is NOT a build fix, and the IR-21 remedy (1) claim that these sites "force cargo to build" the fixture is measured FALSE: deleting all four `translate_proof_fixture` artifacts and then building one unrelated integration test (`--test attach_wedge_e2e`, which never references the fixture) rebuilt the plain exe anyway, while the hash-suffixed harness exes stayed absent. Cargo builds every bin target of a package whenever it builds any integration test of that package, which is what sets `CARGO_BIN_EXE_*` in the first place. The build hazard is real only for cross-package consumers (33 sites) and for the one same-package UNIT-test member (`crates/spt/src/cli.rs`), which gets no `CARGO_BIN_EXE_*` at all — neither is touched here. Completes a migration already paid for: `tests/fixtures/translate_proof_fixture.rs` records that it was re-homed into `spt` precisely so `CARGO_BIN_EXE_translate_proof_fixture` would be set, and then every call site resolved by path anyway. Verified: cargo check -p spt --tests exit 0 / 0 warnings; cargo clippy -p spt --tests -D warnings exit 0 / 0 warnings; cargo nextest run over the five non-daemon affected targets 6/6 pass, covering 6 of the 11 converted sites. The remaining 5 sites (idle_edge_drain_e2e 1, live_adapt_translation_swap_e2e 4) were NOT executed — they spawn daemons and this ran in the root checkout on a live box. Co-authored by: todlando diff --git a/crates/spt/tests/adapter_post_step.rs b/crates/spt/tests/adapter_post_step.rs index ddbe641..0715fe1 100644 --- a/crates/spt/tests/adapter_post_step.rs +++ b/crates/spt/tests/adapter_post_step.rs @@ -21,9 +21,4 @@ use common::CommandNoWindowExt; use spt_store::perch; -fn sibling_bin(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_BIN_EXE_spt")) - .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX)) -} - fn run_update(spt_bin: &Path, home: &Path, mode: &str, seam_out: &Path) -> std::process::Output { Command::new(spt_bin) @@ -43,5 +38,5 @@ fn run_update(spt_bin: &Path, home: &Path, mode: &str, seam_out: &Path) -> std:: fn adapter_update_runs_post_step_unconditionally_and_arbitrates_notice() { let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt")); - let fixture = sibling_bin("post_step_fixture"); + let fixture = PathBuf::from(env!("CARGO_BIN_EXE_post_step_fixture")); assert!( fixture.exists(), diff --git a/crates/spt/tests/composite_e2e.rs b/crates/spt/tests/composite_e2e.rs index 7ef7e34..408c71e 100644 --- a/crates/spt/tests/composite_e2e.rs +++ b/crates/spt/tests/composite_e2e.rs @@ -38,9 +38,4 @@ fn hex(bytes: &[u8]) -> String { } -fn sibling_bin(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_BIN_EXE_spt")) - .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX)) -} - /// `Command::output()` with a deadline, off-thread — bounds a hang into a failure. fn output_bounded(mut cmd: Command, deadline: Duration) -> Output { @@ -135,5 +130,6 @@ fn bare_update_applies_core_then_updates_adapter_in_one_invocation() { std::fs::copy(env!("CARGO_BIN_EXE_spt"), &spt).unwrap(); let gh = bindir.join(if cfg!(windows) { "gh.exe" } else { "gh" }); - std::fs::copy(sibling_bin("gh_fixture"), &gh).expect("gh_fixture must be built"); + std::fs::copy(PathBuf::from(env!("CARGO_BIN_EXE_gh_fixture")), &gh) + .expect("gh_fixture must be built"); #[cfg(unix)] { diff --git a/crates/spt/tests/idle_edge_drain_e2e.rs b/crates/spt/tests/idle_edge_drain_e2e.rs index 2e9471f..2a4db6d 100644 --- a/crates/spt/tests/idle_edge_drain_e2e.rs +++ b/crates/spt/tests/idle_edge_drain_e2e.rs @@ -103,5 +103,5 @@ fn spool_while_active_then_idle_fires_injection() { .unwrap_or_default(); let mock_session = sibling_bin("mock-session"); - let xlate_fixture = sibling_bin("translate_proof_fixture"); + let xlate_fixture = PathBuf::from(env!("CARGO_BIN_EXE_translate_proof_fixture")); assert!(mock_session.exists(), "dummy-harness must be built: {}", mock_session.display()); assert!(xlate_fixture.exists(), "translation fixture must be built: {}", xlate_fixture.display()); diff --git a/crates/spt/tests/live_adapt_translation_swap_e2e.rs b/crates/spt/tests/live_adapt_translation_swap_e2e.rs index 14a3672..54ff372 100644 --- a/crates/spt/tests/live_adapt_translation_swap_e2e.rs +++ b/crates/spt/tests/live_adapt_translation_swap_e2e.rs @@ -130,5 +130,5 @@ fn adapter_apply_swaps_locked_translation_binary_without_restarting_endpoint() { .unwrap_or_default(); let mock_session = sibling_bin("mock-session"); - let xlate_fixture = sibling_bin("translate_proof_fixture"); + let xlate_fixture = PathBuf::from(env!("CARGO_BIN_EXE_translate_proof_fixture")); assert!( mock_session.exists(), @@ -438,5 +438,5 @@ fn adapter_apply_with_no_matching_session_still_swaps() { .map(|e| format!(".{}", e.to_string_lossy())) .unwrap_or_default(); - let xlate_fixture = sibling_bin("translate_proof_fixture"); + let xlate_fixture = PathBuf::from(env!("CARGO_BIN_EXE_translate_proof_fixture")); assert!(xlate_fixture.exists(), "fixture must be built: {}", xlate_fixture.display()); @@ -580,5 +580,5 @@ fn adapter_apply_swaps_composite_profile_endpoint_matched_on_parent() { .unwrap_or_default(); let mock_session = sibling_bin("mock-session"); - let xlate_fixture = sibling_bin("translate_proof_fixture"); + let xlate_fixture = PathBuf::from(env!("CARGO_BIN_EXE_translate_proof_fixture")); assert!(mock_session.exists(), "dummy-harness must be built: {}", mock_session.display()); assert!(xlate_fixture.exists(), "fixture must be built: {}", xlate_fixture.display()); @@ -839,5 +839,5 @@ fn adapter_apply_for_foreign_adapter_leaves_live_endpoint_untouched() { .unwrap_or_default(); let mock_session = sibling_bin("mock-session"); - let xlate_fixture = sibling_bin("translate_proof_fixture"); + let xlate_fixture = PathBuf::from(env!("CARGO_BIN_EXE_translate_proof_fixture")); assert!(mock_session.exists(), "dummy-harness must be built: {}", mock_session.display()); assert!(xlate_fixture.exists(), "fixture must be built: {}", xlate_fixture.display()); diff --git a/crates/spt/tests/projindex_reader_e2e.rs b/crates/spt/tests/projindex_reader_e2e.rs index 27942c0..65e9bec 100644 --- a/crates/spt/tests/projindex_reader_e2e.rs +++ b/crates/spt/tests/projindex_reader_e2e.rs @@ -29,9 +29,4 @@ use spt_store::contextstore::ContextStore; use spt_store::projindex::{self, IndexRead}; -fn sibling_bin(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_BIN_EXE_spt")) - .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX)) -} - fn kill_pid(pid: u32) { #[cfg(windows)] @@ -156,5 +151,6 @@ fn readers_answer_from_daemon_maintained_index_with_zero_git() { std::fs::create_dir_all(&bindir).unwrap(); let git = bindir.join(if cfg!(windows) { "git.exe" } else { "git" }); - std::fs::copy(sibling_bin("git_fixture"), &git).expect("git_fixture must be built"); + std::fs::copy(PathBuf::from(env!("CARGO_BIN_EXE_git_fixture")), &git) + .expect("git_fixture must be built"); #[cfg(unix)] { diff --git a/crates/spt/tests/translate_proof.rs b/crates/spt/tests/translate_proof.rs index 33226cc..cd9b473 100644 --- a/crates/spt/tests/translate_proof.rs +++ b/crates/spt/tests/translate_proof.rs @@ -29,10 +29,4 @@ use spt_store::perch; static E2E_LOCK: Mutex<()> = Mutex::new(()); -/// Absolute path to a sibling binary in the same target dir as the `spt` test bin. -fn sibling_bin(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_BIN_EXE_spt")) - .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX)) -} - /// Run `spt ` under an isolated `SPT_HOME`, bounded, capturing output. fn run_spt( @@ -61,5 +55,5 @@ fn translate_proof_drives_the_real_translation_binary() { let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt")); - let fixture = sibling_bin("translate_proof_fixture"); + let fixture = PathBuf::from(env!("CARGO_BIN_EXE_translate_proof_fixture")); assert!( fixture.exists(), @@ -176,5 +170,5 @@ fn translate_proof_dir_override_proofs_unregistered_install() { let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt")); - let fixture = sibling_bin("translate_proof_fixture"); + let fixture = PathBuf::from(env!("CARGO_BIN_EXE_translate_proof_fixture")); assert!( fixture.exists(), diff --git a/crates/spt/tests/whoami_identity_e2e.rs b/crates/spt/tests/whoami_identity_e2e.rs index 94ee3a5..5a06ac3 100644 --- a/crates/spt/tests/whoami_identity_e2e.rs +++ b/crates/spt/tests/whoami_identity_e2e.rs @@ -21,9 +21,4 @@ mod common; use common::CommandNoWindowExt; -fn sibling_bin(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_BIN_EXE_spt")) - .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX)) -} - /// `Command::output()` with a deadline, off-thread — the whole point of the /// verb is bounded time, so a hang is a failure, not a wait. @@ -63,5 +58,6 @@ fn whoami_answers_identity_on_a_multi_perch_home_with_zero_git() { std::fs::create_dir_all(&bindir).unwrap(); let git = bindir.join(if cfg!(windows) { "git.exe" } else { "git" }); - std::fs::copy(sibling_bin("git_fixture"), &git).expect("git_fixture must be built"); + std::fs::copy(PathBuf::from(env!("CARGO_BIN_EXE_git_fixture")), &git) + .expect("git_fixture must be built"); #[cfg(unix)] { Wall time: 0.20 seconds