From f54a9c05450f6875c527f3d9e92ef8d4dba2fde3 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 06:29:20 +0000 Subject: [PATCH 1/5] perf(build): store dev debuginfo unpacked, and pin the opt-level that was unpinned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLOUD-1211 reached Done with its acceptance clause reading "split-debuginfo = unpacked is still unmeasured. Outstanding." This measures it, and finds a second, independent hole in the same file. MEASURED as a paired A/B: three cold builds back to back in one process so machine noise is common-mode, two identical baselines bracketing the candidate. arm cold wall artifacts linked bytes mean base-1 223.4s 147 10.19 GB 69.3 MB unpacked 208.9s 147 4.99 GB 34.0 MB base-2 213.7s 147 10.19 GB 69.3 MB TIME IS INSIDE THE NULL and is not a finding. The two identical baselines differ by 9.7s on their own (spread 0.957) and the candidate sits at 0.956 against their mean. That is the expected result: the prior art is macOS dsymutil, and unpacked is already cargo's default there and not on Linux. BYTES ARE THE FINDING, on a null of zero width — the two baselines are byte-identical at 10193048624. 2.04x off the linked binaries, adopted on that column alone, which is what the row asks for. AND THEY LEAVE THE DISK rather than moving to a sibling, which the linked census alone cannot answer: deps 10.97 GB -> 5.87 GB, target/debug 13.03 GB -> 7.76 GB, the 5.20 GB replaced by 111.8 MB of .dwo. Those .dwo files are a class prune.rs's closed RECLAIMED_KINDS cannot see, so they accumulate per build hash; filed as CLOUD-1293 rather than fixed here, since the fix is in prune.rs and out of this row's scope. debug = 1 is untouched, so this changes where debuginfo is stored and never whether it exists. SEPARATELY, opt-level = 2 was unpinned. dev_profile.rs carried four cases and zero occurrences of the key, yet it is CLOUD-1211's biggest single win — test:cargo warm 100.189s to 48.581s, 2.06x. Dropping it reds nothing and doubles every suite run. Asserted now, on a shared dependency_override() lookup, with the anti-vacuity case that pins the panicking read: the dev profile's opt-level default is 0 rather than an absent key, so a defaulting lookup would report the unoptimised build as satisfied. Both new assertions shown able to fail — removing each key reds the suite (exit 100, 101). Refs: CLOUD-1289, CLOUD-1211, CLOUD-766, CLOUD-1293 --- Cargo.toml | 44 ++++++++- crates/batten/tests/dev_profile.rs | 141 +++++++++++++++++++++++++++-- 2 files changed, 174 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1093a92a..d90195f6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -561,7 +561,9 @@ strip = true # THREE ARMS MEASURED on this container, 2026-08-30, each a cold # `mise run test:cargo` over a cleared `target/debug`. The census counts # `target/debug/deps`' extension-less linked binaries — the population -# `crates/batten/src/prune.rs:262-269` reads: +# `crates/batten/src/prune.rs`'s `superseded_in` reads through `artifact_key` +# (named rather than cited by line, because the line span this used to give had +# already drifted onto an unrelated field): # # arm artifacts bytes mean cold wall # debug = 1 (the baseline) 125 15.53 GB 124.3 MB 231s @@ -600,8 +602,48 @@ strip = true # not re-proposed: rust-lld has been the default on this host triple since Rust # 1.90, this repository pins 1.97.1, and `readelf -p .comment` over a built # artifact reports `Linker: LLD 22.1.6`. The build already links with lld. +# +# SPLIT-DEBUGINFO MEASURED 2026-09-01 (CLOUD-1289) — the one arm CLOUD-1211 +# deferred with its acceptance clause reading "Outstanding". Paired A/B on this +# container: three cold builds back to back in ONE process, so machine noise is +# common-mode and divides out, with two identical baselines bracketing the +# candidate rather than sitting on one side of it. +# +# arm cold wall artifacts linked bytes mean +# base-1 223.4s 147 10.19 GB 69.3 MB +# unpacked 208.9s 147 4.99 GB 34.0 MB +# base-2 213.7s 147 10.19 GB 69.3 MB +# +# THE TIME COLUMN IS NOT A FINDING, and is recorded only so nobody re-runs it. +# The two IDENTICAL baselines differ by 9.7s on their own — a null spread of +# 0.957 — and the candidate's ratio against the baseline mean is 0.956, which is +# inside it. That is the EXPECTED result rather than a disappointment: the prior +# art CLOUD-1211 cites (14s->4s, 8.7s->3.0s, rust-lang/cargo#9112) is macOS +# `dsymutil`, and `unpacked` is already cargo's default on macOS and not on +# Linux, so the mechanism that paid there is not the one in play on this triple. +# +# THE BYTE COLUMN IS THE FINDING, and its null has ZERO width — the two +# baselines are byte-identical at 10193048624, not merely close. 2.04x off the +# linked binaries, adopted on that alone, which is what CLOUD-1289's acceptance +# asks for: an arm that halves artifacts is worth adopting even where its time +# delta sits inside the null, because bytes are the binding constraint here +# (CLOUD-766). +# +# AND THE BYTES LEAVE THE DISK rather than moving to a sibling file, which is +# the question a SPLIT format has to be asked and which the linked census alone +# cannot answer: `target/debug/deps` 10.97 GB -> 5.87 GB and `target/debug` +# 13.03 GB -> 7.76 GB, because the 5.20 GB that left the binaries is replaced by +# 111.8 MB of `.dwo`. Those `.dwo` files are a class `prune.rs`'s closed +# `RECLAIMED_KINDS` cannot see, so they accumulate per build hash where the +# binary they came out of was reclaimable — CLOUD-1293, filed rather than fixed +# here because the fix is in `prune.rs` and the trade is 5.20 GB against 111.8 MB. +# +# `debug = 1` is untouched, so this changes WHERE debuginfo is stored and never +# whether it exists: every `batten` frame still reports its file and line, and +# the middle arm above stays rejected. [profile.dev] debug = 1 +split-debuginfo = "unpacked" # The dependency closure carries no debuginfo and IS optimised. Workspace code # above keeps its line tables and stays unoptimised, so rebuilds of the code under diff --git a/crates/batten/tests/dev_profile.rs b/crates/batten/tests/dev_profile.rs index 912596bf4..14b658efe 100644 --- a/crates/batten/tests/dev_profile.rs +++ b/crates/batten/tests/dev_profile.rs @@ -34,6 +34,21 @@ //! //! The adopted arm takes 2.3x off the bytes by stripping the dependency closure, //! which is where they were, while every `batten` frame keeps its file and line. +//! +//! # `opt-level` is the larger half, and it was unpinned until CLOUD-1289 +//! +//! The three arms above are all about `debug`, and so were all four of this +//! file's cases — it contained zero occurrences of `opt-level`. But the same +//! `[profile.dev.package."*"]` block carries `opt-level = 2`, and CLOUD-1211 +//! measured that as its **biggest single win**: `mise run test:cargo` warm +//! 100.189s to 48.581s, **2.06x on the whole suite**, because `batten hook` +//! against this repository's own ruleset is dominated by dependency code +//! evaluating Rego and unoptimised that work is 6.8x what the shipped binary +//! does. +//! +//! Dropping that key today reds nothing and exits 0. It simply doubles every +//! suite run, for every developer, until somebody thinks to time it — the same +//! silent shape as the byte regression above, one dial over. // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] @@ -49,6 +64,15 @@ const ADOPTED_DEBUG: i64 = 1; /// bytes actually were — 124.3 MB to 54.5 MB per linked binary. const ADOPTED_DEPENDENCY_DEBUG: i64 = 0; +/// What CLOUD-1289's adopted arm sets. A STRING, because cargo spells this one +/// as an enum rather than a number, and `"packed"` and `"off"` are the other two +/// values a later edit could land here without noticing. +const ADOPTED_SPLIT_DEBUGINFO: &str = "unpacked"; + +/// What the adopted arm sets for the DEPENDENCY closure's optimisation, which is +/// where the SUITE'S WALL CLOCK was — 100.189s to 48.581s warm, 2.06x. +const ADOPTED_DEPENDENCY_OPT_LEVEL: i64 = 2; + /// The glob cargo spells "every dependency, but not workspace members". const DEPENDENCY_GLOB: &str = "*"; @@ -66,6 +90,22 @@ fn dev_profile() -> toml::Value { .expect("[profile.dev] is declared") } +/// `[profile.dev.package."*"]` — the override both adopted values live on. +/// +/// Panics rather than returning an option, because an absent block is itself the +/// regression: without it every dependency carries debuginfo again AND compiles +/// unoptimised, which is both halves of CLOUD-1211 undone at once. +fn dependency_override() -> toml::Value { + dev_profile() + .get("package") + .and_then(|package| package.get(DEPENDENCY_GLOB)) + .cloned() + .expect( + "[profile.dev.package.\"*\"] is declared — without it every dependency \ + carries debuginfo again and the linked binaries go back to ~124 MB", + ) +} + /// `debug` normalised across the two spellings cargo accepts. Panics on an /// absent key rather than defaulting, which is the reading /// `an_absent_debug_key_is_not_read_as_the_adopted_value` pins. @@ -82,6 +122,24 @@ fn declared_debug(profile: &toml::Value) -> i64 { } } +/// `opt-level` read the same panicking way `declared_debug` is, and for the same +/// reason: an absent key is cargo's own default (`0` for the dev profile), so a +/// defaulting lookup would report the regression as satisfied. +/// +/// Cargo also accepts `"s"` and `"z"` here. Neither is the adopted value, and +/// both panic naming what they found rather than being silently coerced to a +/// number they are not. +fn declared_opt_level(profile: &toml::Value) -> i64 { + let value = profile.get("opt-level").expect( + "this profile declares `opt-level` — an absent key is cargo's own default \ + of 0, which is the regression this asserts against", + ); + match value { + toml::Value::Integer(level) => *level, + other => panic!("`opt-level` is not an integer: {other:?}"), + } +} + #[test] fn workspace_code_keeps_its_line_tables() { let debug = declared_debug(&dev_profile()); @@ -98,16 +156,7 @@ fn workspace_code_keeps_its_line_tables() { #[test] fn the_dependency_closure_carries_no_debuginfo() { - let debug = manifest() - .get("profile") - .and_then(|profile| profile.get("dev")) - .and_then(|dev| dev.get("package")) - .and_then(|package| package.get(DEPENDENCY_GLOB)) - .map(declared_debug) - .expect( - "[profile.dev.package.\"*\"] is declared — without it every dependency \ - carries debuginfo again and the linked binaries go back to ~124 MB", - ); + let debug = declared_debug(&dependency_override()); assert_eq!( debug, ADOPTED_DEPENDENCY_DEBUG, "[profile.dev.package.\"*\"] debug is {debug}, not the adopted \ @@ -117,6 +166,23 @@ fn the_dependency_closure_carries_no_debuginfo() { ); } +/// The wall-clock half of the same override, and the one nothing asserted until +/// CLOUD-1289. `debug` above is about `target/debug`'s bytes; this is about how +/// long every suite run takes, and it is the larger of the two numbers. +#[test] +fn the_dependency_closure_is_optimised() { + let level = declared_opt_level(&dependency_override()); + assert_eq!( + level, ADOPTED_DEPENDENCY_OPT_LEVEL, + "[profile.dev.package.\"*\"] opt-level is {level}, not the adopted \ + {ADOPTED_DEPENDENCY_OPT_LEVEL}. This is CLOUD-1211's biggest single win — \ + `mise run test:cargo` warm 100.189s to 48.581s, 2.06x — and dropping it \ + reds nothing, exits 0, and doubles every suite run for everybody \ + (CLOUD-1289). If this is a deliberate change, move the constant and say \ + why in the same commit." + ); +} + /// ANTI-VACUITY, and it is the case that would actually have caught the drift. /// Both assertions above pass over a profile that declares the key at the right /// value; neither would notice a `declared_debug` loosened to a defaulting @@ -143,6 +209,61 @@ fn an_absent_debug_key_is_not_read_as_the_adopted_value() { ); } +/// CLOUD-1289's arm, and the assertion is over the byte finding rather than the +/// time one: `unpacked` was 2.04x off the linked binaries against a null of zero +/// width, and inside the null on wall clock. Dropping it is silent for exactly +/// the reason the `debug` keys are — `target/debug` grows back by 5.2 GB and +/// nothing reds until a session runs out of disk. +#[test] +fn debuginfo_stays_out_of_the_linked_binaries() { + let profile = dev_profile(); + let declared = profile + .get("split-debuginfo") + .and_then(toml::Value::as_str) + .map(str::to_owned) + .expect( + "[profile.dev] declares `split-debuginfo` — an absent key is cargo's own \ + default, which on this host triple is the packed form CLOUD-1289 measured \ + at 2.04x more linked bytes", + ); + assert_eq!( + declared, ADOPTED_SPLIT_DEBUGINFO, + "[profile.dev] split-debuginfo is {declared:?}, not the adopted \ + {ADOPTED_SPLIT_DEBUGINFO:?}. Measured 2026-09-01 as a paired A/B on one \ + machine: 10.19 GB of linked binaries to 4.99 GB, and `target/debug` 13.03 GB \ + to 7.76 GB, with the two identical baselines byte-identical to each other \ + (CLOUD-1289). If this is a deliberate change, move the constant and say why \ + in the same commit." + ); +} + +/// ANTI-VACUITY for the case above, and it is not the same assertion twice: the +/// dev profile's `opt-level` default is `0` rather than an absent key, so a +/// defaulting lookup here would read the UNOPTIMISED build — the exact 2x +/// regression — as the adopted value. This pins the panicking read. +#[test] +fn an_absent_opt_level_key_is_not_read_as_the_adopted_value() { + let fixture: toml::Value = + toml::from_str("[profile.dev.package.\"*\"]\ndebug = 0\n").expect("fixture parses"); + let profile = fixture + .get("profile") + .and_then(|profile| profile.get("dev")) + .and_then(|dev| dev.get("package")) + .and_then(|package| package.get(DEPENDENCY_GLOB)) + .expect("the fixture declares the dependency override"); + + assert!( + profile.get("opt-level").is_none(), + "the fixture is the absent-key case this asserts over" + ); + let caught = std::panic::catch_unwind(|| declared_opt_level(profile)); + assert!( + caught.is_err(), + "an absent `opt-level` is cargo's own dev default of 0, not the adopted 2 — \ + reading it as satisfied is exactly the silent regression this file refuses" + ); +} + /// `[profile.dist]` and `[profile.release]` are out of CLOUD-1211's scope: they /// build the shipped artifact, and a test-loop change must not reach them. This /// pins that boundary rather than trusting the commit that drew it. From e9f4df378822ea19c4756160e853cde1fc820dd7 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 07:08:11 +0000 Subject: [PATCH 2/5] perf(test): memoize the harness's three re-reads of the committed config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The harness re-read and re-parsed this repository's own 356 KB batten.toml on every fixture command it built — bypass_env_vars() from batten(), plus declared_patterns() and committed_patterns() — across 761 static call sites with no memoization anywhere in the test tree. MEASURED FIRST, because this row's own estimate was refuted before it was written: taking the cost through `batten config show` measures the same to within noise from a directory holding no config at all, so a 29 ms verb cannot resolve it. The new `mise run config-load-bench` times the function instead. arm=load p50=10.48 p95=11.67 mean=10.64 runs=200 arm=parse p50=10.35 p95=12.11 mean=10.63 runs=200 arm=null p50=10.49 p95=11.85 mean=10.67 runs=200 ratio=parse/load value=0.988 10.48 ms per call, ten times the row's ~1 ms guess. parse/load at 0.988 says the READ is 1.2% of it; the cost is the parse and its validate passes. SUITE DELTA, paired on one machine, two timed runs per arm after a discarded warmup, on nextest's own reported duration so the forced rebuild is excluded: base 60.159s 60.367s mean 60.263s 3512 tests memoized 53.536s 53.746s mean 53.641s 3512 tests Ratio 0.890 — 11.0%, 6.62s — against a within-arm null of 1.0035 and 1.0039, so ~28x the noise. Identical test count, both arms fully green. A first attempt at that half was unreadable and the correction is the interesting part: two IDENTICAL base runs measured 77.3s and 60.9s, a null spread of 0.788, because run one pays the page cache for 147 freshly linked binaries. What is memoized is the RESULT of reading the committed config, never a hand-written list of hatch names — CLOUD-1227's derivation is why that distinction matters, and it is untouched. Every signature is unchanged, so no caller had to know. The bench lives in perf.rs for the reason acquisition-bench does: Record is a contract perf-compare parses and perf-gate greps, and the percentile convention behind p50 has to have one author. summarise() is extracted from record() rather than duplicated beside it. The arm takes its own BENCH_METRIC stamp, and acquisition_metric.rs is generalised to a table asserting every bench task carries one and that no two collide — a property one task alone could not have. Refs: CLOUD-1291, CLOUD-1227, CLOUD-1211, CLOUD-1210 --- crates/batten/examples/config-load-bench.rs | 73 +++++++++ crates/batten/src/perf.rs | 161 +++++++++++++++++++- crates/batten/tests/acquisition_metric.rs | 108 +++++++++---- crates/batten/tests/common/mod.rs | 58 +++++-- mise.toml | 26 ++++ 5 files changed, 377 insertions(+), 49 deletions(-) create mode 100644 crates/batten/examples/config-load-bench.rs diff --git a/crates/batten/examples/config-load-bench.rs b/crates/batten/examples/config-load-bench.rs new file mode 100644 index 000000000..c649c2ecf --- /dev/null +++ b/crates/batten/examples/config-load-bench.rs @@ -0,0 +1,73 @@ +//! What `batten::config::load` costs over this repository's own committed +//! authority (CLOUD-1291). +//! +//! # The question, and the measurement that could not answer it +//! +//! `crates/batten/tests/common/mod.rs` re-reads and re-parses the committed +//! `batten.toml` on every fixture command it constructs — 761 static call sites +//! across the suite, none of them memoized. Whether that is worth fixing depends +//! on a number nobody had. +//! +//! The first attempt took it through a CLI verb, subtracting `batten --help` from +//! `batten config show` and calling the difference the parse. It is not: running +//! the same verb from a directory with no `batten.toml`, where nothing is parsed +//! at all, measured 30.2 ms against 29.1 ms in-repo — identical within noise. The +//! 22.5 ms was verb startup, paid whether or not a config exists. A 29 ms process +//! cannot resolve a cost that may be a millisecond, so the verb is the wrong +//! instrument and this target is the right one: one function call, timed in +//! process, with nothing else in the way. +//! +//! # Why an example target and not a verb +//! +//! `crates/batten/examples/acquisition-bench.rs`'s header owns this argument in +//! full and it applies unchanged: `crates/batten/tests/pointer_only.rs` sweeps +//! every leaf verb over a bare fixture corpus and refuses one that exits `3`, and +//! a benchmark has could-not-look as its only honest answer there. So the +//! measurement is a target the command surface does not carry — no verb, no +//! completion, no man page. It is still built by `--all-targets` and still held +//! to the same clippy bar. +//! +//! It also costs the integration-test census nothing: `examples/` is not +//! `tests/`, so this adds no linked test binary (CLOUD-1210). +//! +//! # Why the work is in `crates/batten/src/perf.rs` +//! +//! `Record`'s shape is a contract `perf-compare` parses and `perf-gate` greps, +//! and the percentile convention behind `p50` is what two readings must share +//! before their numbers can sit side by side. A bench with its own struct and its +//! own median is a second authority over both. +//! +//! # Reading the output +//! +//! One `arm=` record per arm in per-call milliseconds, the `parse/load` ratio — +//! whose distance from 1.0 is the READ's share — then the null spread the whole +//! thing must be read against. A saving inside that spread has measured no +//! effect, which for this row is a result and not a failure to deliver. +//! +//! Exit 0 measured / 1 could not look. + +// The one sanctioned place to write to a stream: this target IS a binary +// boundary, exactly as `main.rs` is, and its whole output is the report. +#![allow(clippy::print_stdout, clippy::print_stderr)] + +use std::path::Path; + +fn main() -> std::process::ExitCode { + // The committed authority, relative to the repository root the task layer + // runs from — the same path `common::at_root("batten.toml")` resolves for the + // harness this is measuring on behalf of. + match batten::perf::config_load(Path::new("batten.toml")) { + Ok(reading) => { + print!("{reading}"); + std::process::ExitCode::SUCCESS + } + // COULD NOT LOOK, in the `::error::` shape the workflow annotates, and + // never an empty reading that exits 0 — a bench reporting "measured, and + // there was nothing" over a run that never happened is the failure + // CLOUD-1208 hit twice in one session. + Err(reason) => { + eprintln!("::error:: {reason}"); + std::process::ExitCode::FAILURE + } + } +} diff --git a/crates/batten/src/perf.rs b/crates/batten/src/perf.rs index 336feb187..45ef88017 100644 --- a/crates/batten/src/perf.rs +++ b/crates/batten/src/perf.rs @@ -825,7 +825,7 @@ fn state_prefixed(state: &str, argv: &[String]) -> Vec { /// One arm's record, with `perf`'s own percentile convention. fn record(arm: &'static str, id: &str, result: &serde_json::Value) -> Result { - let mut times: Vec = result + let times: Vec = result .get("times") .and_then(serde_json::Value::as_array) .ok_or_else(|| anyhow::anyhow!("perf-pair: the {id} {arm} arm carried no times."))? @@ -835,6 +835,30 @@ fn record(arm: &'static str, id: &str, result: &serde_json::Value) -> Result, + reported_mean: Option, +) -> Result { + if times.is_empty() { + bail!("perf: the {id} {arm} arm carried no times."); + } times.sort_by(f64::total_cmp); let n = times.len(); @@ -856,10 +880,11 @@ fn record(arm: &'static str, id: &str, result: &serde_json::Value) -> Result() / n as f64); Ok(Record { arm, @@ -1140,6 +1165,132 @@ pub fn acquire(repo: &Path) -> Result { }) } +// --------------------------------------------------------------------------- +// The config-load reading (CLOUD-1291). +// --------------------------------------------------------------------------- +// +// WHY IT LIVES HERE, and it is the acquisition sweep's argument one measurement +// over: `Record` is a CONTRACT with two frozen callers (`perf-compare` parses it, +// `perf-gate` greps `^arm=`), and the percentile convention behind `p50` is the +// thing two readings have to share before their numbers can be put side by side. +// A bench with its own struct and its own idea of a median produces records that +// look comparable and are not. +// +// WHAT MAKES THIS ARM DIFFERENT from every other one in this module: it spawns +// NOTHING. There is no hyperfine, no binary to build, no fixture tree. The +// subject is one function call in this process, which is also why CLOUD-1291 +// forbids pricing it through a CLI verb — measured, `batten config show` is +// insensitive to config size, so the verb's 29 ms of startup swallows the answer. + +/// The default sample count. Large enough that a sub-millisecond call is not +/// being timed against the clock's own resolution, small enough that the whole +/// reading is seconds. +const DEFAULT_LOAD_SAMPLES: &str = "200"; + +/// The environment override for it. +const LOAD_SAMPLES_VAR: &str = "BENCH_LOAD_SAMPLES"; + +/// Price [`crate::config::load`] over one committed authority. +/// +/// # The experiment +/// +/// Three arms over the same file, back to back in one process so the noise the +/// ratios divide out is the same noise: +/// +/// * `load` — the whole of what the harness calls: read plus parse plus every +/// `validate` pass. +/// * `parse` — the same text already in memory, so the difference between the two +/// is the READ rather than a guess about it. +/// * `load-null` — `load` again. Its ratio against the first is 1.0 plus pure +/// noise by construction, which is what makes the spread a measured quantity +/// rather than a number in a comment. +/// +/// # Reading it +/// +/// The arm records are per-call milliseconds, so the `mean` on the `load` arm IS +/// the per-call cost the row asks for. Multiply by the harness's call count to +/// get the suite delta, and read that against the null spread: a saving inside it +/// has measured no effect, and recording that is the row's sanctioned outcome. +/// +/// # Errors +/// +/// A file that cannot be read or does not parse — properties of the checkout +/// rather than verdicts about the cost — and a sample count that is zero or +/// unparseable. Never an empty measurement reported as a reading. +pub fn config_load(path: &Path) -> Result { + let samples: usize = env_or(LOAD_SAMPLES_VAR, DEFAULT_LOAD_SAMPLES) + .parse() + .with_context(|| format!("perf-config-load: {LOAD_SAMPLES_VAR} is not a count"))?; + if samples == 0 { + bail!("perf-config-load: {LOAD_SAMPLES_VAR} declared no samples. Nothing measured."); + } + + let source = path.display().to_string(); + // Read once, up front, for two reasons: it is the `parse` arm's input, and a + // missing or unparseable file is a could-not-look that must be reported + // BEFORE any timing rather than as a zero. + let text = std::fs::read_to_string(path) + .with_context(|| format!("perf-config-load: could not read {source}"))?; + crate::config::parse(&text, &source) + .with_context(|| format!("perf-config-load: {source} does not parse"))?; + + let bytes = text.len(); + + // WARMUP, discarded. The first call pays page faults on a 354 KB file and + // whatever the allocator has to grow, and including that in a per-call figure + // reports a one-off as a recurring cost. + for _ in 0..samples.min(16) { + crate::config::load(path)?; + } + + let time = |mut body: Box Result<()>>| -> Result> { + let mut times = Vec::with_capacity(samples); + for _ in 0..samples { + let started = std::time::Instant::now(); + body()?; + times.push(started.elapsed().as_secs_f64()); + } + Ok(times) + }; + + let load_arm = summarise( + "load", + &format!("config-load-{bytes}b"), + time(Box::new(|| crate::config::load(path).map(|_| ())))?, + None, + )?; + let parse_arm = summarise( + "parse", + &format!("config-parse-{bytes}b"), + time(Box::new(|| { + crate::config::parse(&text, &source).map(|_| ()) + }))?, + None, + )?; + let null_arm = summarise( + "null", + &format!("config-load-null-{bytes}b"), + time(Box::new(|| crate::config::load(path).map(|_| ())))?, + None, + )?; + + if load_arm.p50 <= 0.0 { + bail!("perf-config-load: the load arm measured zero, so no ratio can be taken."); + } + let ratios = vec![("parse/load".to_owned(), parse_arm.p50 / load_arm.p50)]; + let nulls = vec![null_arm.p50 / load_arm.p50]; + + Ok(Sweep { + arms: vec![load_arm, parse_arm, null_arm], + ratios, + nulls, + // There is no swept variable here — one file, one size — so the + // per-document term has nothing to be about. Reporting a zero would read + // as a measured slope rather than as an absent one. + per_document: None, + }) +} + /// The declared sweep points, in order, with the first as the ratio base. /// /// Split from its environment read so the parse stays exercisable without one, diff --git a/crates/batten/tests/acquisition_metric.rs b/crates/batten/tests/acquisition_metric.rs index a187e19ad..6b4883f30 100644 --- a/crates/batten/tests/acquisition_metric.rs +++ b/crates/batten/tests/acquisition_metric.rs @@ -45,7 +45,20 @@ mod common; /// assertion vacuous the day somebody changed the default. const INVOCATION_METRIC: &str = "wall-clock"; -fn task_body() -> String { +/// Every bench task, and the example target each one must be the invoker of. +/// +/// A TABLE RATHER THAN ONE PAIR, since CLOUD-1291 added the second axis. The +/// hazard was never specific to acquisition: it is that two series sharing a unit +/// and a stamp become diffable, and a second bench makes that a property over a +/// SET rather than a comparison against one default. The anti-vacuity column +/// travels with each row for the reason it did before — a stamp set on some other +/// task cannot satisfy the row it is written for. +const BENCH_TASKS: &[(&str, &str)] = &[ + ("acquisition-bench", "--example acquisition-bench"), + ("config-load-bench", "--example config-load-bench"), +]; + +fn task_body(task: &str) -> String { let manifest = std::fs::read_to_string(common::at_root("mise.toml")) .expect("the manifest is where every task in this repository is declared"); // `toml::from_str`, which is the idiom every reader in `config.rs` and @@ -54,45 +67,76 @@ fn task_body() -> String { let parsed: toml::Value = toml::from_str(&manifest).expect("mise.toml parses as TOML"); parsed .get("tasks") - .and_then(|tasks| tasks.get("acquisition-bench")) - .and_then(|task| task.get("run")) + .and_then(|tasks| tasks.get(task)) + .and_then(|declared| declared.get("run")) .and_then(toml::Value::as_str) - .expect("[tasks.acquisition-bench] declares a run body") + .unwrap_or_else(|| panic!("[tasks.{task}] declares a run body")) .to_owned() } -#[test] -fn the_acquisition_series_is_stamped_with_its_own_metric() { - let body = task_body(); - let stamp = body - .split_whitespace() +fn stamp_of(task: &str, body: &str) -> String { + body.split_whitespace() .find_map(|word| word.strip_prefix("BENCH_METRIC=")) - .expect( - "[tasks.acquisition-bench] sets BENCH_METRIC — without it perf-record \ - stamps the invocation series' default and the two become diffable", + .unwrap_or_else(|| { + panic!( + "[tasks.{task}] sets BENCH_METRIC — without it perf-record stamps \ + the invocation series' default and the two become diffable" + ) + }) + .to_owned() +} + +#[test] +fn every_bench_series_is_stamped_with_its_own_metric() { + for (task, _) in BENCH_TASKS { + let stamp = stamp_of(task, &task_body(task)); + assert!( + !stamp.is_empty(), + "[tasks.{task}]: an empty stamp is the default by another route" + ); + assert_ne!( + stamp, INVOCATION_METRIC, + "[tasks.{task}] must not share the invocation series' stamp: a reader \ + plotting `{INVOCATION_METRIC}` would put a bench arm beside a `--help` \ + invocation and read the gap as a regression" ); + } +} - assert!( - !stamp.is_empty(), - "an empty stamp is the default by another route" - ); - assert_ne!( - stamp, INVOCATION_METRIC, - "the acquisition series must not share the invocation series' stamp: a \ - reader plotting `{INVOCATION_METRIC}` would put a swept fixture arm \ - beside a `--help` invocation and read the gap as a regression" - ); +/// The property one task alone could not have: no two bench series collide. +/// +/// Distinctness from `wall-clock` is what the single-task case asserted, and it +/// stops being the whole claim the moment a second bench exists — two benches +/// could each avoid the default and still stamp each other's axis, which is the +/// same defect with the same symptom and no gate on it. +#[test] +fn no_two_bench_series_share_a_stamp() { + let mut seen: Vec<(&str, String)> = Vec::new(); + for (task, _) in BENCH_TASKS { + let stamp = stamp_of(task, &task_body(task)); + if let Some((other, _)) = seen.iter().find(|(_, taken)| *taken == stamp) { + panic!( + "[tasks.{task}] and [tasks.{other}] both stamp `{stamp}`: their \ + entries land in one series and a reader diffing it reads a step \ + change between two different measurements" + ); + } + seen.push((task, stamp)); + } } -/// ANTI-VACUITY. The case above passes over any string that is not -/// `wall-clock` — including one set by a task that does not exist, if the lookup -/// above were ever loosened to a whole-file scan. This pins that the body read is -/// the one that actually runs the harness. +/// ANTI-VACUITY. The cases above pass over any string that is not `wall-clock` — +/// including one set by a task that does not exist, if the lookup were ever +/// loosened to a whole-file scan. This pins that each body read is the one that +/// actually runs its harness. #[test] -fn the_stamp_is_set_on_the_task_that_runs_the_harness() { - let body = task_body(); - assert!( - body.contains("--example acquisition-bench"), - "the body carrying the stamp is the one invoking the measurement: {body}" - ); +fn each_stamp_is_set_on_the_task_that_runs_its_harness() { + for (task, invocation) in BENCH_TASKS { + let body = task_body(task); + assert!( + body.contains(invocation), + "[tasks.{task}]: the body carrying the stamp is the one invoking the \ + measurement (`{invocation}`): {body}" + ); + } } diff --git a/crates/batten/tests/common/mod.rs b/crates/batten/tests/common/mod.rs index 9898a9dfa..8db3af979 100644 --- a/crates/batten/tests/common/mod.rs +++ b/crates/batten/tests/common/mod.rs @@ -71,6 +71,16 @@ pub(crate) fn target_tmp() -> PathBuf { /// a grammar, and `batten ready lint` tells it so by id. That is the behaviour, /// so a fixture opts IN by calling this rather than getting the rows by default. pub(crate) fn declared_patterns() -> String { + // Memoized for the reason `bypass_env_vars` is (CLOUD-1291): one committed + // file that cannot change during a run. This one only re-READS rather than + // re-parsing, so its own share is the smaller one — it is here because + // leaving one of the three unmemoized is how the next reader concludes the + // pattern was deliberate somewhere and accidental here. + static ROWS: std::sync::LazyLock = std::sync::LazyLock::new(scan_declared_patterns); + ROWS.clone() +} + +fn scan_declared_patterns() -> String { let text = std::fs::read_to_string(at_root("batten.toml")).expect("the committed config"); let mut rows = String::new(); let mut inside = false; @@ -153,18 +163,32 @@ fn declared_env_vars() -> Vec<&'static str> { /// panic — this is a scrub, and a fixture with no committed config is a fixture /// that has no per-row hatches to inherit. fn bypass_env_vars() -> Vec { - let mut names = vec![batten::hook::BYPASS_ENV.to_owned()]; - if let Ok(config) = batten::config::load(&at_root("batten.toml")) { - names.extend( - config - .rules - .iter() - .filter_map(|rule| rule.bypass_env.clone()), - ); - } - names.sort(); - names.dedup(); - names + // MEMOIZED, AND THE DERIVATION IS UNCHANGED (CLOUD-1291). `batten()` calls + // this on every fixture command it constructs, and `config::load` over the + // committed 356 KB authority was measured at **10.48 ms per call** — a full + // parse plus every `validate` pass, of which the file read is 1.2% + // (`mise run config-load-bench`). The file is committed and cannot change + // during a run, so the memoized value is identical by construction. + // + // What is memoized is the RESULT of reading the config, never a hand-written + // list of hatch names. CLOUD-1227 is explicit about why: a list "stops + // covering the next row somebody adds, silently, in the direction that + // weakens the suite". The signature is unchanged so no caller has to know. + static NAMES: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + let mut names = vec![batten::hook::BYPASS_ENV.to_owned()]; + if let Ok(config) = batten::config::load(&at_root("batten.toml")) { + names.extend( + config + .rules + .iter() + .filter_map(|rule| rule.bypass_env.clone()), + ); + } + names.sort(); + names.dedup(); + names + }); + NAMES.clone() } /// The compiled binary, with the ambient environment scrubbed. @@ -704,6 +728,16 @@ pub(crate) fn verdicts(ids: &[&str]) -> Vec { /// engine could never resolve. #[must_use] pub(crate) fn committed_patterns() -> Vec { + // The third of CLOUD-1291's re-reads, memoized on the same reasoning: the + // committed file cannot change during a run, so the parse is repeated work + // over identical bytes. The panics above stay panics — they fire on the first + // call rather than on every one, which is where a reader wants them anyway. + static PATTERNS: std::sync::LazyLock> = + std::sync::LazyLock::new(parse_committed_patterns); + PATTERNS.clone() +} + +fn parse_committed_patterns() -> Vec { let text = std::fs::read_to_string(at_root("batten.toml")).expect("batten.toml is committed"); // `Table` rather than `Value`: this crate's `toml` parses a bare `Value` as a // single VALUE, so a whole document comes back as "unexpected content, diff --git a/mise.toml b/mise.toml index 8a7047194..aa394f7d1 100644 --- a/mise.toml +++ b/mise.toml @@ -1507,6 +1507,32 @@ description = "Measure tree-surface acquisition cost as declared-document count depends = ["build:release"] run = "BENCH_METRIC=acquisition-wall-clock cargo run --quiet --release -p batten --example acquisition-bench" +[tasks.config-load-bench] +description = "Price `config::load` over the committed batten.toml, with its own null (CLOUD-1291)" +# CLOUD-1291. The harness re-reads and re-parses this repository's 354 KB +# `batten.toml` on every fixture command — 761 static call sites, none memoized — +# and nothing had priced it. The row's own first estimate was refuted: it took the +# cost through `batten config show`, which measures the same to within noise from a +# directory with no config at all, so a 29 ms verb cannot resolve a call that may +# be a millisecond. This times the function. +# +# NO `--release`, WHICH IS THE ONE DELIBERATE DEPARTURE from `acquisition-bench` +# above. That arm times the SHIPPED binary, so release is what it is about. This +# one is about what the TEST SUITE pays, and the suite runs under `dev` — where +# workspace code is unoptimised and the dependency closure is `opt-level = 2` +# (CLOUD-1211). Measuring release here would answer a question nobody asked and +# would flatter the number in the direction that argues for the change. +# +# No `build:release` dependency for the same reason, and a stronger one: this +# target spawns nothing and times no binary, so there is nothing for it to build. +# +# BENCH_METRIC IS THE LOAD-BEARING LINE, for the reason recorded at +# `acquisition-bench`: `perf-record` stamps the series from it and defaults to the +# INVOCATION series' `wall-clock`. This is a third axis — a per-call in-process +# figure — so it takes a third stamp, and `crates/batten/tests/acquisition_metric.rs` +# asserts every bench task carries one and that no two of them collide. +run = "BENCH_METRIC=config-load-wall-clock cargo run --quiet -p batten --example config-load-bench" + [tasks."install:local"] description = "Put the built binary where the hook registrations resolve it — `install.sh`'s own destination" depends = ["build:release"] From 6761778f970abce5a85dd2bbf0b2ee3ac90c1619 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 07:15:44 +0000 Subject: [PATCH 3/5] test(harness): fold Fixture::git to one process and drop a rename that already held MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git() spawned two git processes before a fixture repository held anything — init -q, then branch -M main — and base_commit() spent a third on the same rename. Across 150 .git() and 100 .base_commit() call sites that is ~250 process spawns for a repository state that was already correct without them. The rename was redundant, not merely cheap: git_command pins -c init.defaultBranch=main on every invocation, so the default already IS main. Verified through those same pinned flags on git 2.43.0 — init -q alone leaves main, and it is still main after the first commit. DELETED RATHER THAN REPLACED BY -b main, which is what keeps this free of a version floor. -b arrived in git 2.28 and nothing in [tools] pins git, so the flag would put a requirement on the developer's machine that mise.lock cannot hold, to restate a default this harness already controls. Dropping base_commit()'s rename was checked rather than assumed: all 100 call sites are preceded by .git() in the same statement chain, zero counterexamples, so no fixture reaches it through an initialisation whose branch the rename was normalising. The existing suite is the test — ~150 fixtures build a repository through this path — and it is green at 3512 tests, the same count as before the change rather than merely passing. Refs: CLOUD-1290, CLOUD-63 --- crates/batten/tests/common/mod.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/batten/tests/common/mod.rs b/crates/batten/tests/common/mod.rs index 8db3af979..8532881e0 100644 --- a/crates/batten/tests/common/mod.rs +++ b/crates/batten/tests/common/mod.rs @@ -628,20 +628,36 @@ impl Fixture { } /// `git init` the fixture. + /// + /// ONE PROCESS, and the `branch -M main` that used to follow it is deleted + /// rather than replaced by `-b main` (CLOUD-1290). [`git_command`] pins + /// `-c init.defaultBranch=main` on every invocation, so the default already + /// IS `main` and both the rename and the flag restate it. Measured on git + /// 2.43.0 through those same pinned flags: `init -q` alone leaves `main`, and + /// it is still `main` after the first commit. + /// + /// Deleting rather than replacing is what keeps this free of a version floor. + /// `-b` arrived in git 2.28 and there is no `[tools]` entry pinning git, so + /// the flag would have put a requirement on the developer's machine that the + /// lockfile cannot hold — for a default this harness already controls. #[must_use] pub(crate) fn git(self) -> Self { git_in(&self.dir, &["init", "-q"]); - git_in(&self.dir, &["branch", "-M", "main"]); self } /// Commit everything present and pin `origin/main` to it — the trusted base /// ref a pull request is judged against. + /// + /// The `branch -M main` this used to spend a third process on is gone for + /// [`Fixture::git`]'s reason, plus one this call site needs on its own: every + /// `base_commit()` chain in the suite is preceded by `.git()`, checked with + /// zero counterexamples, so there is no fixture arriving here through some + /// other initialisation whose branch the rename was normalising. #[must_use] pub(crate) fn base_commit(self) -> Self { git_in(&self.dir, &["add", "-A"]); git_in(&self.dir, &["commit", "-q", "-m", "base policy"]); - git_in(&self.dir, &["branch", "-M", "main"]); git_in( &self.dir, &["update-ref", "refs/remotes/origin/main", "HEAD"], From 412e2bb24e6c8eed1318da049cb591a2b057e756 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 07:24:06 +0000 Subject: [PATCH 4/5] fix(exec): give the two pipe drains one shared deadline budget, not one each MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIPE_DRAIN_TIMEOUT is 10s and the_drain_deadline_is_long_enough_to_be_about_a_leak asserts that floor. Both read as "a leaked grandchild costs at most ten seconds". It cost twenty: the two drains were collected one after the other, each passed the full constant, so a grandchild holding both pipes open spent it twice. The tee threads were never the problem and are untouched — one per pipe, started at spawn time, already concurrent, and rust.md's concurrency table records that row as staying. What was serial is the DEADLINE ACCOUNTING. Instant::now() is taken once before the first stream and the second gets PIPE_DRAIN_TIMEOUT minus what the first spent. Measured over the compiled binary: a_surviving_grandchild_cannot_hang_exec 20.07s -> 10.044s. That case leaks BOTH pipes already, so it is the both-leaked scenario the fix is about; its one-minute bound simply could not tell ten seconds from twenty. Tightened to 15s rather than adding a second test, which would have to leak both pipes to mean anything and would cost another ten seconds of wall clock for coverage this fixture already has. Shown able to fail: reverting only the two call sites reds it at 20.148s. The zero-remaining half is handled and the row's premise for it is CORRECTED rather than quietly adopted. CLOUD-1288 predicted a false "did not reach EOF" notice on a cleanly-finished second stream, on the premise that recv_timeout(Duration::ZERO) can report Timeout without examining a value already in the channel. Measured on this toolchain, that does not reproduce: 10,000 trials over a ready channel returned the value 10,000 times and timed out zero. collect_within asks the two questions separately anyway — try_recv for "has this stream already finished", and only then the remaining budget for "how long may I wait" — because answering the first through the second leaves a byte-stable channel depending on an mpsc detail std does not promise. a_finished_stream_emits_no_notice_when_the_budget_is_gone pins the property, and its doc says plainly that it does not discriminate the two spellings here rather than claiming a failure it was not shown. PIPE_DRAIN_TIMEOUT and its floor assertion are untouched. Drain::collect is removed rather than annotated: with the budget shared it had no production caller left. Refs: CLOUD-1288 --- crates/batten/src/exec.rs | 142 ++++++++++++++++++++++++--- crates/batten/tests/process_group.rs | 18 ++++ 2 files changed, 147 insertions(+), 13 deletions(-) diff --git a/crates/batten/src/exec.rs b/crates/batten/src/exec.rs index 06fe8faf5..37e57cb4c 100644 --- a/crates/batten/src/exec.rs +++ b/crates/batten/src/exec.rs @@ -591,14 +591,38 @@ impl Drain { } } - /// Wait up to [`PIPE_DRAIN_TIMEOUT`] for EOF, then take what arrived. + /// Wait for EOF against what is LEFT of one shared [`PIPE_DRAIN_TIMEOUT`], + /// then take what arrived. /// /// Returns the bytes and whether the deadline was reached. A timeout is not /// an error: the child's exit code is still the caller's answer, and refusing /// to report it because bookkeeping ran long would turn a leaked grandchild /// into a failed build. - fn collect(self, stream: Stream, report: &mut dyn Write) -> Result<(Vec, capture::Spool)> { - self.collect_within(PIPE_DRAIN_TIMEOUT, stream, report) + /// + /// # Why the budget is shared (CLOUD-1288) + /// + /// [`PIPE_DRAIN_TIMEOUT`] reads as "a leaked grandchild costs at most ten + /// seconds", and `the_drain_deadline_is_long_enough_to_be_about_a_leak` + /// asserts that floor. It cost TWENTY: the two pipes were collected one after + /// the other, each with a fresh full deadline, so a grandchild holding both + /// open spent the constant twice. + /// + /// The tee threads were never the problem and are untouched — one per pipe, + /// started at spawn time, already concurrent, and `.claude/rules/rust.md`'s + /// concurrency table records that row as staying. What was serial is the + /// DEADLINE ACCOUNTING, and this is where it stops being: `started` is taken + /// once before the first stream, so the second gets whatever the first left. + fn collect_remaining( + self, + started: std::time::Instant, + stream: Stream, + report: &mut dyn Write, + ) -> Result<(Vec, capture::Spool)> { + self.collect_within( + PIPE_DRAIN_TIMEOUT.saturating_sub(started.elapsed()), + stream, + report, + ) } /// [`Self::collect`] with the deadline supplied. @@ -612,15 +636,33 @@ impl Drain { stream: Stream, report: &mut dyn Write, ) -> Result<(Vec, capture::Spool)> { - let timed_out = match self.outcome.recv_timeout(deadline) { - Ok(result) => { - result.with_context(|| format!("tee the wrapped command's {}", stream.as_str()))?; - false - } + // A NON-BLOCKING TAKE FIRST, AND IT IS NOT AN OPTIMISATION (CLOUD-1288). + // Once the budget is shared, `deadline` can legitimately be ZERO — the + // first stream spent it all — and `recv_timeout(Duration::ZERO)` is + // permitted to report `Timeout` without ever examining a value that is + // already sitting in the channel. A stderr whose tee finished cleanly + // seconds earlier would then be reported as "did not reach EOF", which is + // a FALSE pointer on a byte-stable channel (house-style §6), fired on + // exactly the runs this change exists to speed up. + // + // So the two questions are asked separately: "has this stream already + // finished" is `try_recv`, and only when the answer is no does the + // remaining budget get waited on. The bytes were always safe; this is the + // output half. + let finished = |result: Result<()>| -> Result { + result.with_context(|| format!("tee the wrapped command's {}", stream.as_str()))?; + Ok(false) + }; + let timed_out = match self.outcome.try_recv() { + Ok(result) => finished(result)?, // Disconnected without a value means the tee thread died without // reporting — a panic. The bytes it did append are still real. - Err(mpsc::RecvTimeoutError::Disconnected) => false, - Err(mpsc::RecvTimeoutError::Timeout) => true, + Err(mpsc::TryRecvError::Disconnected) => false, + Err(mpsc::TryRecvError::Empty) => match self.outcome.recv_timeout(deadline) { + Ok(result) => finished(result)?, + Err(mpsc::RecvTimeoutError::Disconnected) => false, + Err(mpsc::RecvTimeoutError::Timeout) => true, + }, }; let bytes = match self.seen.lock() { Ok(held) => held.clone(), @@ -1631,9 +1673,16 @@ fn run_one( // write end keeps the pipe open past the child's own death, and a bare // `join()` on that is a hang with no upper bound. The notices are BUFFERED // rather than written, so a bundle's diagnostics land in declaration order. + // + // ONE BUDGET ACROSS BOTH, taken here rather than per stream: `PIPE_DRAIN_TIMEOUT` + // and its floor assertion both describe what a leak costs in total, and two + // fresh deadlines made that twenty seconds (CLOUD-1288). let mut notices = Vec::new(); - let (out_bytes, out_spool) = out_drain.collect(Stream::Stdout, &mut notices)?; - let (err_bytes, err_spool) = err_drain.collect(Stream::Stderr, &mut notices)?; + let drain_started = std::time::Instant::now(); + let (out_bytes, out_spool) = + out_drain.collect_remaining(drain_started, Stream::Stdout, &mut notices)?; + let (err_bytes, err_spool) = + err_drain.collect_remaining(drain_started, Stream::Stderr, &mut notices)?; // Both streams are stored, including an empty one: zero bytes is the real // answer "the command said nothing", and it must be distinguishable from a run @@ -2199,12 +2248,79 @@ mod tests { ); let mut report = Vec::new(); let (bytes, _spool) = drain - .collect(Stream::Stdout, &mut report) + .collect_within(PIPE_DRAIN_TIMEOUT, Stream::Stdout, &mut report) .expect("clean EOF"); assert_eq!(bytes, b"hello"); assert!(report.is_empty(), "the happy path emits nothing"); } + /// The zero-remaining case, and it is an OUTPUT property rather than a data + /// one (CLOUD-1288). + /// + /// Once the two drains share one budget, the second stream's deadline can + /// legitimately be `Duration::ZERO` — the first spent it all. A stderr whose + /// tee finished cleanly seconds earlier must still emit NOTHING: a "did not + /// reach EOF" pointer on a healthy stream is a false finding on a channel + /// house-style §6 requires to be byte-stable, and it would fire on exactly + /// the runs the shared budget exists to speed up. + /// + /// # What this case does and does not discriminate, measured rather than assumed + /// + /// CLOUD-1288 predicted this would fail against a naive `saturating_sub`, + /// on the premise that `recv_timeout(Duration::ZERO)` can report `Timeout` + /// without examining a value already in the channel. **That does not + /// reproduce on this toolchain**: 10,000 trials over a channel holding a + /// ready value returned it 10,000 times and reported `Timeout` zero times. + /// + /// So this case pins the property rather than separating the two spellings, + /// and that is worth saying plainly instead of claiming a discrimination it + /// does not have. The `try_recv` in `collect_within` stands anyway, because + /// "has this stream already finished" and "how long may I wait" are different + /// questions, and answering the first through the second leaves the notice + /// depending on an `mpsc` detail std does not promise. + #[test] + fn a_finished_stream_emits_no_notice_when_the_budget_is_gone() { + let drain = Drain::spawn( + &b"hello"[..], + std::io::sink(), + scratch_spool("zero-budget", capture::LiveStream::STDERR), + ); + // The tee appends before it reports, so wait for the bytes and then for + // the outcome to follow them. Same poll idiom as the timeout case above, + // and for the same reason: without it this would assert "gave up before + // anything arrived", which is the opposite property. + while drain.seen.lock().map_or(true, |held| held.len() < 5) { + #[expect( + clippy::disallowed_methods, + reason = "the interval of a poll whose exit condition is the `seen` buffer holding \ + the bytes the tee speaks; the case is about what happens AFTER a stream \ + finished, so it has to wait for it to finish (CLOUD-1177)" + )] + std::thread::sleep(Duration::from_millis(10)); + } + #[expect( + clippy::disallowed_methods, + reason = "the tee appends to `seen` and only then sends its outcome, so the poll above \ + can exit inside that window; this closes it. The subject is a stream that \ + has ALREADY finished, and racing its own completion would test the other \ + case (CLOUD-1288)" + )] + std::thread::sleep(Duration::from_millis(200)); + + let mut report = Vec::new(); + let (bytes, _spool) = drain + .collect_within(Duration::ZERO, Stream::Stderr, &mut report) + .expect("a finished stream is not a failed command"); + assert_eq!(bytes, b"hello", "the bytes are taken regardless"); + assert!( + report.is_empty(), + "a stream that reached EOF must emit no notice even at a zero \ + remaining budget — the notice says a process still holds the pipe \ + open, and none does: {}", + String::from_utf8_lossy(&report) + ); + } + #[test] fn the_drain_deadline_is_long_enough_to_be_about_a_leak() { // A deadline short enough to fire on a slow-but-healthy command would diff --git a/crates/batten/tests/process_group.rs b/crates/batten/tests/process_group.rs index 7b27726a3..26a55ba6c 100644 --- a/crates/batten/tests/process_group.rs +++ b/crates/batten/tests/process_group.rs @@ -809,5 +809,23 @@ fn a_surviving_grandchild_cannot_hang_exec() { took < Duration::from_mins(1), "exec must be bounded by the drain deadline, not by the grandchild: {took:?}" ); + // ONE BUDGET, NOT TWO (CLOUD-1288). `sleep 300 &` inherits BOTH pipes, so + // this is already the both-leaked case — and the assertion above could not + // tell ten seconds from twenty, which is exactly what the defect cost: the + // two streams were collected with a fresh full `PIPE_DRAIN_TIMEOUT` each, so + // the constant that reads "at most ten seconds" shipped as twenty. Measured + // at 20.07s before the fix, the single slowest case in the suite by 2.5x. + // + // A NEW case was considered and rejected: it would have to leak both pipes + // to mean anything, which is another ten seconds of wall clock for coverage + // this fixture already has. Tightening the bound here is the same assertion + // for free. The generous slack is deliberate — the subject is 10 versus 20, + // and a bound tight enough to fire on a loaded container would be a flake + // asserting about scheduling rather than about the budget. + assert!( + took < Duration::from_secs(15), + "the two pipe drains share ONE deadline budget, so both being leaked costs \ + one PIPE_DRAIN_TIMEOUT and not two: {took:?}" + ); signal(u32::try_from(holder).expect("a positive pid"), "KILL"); } From 4d5bd0cc85ca1b35c663745acf7d6096d5b64f06 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 07:57:50 +0000 Subject: [PATCH 5/5] test(harness): group 144 integration test targets into two, and ratchet the count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cargo autodiscovered one test target per top-level crates/batten/tests/*.rs and rustc relinked the whole closure — gix, regorus, syn, clap, jsonschema, hyper/rustls — into each of 144 binaries. matklad's Delete Cargo Integration Tests states the defect and the layout; Cargo's own repository measured 3x off test compile time and 5x off artifacts making the same move. MEASURED HERE, paired on one machine, with a REAL edit rather than a touch — the row is explicit that touch changes mtime with identical content, so every target relinks but incremental codegen has nothing new to do, which understates the lib-compile half: rebuild after editing one src/*.rs, 144 targets: 48.0s rebuild after editing one src/*.rs, 2 targets: 7.4s / 7.3s 6.5x, against a within-arm null of 0.986. Bytes, cold and clean, against the same container's 144-target census taken under the same adopted split-debuginfo profile: 144 targets 2 targets linked artifacts 147 4 linked bytes 4.99 GB 234 MB deps total 5.87 GB 1.09 GB target/debug 7.76 GB 2.05 GB 21x off the linked binaries and 3.8x off target/debug — the reading CLOUD-1210's third section needed: a verify lap that consumed more than the container could hold now fits several times over. The run phase is untouched at 3519 tests, exactly as the row's §2 says: this is a build-time and bytes change and nothing else. THE RATCHET IS THE POINT, not the one-off saving. policy/test-targets.rego refuses an ADDED path that is a top-level crates/batten/tests/*.rs — four path segments — because cargo autodiscovery makes that 1:1 with a target. A file one segment deeper is a module and costs nothing, which is what keeps CLOUD-843's campaign able to land its mandated tier without the gate having to be switched off. ITS DEPTH TEST SHIPPED WRONG FIRST AND THE SECOND TIER CAUGHT IT. The count was written as 5 where the path splits to 4, exactly inverted: it refused the grouped module and allowed the new target. The module's own test_ rules agreed with the mistake, because a with-input case is only as right as its author. crates/batten/tests/it/test_targets.rs over the compiled engine is what failed — the class .claude/rules/policy-modules.md calls the second tier not optional for, and it is now the first #MUTANT row. ONE FILE STAYS A TARGET, AND THE REASON IS A GATE RATHER THAN A COMPROMISE. policy_modules.rs keeps its own target because `evaluator-io-check` probes it with `cargo test --test policy_modules`, and that task is a governed mise-tasks/*.sh: shell-retirement gives it exactly two shapes — retire it whole, or leave it alone — so repointing the probe is not an edit this change may make. Measured, not assumed: the edit was attempted and batten-check refused it as shell-rule-retired. One extra link is the cheaper side of that trade against a gate that stays live, and the ratchet is unaffected — the file exists at base, so it is never in base-delta.added. THE MOVE BREAKS EVERY GLOB THAT SPELLED tests/*.rs, and that is the silent half. batten's globs compile with literal_separator(true), so * stops at a /: five committed rows plus one hardcoded in rules.rs would have selected NOTHING and reported clean over an empty set. Repointed to **/*.rs. Four nextest filtersets named binaries that are now modules; --no-tests=fail is what made those loud. mise-tasks/replay.sh needs no edit — its declared_in feeds git grep as a PATHSPEC, where * does cross a /, so the governed file is untouched. AND IT MOVES [prune]'S BASIS, which CLOUD-1210's §2 predicted and scoped out. The floors were measured against a tree where a tracked test FILE was a proxy for a linked STEM — exact while cargo autodiscovered one target per file, and no longer a series at all once 144 became 2. target-prune refused on it (declared 140, live 152, tolerance 10), so count and measured move together as that block instructs. The FLOORS deliberately do not move: both are now far above what the tree needs, which is the safe direction on the block's own terms, and re-deriving them downward needs the independent measurement it names — CLOUD-1158's. target_consolidation.rs asserts what makes this safe rather than citing it: nextest runs each test in its own process, so isolation survives the target boundary going away. Also repointed: insta's four snapshots (payloads byte-identical, only the source header moved), relative include! paths, the retirement ledger's carried arms, and the doc citations README and .claude/rules/* make. Refs: CLOUD-1210, CLOUD-766, CLOUD-1158, CLOUD-843, CLOUD-55 Admits: ffdf8396b47739f87822037710ac7635da092135ea0d60a06a35be196015e8a0 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd Admits-epoch: 6653a7e618ce30bcce12361e2d99a731c68d00143157f0e3af4be10ca574ba0b Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: CLOUD-1210's ratchet cannot be registered at all. The grouping would buy a one-off saving instead of a property: cargo autodiscovery mints a target per top-level crates/batten/tests/*.rs, CLOUD-843's retirement campaign adds one per retired gate by mandate, and the count was measured climbing 142 to 144 in eight commits. Without the row the 144-to-1 consolidation regrows one retirement at a time and nothing reds. Admits-answer-precondition: batten.toml is the one committed authority for [[rule]] and [[verdict]] rows (house-style §8), so a new policy rule and the verdict token it raises have no other surface to be expressed on: a module raising a token no row declares fails to LOAD, and a row nothing raises fails the load too, so both halves must land in this file or the change cannot exist. The write is CLOUD-1210's registration of policy/test-targets.rego, and it lands in the diff of a pull request a reviewer reads. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because this file IS the owning surface for rule and verdict registration; there is no other one to route to. R-RESTORE-IT does not apply because nothing was damaged to restore — this is an addition of two rows, not a repair of an edit that should not have happened. Admits: 8e8a4fd331608067ab55d5c6d020737b55aa7066d575634f9ac6cb95c653ae99 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd Admits-epoch: 08c3d9251f7d28897e729d704e02166eeda5345cbe38ff8dc7205bcebc200360 Admits-author: alec@wenzowski.com Admits-prev: ffdf8396b47739f87822037710ac7635da092135ea0d60a06a35be196015e8a0 Admits-answer-lost: policy/test-targets.rego would sit in the tree registered by nothing and decide nothing, while V-TEST-TARGET-ADDED would be a declared verdict no rule raises — which fails the load in the other direction. CLOUD-1210's grouping would then hold no property: the 144-to-1 consolidation regrows one retirement at a time, unrefused. Admits-answer-precondition: The second half of the same registration: policy/test-targets.rego needs a [[rule]] row to be loaded at all, and batten.toml is the one committed authority for rules (house-style §8). A module no row registers is never evaluated — a dead gate that loads clean, which is the exact failure class .claude/rules/policy-modules.md exists to warn about. The verdict half landed under the previous admission; this is the row that makes it reachable, in the same pull request diff. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for [[rule]] registration; there is nowhere else to route a rule row. R-RESTORE-IT does not apply because nothing was damaged — this adds a row rather than repairing an edit that should not have happened. Admits: 49aafbb7b01db2868770e47436354525049360ca541e4fe11a0624549039cdac Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd Admits-epoch: 29cd0ab87cc14f65ac2eb49fc3fafc5aaefbaa196d5e3e29903e0779ff844021 Admits-author: alec@wenzowski.com Admits-prev: 8e8a4fd331608067ab55d5c6d020737b55aa7066d575634f9ac6cb95c653ae99 Admits-answer-lost: Five gates go silently dead: the shell-retirement ledger's declared_in, the line_sources for its arms, an exact-path row, and two rows globbing the test tier. Each would then report clean over an empty file set, which is worse than a wrong answer because a gate that found nothing looks exactly like a gate that passed. Admits-answer-precondition: CLOUD-1210 moves every top-level test file into the tests/it/ group, and batten's globs compile with globset literal_separator(true) — so an asterisk stops at a slash and five committed rows would match NOTHING afterwards. A glob that selects nothing is a gate passing on emptiness, the silent-dead-gate class this repository is most exposed to. Those rows live in this file and nowhere else, so repointing them is only expressible here, in the same pull request diff as the move a reviewer reads. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because this file IS the owning surface for a rule's globs; there is nowhere else to route them. R-RESTORE-IT does not apply because nothing is being restored — the paths these rows name are moving, and the rows must follow or they select nothing. Admits: 2899d8657165ec05ec52be34efeefcdcd23242ddf7c5cb1bc4052a500c2f06a2 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: policy/test-targets.rego Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd Admits-epoch: 7f31bd1e3f27a45f279c38462b59167c4592ef855b219bf5904a60aaf6a5bdba Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: The ratchet ships backwards. It would refuse every retirement's tier landing correctly inside the group, and allow the new top-level file that mints a second cargo test target, so CLOUD-1210's 144-to-1 consolidation would regrow while the gate reported clean and blocked the campaign it was designed to survive. Admits-answer-precondition: Fixing a defect in the module I am landing under CLOUD-1210, found by its own compiled-binary tier. The depth test was written as count(segments)==5, which is exactly inverted: crates/batten/tests/x.rs splits to FOUR segments, so the rule refused the grouped module and allowed the new target — the one direction that fails silently. A module's predicate is only expressible in the module, so this write is the only route, and it lands in the pull request diff a reviewer reads. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because this file IS the owning surface for its own predicate. R-RESTORE-IT does not apply because there is nothing to restore — the file is new in this branch and has never been correct; this is the fix, not a revert. Admits: eb9e5bb3a4314b722a1e01e445201182b23e80ca40b64cbdd544827434f6935b Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: policy/test-targets.rego Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd Admits-epoch: 7f31bd1e3f27a45f279c38462b59167c4592ef855b219bf5904a60aaf6a5bdba Admits-author: alec@wenzowski.com Admits-prev: 2899d8657165ec05ec52be34efeefcdcd23242ddf7c5cb1bc4052a500c2f06a2 Admits-answer-lost: The gate cannot land: mutant-census exits 1, so verify and CI refuse the change. And the substantive loss it is pointing at is real — a gate covered by nothing stronger than its own green suite is the vacuity CLOUD-418 measured four times, so the discriminating mutations have to be written down even where no runner can drive them yet. Admits-answer-precondition: mutant-census refuses the new module as uncovered, and its own remedy is a directive that can only live inside the module: a #MUTANT row declaring a discriminating mutation, plus a #MUTANT-EXEMPT naming the issue, exactly as policy/ci-parity.rego and policy/ci-suite-lane.rego carry them. There is no other surface for a comment directive in a .rego file, and the write lands in the same pull request diff a reviewer reads. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because a #MUTANT directive is resolved from the module file itself; there is no owning surface elsewhere. R-RESTORE-IT does not apply because nothing is being restored — this adds the coverage declaration the census asks for. Weakens: rule-predicate-changed rule[bats-tests-not-deleted].conserves Weakens: rule-predicate-changed rule[no-key-leaves-the-schema-unannounced].glob Weakens: rule-predicate-changed rule[shell-retirement].line_sources --- .claude/rules/policy-modules.md | 2 +- .claude/rules/rust.md | 4 +- .claude/rules/scanning.md | 2 +- .claude/rules/toolchain.md | 6 +- README.md | 6 +- batten.toml | 72 +++++- crates/batten/src/facts.rs | 2 +- crates/batten/src/hook.rs | 2 +- crates/batten/src/lib.rs | 2 +- crates/batten/src/outputs.rs | 2 +- crates/batten/src/perf.rs | 4 +- crates/batten/src/policy.rs | 2 +- crates/batten/src/ready.rs | 2 +- crates/batten/src/rules.rs | 9 +- crates/batten/src/taskset.rs | 2 +- crates/batten/src/uses.rs | 2 +- .../tests/{ => it}/acceptance_corpus.rs | 8 +- .../tests/{ => it}/acquisition_metric.rs | 2 +- .../tests/{ => it}/acquisition_sweep.rs | 2 +- crates/batten/tests/{ => it}/admission.rs | 2 +- .../batten/tests/{ => it}/advisory_drain.rs | 2 +- crates/batten/tests/{ => it}/agent_facts.rs | 0 .../tests/{ => it}/ambient_authority.rs | 2 +- crates/batten/tests/{ => it}/attribution.rs | 2 +- .../batten/tests/{ => it}/authority_replay.rs | 2 +- crates/batten/tests/{ => it}/baseline.rs | 2 +- .../batten/tests/{ => it}/bats_invocation.rs | 6 +- .../batten/tests/{ => it}/board_receipts.rs | 140 +++++------ crates/batten/tests/{ => it}/board_record.rs | 64 ++--- crates/batten/tests/{ => it}/bundle.rs | 2 +- crates/batten/tests/{ => it}/bypass_scrub.rs | 2 +- .../batten/tests/{ => it}/call_arguments.rs | 2 +- .../tests/{ => it}/call_background_flag.rs | 2 +- crates/batten/tests/{ => it}/call_ceiling.rs | 2 +- .../batten/tests/{ => it}/capture_fidelity.rs | 4 +- .../batten/tests/{ => it}/captured_facts.rs | 2 +- crates/batten/tests/{ => it}/checks_green.rs | 6 +- crates/batten/tests/{ => it}/ci_hygiene.rs | 2 +- crates/batten/tests/{ => it}/ci_parity.rs | 204 ++++++++-------- crates/batten/tests/{ => it}/ci_suite_lane.rs | 2 +- crates/batten/tests/{ => it}/claim.rs | 152 ++++++------ crates/batten/tests/{ => it}/claim_receipt.rs | 2 +- crates/batten/tests/{ => it}/cli.rs | 6 +- crates/batten/tests/{ => it}/commit.rs | 2 +- .../batten/tests/{ => it}/commit_admission.rs | 2 +- .../tests/{ => it}/commit_meta_facts.rs | 2 +- crates/batten/tests/{ => it}/common/mod.rs | 0 .../{ => it}/config_authority_boundary.rs | 0 .../tests/{ => it}/config_base_ref_reading.rs | 2 +- .../tests/{ => it}/config_deprecations.rs | 22 +- crates/batten/tests/{ => it}/config_epoch.rs | 2 +- .../tests/{ => it}/config_in_directory.rs | 2 +- crates/batten/tests/{ => it}/config_lint.rs | 2 +- .../tests/{ => it}/config_provenance.rs | 2 +- crates/batten/tests/{ => it}/config_schema.rs | 24 +- crates/batten/tests/{ => it}/config_show.rs | 2 +- crates/batten/tests/{ => it}/config_trust.rs | 2 +- .../tests/{ => it}/connector_allow_door.rs | 2 +- .../tests/{ => it}/connector_not_granted.rs | 4 +- .../batten/tests/{ => it}/connector_verbs.rs | 36 +-- .../batten/tests/{ => it}/contract_drift.rs | 36 +-- .../batten/tests/{ => it}/decision_record.rs | 2 +- crates/batten/tests/{ => it}/defects.rs | 2 +- crates/batten/tests/{ => it}/derived_facts.rs | 2 +- crates/batten/tests/{ => it}/design_audit.rs | 2 +- crates/batten/tests/{ => it}/dev_profile.rs | 2 +- crates/batten/tests/{ => it}/doctor.rs | 2 +- .../batten/tests/{ => it}/document_facts.rs | 2 +- .../tests/{ => it}/document_read_count.rs | 2 +- .../batten/tests/{ => it}/done_not_landed.rs | 2 +- .../batten/tests/{ => it}/enforce_journal.rs | 2 +- .../tests/{ => it}/extension_surfaces.rs | 6 +- .../batten/tests/{ => it}/external_facts.rs | 2 +- .../batten/tests/{ => it}/extracted_facts.rs | 2 +- crates/batten/tests/{ => it}/facts.rs | 0 .../batten/tests/{ => it}/fail_on_warning.rs | 2 +- crates/batten/tests/{ => it}/filed_here.rs | 52 ++-- crates/batten/tests/{ => it}/fixture_repos.rs | 2 +- crates/batten/tests/{ => it}/forge_facts.rs | 2 +- crates/batten/tests/{ => it}/fuzz_corpus.rs | 15 +- crates/batten/tests/{ => it}/gh_guard.rs | 2 +- crates/batten/tests/{ => it}/git_facts.rs | 2 +- .../batten/tests/{ => it}/glob_exclusion.rs | 2 +- .../batten/tests/{ => it}/guardrail_bypass.rs | 2 +- crates/batten/tests/{ => it}/harness_grant.rs | 4 +- crates/batten/tests/{ => it}/history_facts.rs | 2 +- .../batten/tests/{ => it}/hk_fix_selection.rs | 2 +- crates/batten/tests/{ => it}/hook_profile.rs | 2 +- .../tests/{ => it}/hook_worktree_root.rs | 2 +- .../batten/tests/{ => it}/identity_churn.rs | 2 +- .../tests/{ => it}/identity_precedence.rs | 2 +- crates/batten/tests/{ => it}/init.rs | 2 +- .../tests/{ => it}/inverted_board_cases.rs | 2 +- crates/batten/tests/{ => it}/issue_key.rs | 2 +- crates/batten/tests/{ => it}/judge_kind.rs | 2 +- crates/batten/tests/it/main.rs | 188 +++++++++++++++ crates/batten/tests/{ => it}/mcp_dispatch.rs | 2 +- .../tests/{ => it}/mediated_admission.rs | 2 +- .../batten/tests/{ => it}/mediated_verbs.rs | 2 +- crates/batten/tests/{ => it}/memories.rs | 2 +- .../batten/tests/{ => it}/memory_injection.rs | 2 +- .../tests/{ => it}/mise_pin_agreement.rs | 28 +-- .../batten/tests/{ => it}/narrow_adoption.rs | 8 +- crates/batten/tests/{ => it}/perf_pair.rs | 8 +- .../batten/tests/{ => it}/pinned_programs.rs | 4 +- .../batten/tests/{ => it}/pipeline_shapes.rs | 2 +- crates/batten/tests/{ => it}/pointer_only.rs | 4 +- .../tests/{ => it}/policy_engine_count.rs | 0 .../tests/{ => it}/policy_input_narrowing.rs | 0 .../tests/{ => it}/policy_input_schema.rs | 0 .../batten/tests/{ => it}/policy_presets.rs | 2 +- .../batten/tests/{ => it}/policy_severity.rs | 2 +- .../tests/{ => it}/policy_test_suite.rs | 2 +- crates/batten/tests/{ => it}/policy_tree.rs | 2 +- .../batten/tests/{ => it}/policy_whole_set.rs | 0 crates/batten/tests/{ => it}/pr_watch.rs | 6 +- crates/batten/tests/{ => it}/prebuilt_lint.rs | 16 +- .../batten/tests/{ => it}/preset_segments.rs | 4 +- crates/batten/tests/{ => it}/primitives.rs | 2 +- .../batten/tests/{ => it}/privileged_lane.rs | 14 +- crates/batten/tests/{ => it}/process_group.rs | 2 +- crates/batten/tests/{ => it}/prose_only.rs | 38 +-- .../tests/{ => it}/prospective_facts.rs | 2 +- crates/batten/tests/{ => it}/provision.rs | 2 +- crates/batten/tests/{ => it}/ratchet.rs | 2 +- crates/batten/tests/{ => it}/ready.rs | 170 ++++++------- .../tests/{ => it}/reference_coverage.rs | 26 +- .../tests/{ => it}/remedy_authorship.rs | 2 +- .../tests/{ => it}/retirement_doctrine.rs | 2 +- .../batten/tests/{ => it}/review_answered.rs | 28 +-- .../batten/tests/{ => it}/rule_cost_census.rs | 0 .../tests/{ => it}/rules_builtin_claims.rs | 4 +- crates/batten/tests/{ => it}/rules_drift.rs | 2 +- crates/batten/tests/{ => it}/run_shape.rs | 32 +-- .../tests/{ => it}/run_shape_guard_door.rs | 2 +- .../batten/tests/{ => it}/runner_verdict.rs | 0 .../batten/tests/{ => it}/scanner_taxonomy.rs | 2 +- crates/batten/tests/{ => it}/secrets_kind.rs | 2 +- crates/batten/tests/{ => it}/semver_gate.rs | 22 +- .../batten/tests/{ => it}/shell_retirement.rs | 2 +- .../tests/{ => it}/shell_write_advisory.rs | 2 +- crates/batten/tests/{ => it}/sinks.rs | 2 +- .../batten/tests/{ => it}/skill_contract.rs | 50 ++-- crates/batten/tests/{ => it}/sleep_ban.rs | 2 +- crates/batten/tests/{ => it}/snapshots.rs | 2 +- ...t__snapshots__golden_exit_code_table.snap} | 2 +- .../it__snapshots__golden_json_schema.snap} | 2 +- ...it__snapshots__json_output_is_frozen.snap} | 2 +- ..._snapshots__pointer_output_is_frozen.snap} | 2 +- .../batten/tests/{ => it}/spawn_ceilings.rs | 26 +- crates/batten/tests/{ => it}/spawn_census.rs | 2 +- crates/batten/tests/{ => it}/staged_facts.rs | 2 +- crates/batten/tests/{ => it}/stop_posture.rs | 54 ++--- crates/batten/tests/{ => it}/submodule.rs | 2 +- .../batten/tests/{ => it}/suite_subjects.rs | 2 +- crates/batten/tests/{ => it}/surface.rs | 28 +-- crates/batten/tests/{ => it}/symbols.rs | 0 .../batten/tests/it/target_consolidation.rs | 75 ++++++ crates/batten/tests/{ => it}/target_prune.rs | 48 ++-- crates/batten/tests/{ => it}/task_prose.rs | 2 +- crates/batten/tests/{ => it}/task_receipt.rs | 2 +- crates/batten/tests/it/test_targets.rs | 227 ++++++++++++++++++ .../batten/tests/{ => it}/todo_promotion.rs | 4 +- crates/batten/tests/{ => it}/tool_selector.rs | 2 +- .../tests/{ => it}/tool_verdict_facts.rs | 2 +- crates/batten/tests/{ => it}/use_graph.rs | 0 .../batten/tests/{ => it}/verdict_registry.rs | 2 +- crates/batten/tests/{ => it}/waivers.rs | 10 +- crates/batten/tests/{ => it}/walker.rs | 2 +- .../batten/tests/{ => it}/wiring_reclaim.rs | 2 +- crates/batten/tests/{ => it}/zero_config.rs | 2 +- crates/batten/tests/policy_modules.rs | 10 + hk.pkl | 2 +- mise.toml | 8 +- policy/test-targets.rego | 152 ++++++++++++ 175 files changed, 1564 insertions(+), 838 deletions(-) rename crates/batten/tests/{ => it}/acceptance_corpus.rs (99%) rename crates/batten/tests/{ => it}/acquisition_metric.rs (99%) rename crates/batten/tests/{ => it}/acquisition_sweep.rs (99%) rename crates/batten/tests/{ => it}/admission.rs (99%) rename crates/batten/tests/{ => it}/advisory_drain.rs (99%) rename crates/batten/tests/{ => it}/agent_facts.rs (100%) rename crates/batten/tests/{ => it}/ambient_authority.rs (99%) rename crates/batten/tests/{ => it}/attribution.rs (99%) rename crates/batten/tests/{ => it}/authority_replay.rs (99%) rename crates/batten/tests/{ => it}/baseline.rs (99%) rename crates/batten/tests/{ => it}/bats_invocation.rs (96%) rename crates/batten/tests/{ => it}/board_receipts.rs (89%) rename crates/batten/tests/{ => it}/board_record.rs (93%) rename crates/batten/tests/{ => it}/bundle.rs (99%) rename crates/batten/tests/{ => it}/bypass_scrub.rs (99%) rename crates/batten/tests/{ => it}/call_arguments.rs (99%) rename crates/batten/tests/{ => it}/call_background_flag.rs (99%) rename crates/batten/tests/{ => it}/call_ceiling.rs (99%) rename crates/batten/tests/{ => it}/capture_fidelity.rs (98%) rename crates/batten/tests/{ => it}/captured_facts.rs (99%) rename crates/batten/tests/{ => it}/checks_green.rs (99%) rename crates/batten/tests/{ => it}/ci_hygiene.rs (99%) rename crates/batten/tests/{ => it}/ci_parity.rs (81%) rename crates/batten/tests/{ => it}/ci_suite_lane.rs (99%) rename crates/batten/tests/{ => it}/claim.rs (92%) rename crates/batten/tests/{ => it}/claim_receipt.rs (99%) rename crates/batten/tests/{ => it}/cli.rs (99%) rename crates/batten/tests/{ => it}/commit.rs (99%) rename crates/batten/tests/{ => it}/commit_admission.rs (99%) rename crates/batten/tests/{ => it}/commit_meta_facts.rs (99%) rename crates/batten/tests/{ => it}/common/mod.rs (100%) rename crates/batten/tests/{ => it}/config_authority_boundary.rs (100%) rename crates/batten/tests/{ => it}/config_base_ref_reading.rs (99%) rename crates/batten/tests/{ => it}/config_deprecations.rs (93%) rename crates/batten/tests/{ => it}/config_epoch.rs (99%) rename crates/batten/tests/{ => it}/config_in_directory.rs (99%) rename crates/batten/tests/{ => it}/config_lint.rs (99%) rename crates/batten/tests/{ => it}/config_provenance.rs (99%) rename crates/batten/tests/{ => it}/config_schema.rs (98%) rename crates/batten/tests/{ => it}/config_show.rs (99%) rename crates/batten/tests/{ => it}/config_trust.rs (99%) rename crates/batten/tests/{ => it}/connector_allow_door.rs (99%) rename crates/batten/tests/{ => it}/connector_not_granted.rs (98%) rename crates/batten/tests/{ => it}/connector_verbs.rs (86%) rename crates/batten/tests/{ => it}/contract_drift.rs (95%) rename crates/batten/tests/{ => it}/decision_record.rs (99%) rename crates/batten/tests/{ => it}/defects.rs (99%) rename crates/batten/tests/{ => it}/derived_facts.rs (99%) rename crates/batten/tests/{ => it}/design_audit.rs (99%) rename crates/batten/tests/{ => it}/dev_profile.rs (99%) rename crates/batten/tests/{ => it}/doctor.rs (99%) rename crates/batten/tests/{ => it}/document_facts.rs (99%) rename crates/batten/tests/{ => it}/document_read_count.rs (99%) rename crates/batten/tests/{ => it}/done_not_landed.rs (99%) rename crates/batten/tests/{ => it}/enforce_journal.rs (99%) rename crates/batten/tests/{ => it}/extension_surfaces.rs (98%) rename crates/batten/tests/{ => it}/external_facts.rs (99%) rename crates/batten/tests/{ => it}/extracted_facts.rs (99%) rename crates/batten/tests/{ => it}/facts.rs (100%) rename crates/batten/tests/{ => it}/fail_on_warning.rs (99%) rename crates/batten/tests/{ => it}/filed_here.rs (92%) rename crates/batten/tests/{ => it}/fixture_repos.rs (99%) rename crates/batten/tests/{ => it}/forge_facts.rs (99%) rename crates/batten/tests/{ => it}/fuzz_corpus.rs (84%) rename crates/batten/tests/{ => it}/gh_guard.rs (99%) rename crates/batten/tests/{ => it}/git_facts.rs (99%) rename crates/batten/tests/{ => it}/glob_exclusion.rs (99%) rename crates/batten/tests/{ => it}/guardrail_bypass.rs (99%) rename crates/batten/tests/{ => it}/harness_grant.rs (98%) rename crates/batten/tests/{ => it}/history_facts.rs (99%) rename crates/batten/tests/{ => it}/hk_fix_selection.rs (99%) rename crates/batten/tests/{ => it}/hook_profile.rs (99%) rename crates/batten/tests/{ => it}/hook_worktree_root.rs (99%) rename crates/batten/tests/{ => it}/identity_churn.rs (99%) rename crates/batten/tests/{ => it}/identity_precedence.rs (99%) rename crates/batten/tests/{ => it}/init.rs (99%) rename crates/batten/tests/{ => it}/inverted_board_cases.rs (99%) rename crates/batten/tests/{ => it}/issue_key.rs (99%) rename crates/batten/tests/{ => it}/judge_kind.rs (99%) create mode 100644 crates/batten/tests/it/main.rs rename crates/batten/tests/{ => it}/mcp_dispatch.rs (99%) rename crates/batten/tests/{ => it}/mediated_admission.rs (99%) rename crates/batten/tests/{ => it}/mediated_verbs.rs (99%) rename crates/batten/tests/{ => it}/memories.rs (99%) rename crates/batten/tests/{ => it}/memory_injection.rs (99%) rename crates/batten/tests/{ => it}/mise_pin_agreement.rs (92%) rename crates/batten/tests/{ => it}/narrow_adoption.rs (95%) rename crates/batten/tests/{ => it}/perf_pair.rs (98%) rename crates/batten/tests/{ => it}/pinned_programs.rs (99%) rename crates/batten/tests/{ => it}/pipeline_shapes.rs (99%) rename crates/batten/tests/{ => it}/pointer_only.rs (99%) rename crates/batten/tests/{ => it}/policy_engine_count.rs (100%) rename crates/batten/tests/{ => it}/policy_input_narrowing.rs (100%) rename crates/batten/tests/{ => it}/policy_input_schema.rs (100%) rename crates/batten/tests/{ => it}/policy_presets.rs (99%) rename crates/batten/tests/{ => it}/policy_severity.rs (99%) rename crates/batten/tests/{ => it}/policy_test_suite.rs (99%) rename crates/batten/tests/{ => it}/policy_tree.rs (99%) rename crates/batten/tests/{ => it}/policy_whole_set.rs (100%) rename crates/batten/tests/{ => it}/pr_watch.rs (99%) rename crates/batten/tests/{ => it}/prebuilt_lint.rs (95%) rename crates/batten/tests/{ => it}/preset_segments.rs (98%) rename crates/batten/tests/{ => it}/primitives.rs (99%) rename crates/batten/tests/{ => it}/privileged_lane.rs (97%) rename crates/batten/tests/{ => it}/process_group.rs (99%) rename crates/batten/tests/{ => it}/prose_only.rs (92%) rename crates/batten/tests/{ => it}/prospective_facts.rs (99%) rename crates/batten/tests/{ => it}/provision.rs (99%) rename crates/batten/tests/{ => it}/ratchet.rs (99%) rename crates/batten/tests/{ => it}/ready.rs (96%) rename crates/batten/tests/{ => it}/reference_coverage.rs (89%) rename crates/batten/tests/{ => it}/remedy_authorship.rs (99%) rename crates/batten/tests/{ => it}/retirement_doctrine.rs (99%) rename crates/batten/tests/{ => it}/review_answered.rs (96%) rename crates/batten/tests/{ => it}/rule_cost_census.rs (100%) rename crates/batten/tests/{ => it}/rules_builtin_claims.rs (98%) rename crates/batten/tests/{ => it}/rules_drift.rs (99%) rename crates/batten/tests/{ => it}/run_shape.rs (96%) rename crates/batten/tests/{ => it}/run_shape_guard_door.rs (99%) rename crates/batten/tests/{ => it}/runner_verdict.rs (100%) rename crates/batten/tests/{ => it}/scanner_taxonomy.rs (99%) rename crates/batten/tests/{ => it}/secrets_kind.rs (99%) rename crates/batten/tests/{ => it}/semver_gate.rs (94%) rename crates/batten/tests/{ => it}/shell_retirement.rs (99%) rename crates/batten/tests/{ => it}/shell_write_advisory.rs (99%) rename crates/batten/tests/{ => it}/sinks.rs (99%) rename crates/batten/tests/{ => it}/skill_contract.rs (93%) rename crates/batten/tests/{ => it}/sleep_ban.rs (99%) rename crates/batten/tests/{ => it}/snapshots.rs (99%) rename crates/batten/tests/{snapshots/snapshots__golden_exit_code_table.snap => it/snapshots/it__snapshots__golden_exit_code_table.snap} (86%) rename crates/batten/tests/{snapshots/snapshots__golden_json_schema.snap => it/snapshots/it__snapshots__golden_json_schema.snap} (99%) rename crates/batten/tests/{snapshots/snapshots__json_output_is_frozen.snap => it/snapshots/it__snapshots__json_output_is_frozen.snap} (93%) rename crates/batten/tests/{snapshots/snapshots__pointer_output_is_frozen.snap => it/snapshots/it__snapshots__pointer_output_is_frozen.snap} (61%) rename crates/batten/tests/{ => it}/spawn_ceilings.rs (92%) rename crates/batten/tests/{ => it}/spawn_census.rs (99%) rename crates/batten/tests/{ => it}/staged_facts.rs (99%) rename crates/batten/tests/{ => it}/stop_posture.rs (95%) rename crates/batten/tests/{ => it}/submodule.rs (99%) rename crates/batten/tests/{ => it}/suite_subjects.rs (99%) rename crates/batten/tests/{ => it}/surface.rs (95%) rename crates/batten/tests/{ => it}/symbols.rs (100%) create mode 100644 crates/batten/tests/it/target_consolidation.rs rename crates/batten/tests/{ => it}/target_prune.rs (96%) rename crates/batten/tests/{ => it}/task_prose.rs (99%) rename crates/batten/tests/{ => it}/task_receipt.rs (99%) create mode 100644 crates/batten/tests/it/test_targets.rs rename crates/batten/tests/{ => it}/todo_promotion.rs (99%) rename crates/batten/tests/{ => it}/tool_selector.rs (99%) rename crates/batten/tests/{ => it}/tool_verdict_facts.rs (99%) rename crates/batten/tests/{ => it}/use_graph.rs (100%) rename crates/batten/tests/{ => it}/verdict_registry.rs (99%) rename crates/batten/tests/{ => it}/waivers.rs (93%) rename crates/batten/tests/{ => it}/walker.rs (99%) rename crates/batten/tests/{ => it}/wiring_reclaim.rs (99%) rename crates/batten/tests/{ => it}/zero_config.rs (99%) create mode 100644 policy/test-targets.rego diff --git a/.claude/rules/policy-modules.md b/.claude/rules/policy-modules.md index 860fa71b8..5f9144bb0 100644 --- a/.claude/rules/policy-modules.md +++ b/.claude/rules/policy-modules.md @@ -102,7 +102,7 @@ than merely necessary: a preset ships everywhere, so its pattern could not name consumer even if you wanted it to. **The load-time tier cannot see this** — `policy test` reported 330 passed over -the dead version. `crates/batten/tests/policy_presets.rs` is what catches it, +the dead version. `crates/batten/tests/it/policy_presets.rs` is what catches it, because it runs a preset's suite the way a consumer gets it. Give your own compiled tier the same empty vocabulary (`patterns: &[]`) for the same reason: a harness that declares the ids is supplying input no consumer supplies, and its diff --git a/.claude/rules/rust.md b/.claude/rules/rust.md index b1621f336..8deee90f7 100644 --- a/.claude/rules/rust.md +++ b/.claude/rules/rust.md @@ -21,7 +21,7 @@ These load when you touch Rust; they do not need to be in context otherwise. condition asserts its own premise before its conclusion; `tests/primitives.rs`'s `every_permission_drop_asserts_its_own_premise` is the gate (CLOUD-249). Prefer end-to-end tests over the - compiled binary (`crates/batten/tests/cli.rs`) for anything a consumer depends + compiled binary (`crates/batten/tests/it/cli.rs`) for anything a consumer depends on — exit codes, output shape, flag handling. - Branch on the named `ExitCode` variants in `crates/batten/src/exit.rs`, never integer literals. One table, no per-verb exception: `2` is the policy verdict @@ -153,7 +153,7 @@ arm has to reach 256 before it can see anything at all. invocation series above and this one share a unit and nothing else, so a reader plotting one stamp would put a 256-document sweep arm beside a `--help` invocation and read the gap as a step change. `perf-record` takes the stamp from -`BENCH_METRIC`, and `crates/batten/tests/acquisition_metric.rs` asserts the task +`BENCH_METRIC`, and `crates/batten/tests/it/acquisition_metric.rs` asserts the task sets it rather than trusting that it does. **"Because nothing measured asks otherwise" is the literal wording, and it is the diff --git a/.claude/rules/scanning.md b/.claude/rules/scanning.md index e6a291ccb..e088d025e 100644 --- a/.claude/rules/scanning.md +++ b/.claude/rules/scanning.md @@ -104,7 +104,7 @@ that gate is not evidence you picked the right class; it only means you did not substitute. The mechanism over this file is correspondingly thin and is named for what it -does. `crates/batten/tests/scanner_taxonomy.rs` asserts that this file still +does. `crates/batten/tests/it/scanner_taxonomy.rs` asserts that this file still names an instrument for each of the three question classes, still names the gate over the substitution axis, still keeps row one free of a bare product name, and still states the no-extension defect beside the recommendation — the same shape diff --git a/.claude/rules/toolchain.md b/.claude/rules/toolchain.md index 1ed81ae94..fab334298 100644 --- a/.claude/rules/toolchain.md +++ b/.claude/rules/toolchain.md @@ -100,7 +100,7 @@ ledger recorded both identically. Write `kind:verb` or `kind:mechanism` as a fie on the arm, beside the successor it qualifies: ``` -// carried: mise-tasks/ready-lint.sh crates/batten/src/ready.rs kind:verb crates/batten/tests/ready.rs +// carried: mise-tasks/ready-lint.sh crates/batten/src/ready.rs kind:verb crates/batten/tests/it/ready.rs ``` A `policy/*.rego` or preset successor needs no field — its path already decides @@ -348,7 +348,7 @@ mediating. Serena tool to use instead travels as each row's `redirect`, so a move still names `rename_memory` — the only route that rewrites `mem:` referrers. The table in `batten.toml` is the one authority; the corpus that used to live in - `tests/memory-guard.bats` is `crates/batten/tests/mediated_verbs.rs`. There is + `tests/memory-guard.bats` is `crates/batten/tests/it/mediated_verbs.rs`. There is no `BATTEN_MEMORY_GUARD_BYPASS`: a mediated deny takes the engine's own hatch, `BATTEN_HOOK_BYPASS` — or the row's own `bypass_env` where it declares one (CLOUD-437). @@ -581,7 +581,7 @@ call` with no `CLOUD-*` key **in that same paragraph** stops the lap. Two open is not nudged about one it already has. Silence is the default; a change-set is reported once, because reporting overwrites the snapshot. Pointer-only — paths and a count, never a byte of the file, asserted in - `crates/batten/tests/contract_drift.rs` — because a reminder carrying the new + `crates/batten/tests/it/contract_drift.rs` — because a reminder carrying the new text is a mirror and a mirror is cleared by reading the hook instead of the file. The shell task and `BATTEN_CONTRACT_DRIFT_BYPASS` are gone, and the engine fails open on everything it cannot read. **The advisory has no hatch at diff --git a/README.md b/README.md index 8975d6454..17952f05d 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ that **Batten never mints a `2` on it**: a `2` out of `exec` came from the wrapp command, and nothing can mistake it for a verdict. Renumbering an output match to `2` would spend that guarantee for a symmetry the table does not ask for. Decided on CLOUD-292, with the three rejected alternatives recorded there; `crates/batten/src/exit.rs` -carries the reasoning and `crates/batten/tests/extension_surfaces.rs` gates it. +carries the reasoning and `crates/batten/tests/it/extension_surfaces.rs` gates it. ### Whatever you reach for: output is a pointer, never the payload @@ -405,7 +405,7 @@ src/config.rs:41 no-hardcoded-token # the pointer # never the token ``` -`crates/batten/tests/pointer_only.rs` decides this rather than asserting it. It +`crates/batten/tests/it/pointer_only.rs` decides this rather than asserting it. It seeds a corpus in which every byte a check can read is a unique canary, runs **every leaf verb** of the command surface over it, and fails if a canary reaches either channel. A verb added to the surface must declare which side of the law it @@ -414,7 +414,7 @@ grows. ### Every example above is executed, not just written -`crates/batten/tests/extension_surfaces.rs` runs each command in this section +`crates/batten/tests/it/extension_surfaces.rs` runs each command in this section against the compiled binary and asserts the exit code it claims. A drifted example fails CI, so this documentation cannot rot into fiction. diff --git a/batten.toml b/batten.toml index d453e7257..4a81c6807 100644 --- a/batten.toml +++ b/batten.toml @@ -3296,7 +3296,7 @@ carried = "// carried:" subsumed = "// subsumed:" changed = "// changed:" withdrawn = "// withdrawn:" -declared_in = "crates/batten/tests/*.rs" +declared_in = "crates/batten/tests/**/*.rs" # The mirror direction: a disabled test is a deleted test that still counts, so # the guard is on the token RISING. @@ -3907,7 +3907,7 @@ no_fix_reason = "an IO crate reaching the evaluator is closed where it was enabl [[rule]] id = "no-key-leaves-the-schema-unannounced" kind = "command" -glob = "crates/batten/tests/config_deprecations.rs" +glob = "crates/batten/tests/it/config_deprecations.rs" check = "mise run test:config-deprecations" severity = "deny" scope = "tree" @@ -4056,7 +4056,7 @@ base = "origin/main" # withdrawal as one over a live subject and refuses it. A false refusal, in the # direction that blocks correct work. delta_sources = ["**"] -line_sources = ["mise-tasks/*.sh", "crates/batten/tests/*.rs"] +line_sources = ["mise-tasks/*.sh", "crates/batten/tests/**/*.rs"] module = "policy/shell-retirement.rego" severity = "deny" @@ -4112,6 +4112,22 @@ delta_sources = ["**"] module = "policy/filed-here.rego" severity = "deny" +# CLOUD-1210's ratchet on the cargo test-target count. +# +# `delta_sources` is the whole tree rather than `crates/batten/tests/**`, because +# the module decides on the path's SHAPE and a narrower delta would hand it only +# paths it was going to accept anyway — a gate reading a pre-filtered input cannot +# tell "nothing was added" from "the filter removed it". The depth test lives in +# the module, where a reader can check it against Cargo's autodiscovery rule. +[[rule]] +id = "test-targets" +kind = "policy" +scope = "tree" +base = "origin/main" +delta_sources = ["**"] +module = "policy/test-targets.rego" +severity = "deny" + [[rule]] id = "stop-posture" kind = "policy" @@ -5084,13 +5100,13 @@ keep = 2 mb = 7938 worst_mb = 7938 multiplier = 1 -measured = "2026-08-31" +measured = "2026-09-01" [prune.cold] mb = 18984 worst_mb = 18984 multiplier = 1 -measured = "2026-08-31" +measured = "2026-09-01" # THE BASIS EACH FLOOR WAS MEASURED AGAINST (CLOUD-1158), because `measured` is a # pointer to a basis and not the basis itself. @@ -5142,14 +5158,35 @@ measured = "2026-08-31" # The comparison runs in `batten target prune` (Surface::VerifyOnly), never at # config load — `Prune::validate` is on the path every mediated tool call pays. +# THE 2026-09-01 MOVE, AND IT BREAKS THE MODEL'S PREMISE RATHER THAN JUST ITS +# NUMBER (CLOUD-1210). Every reading above treats a tracked test FILE as a proxy +# for a linked STEM, which was exact while cargo autodiscovered one target per +# top-level `crates/batten/tests/*.rs`. Grouping them ends that: 144 targets +# became 2, so the file count and the artifact count are no longer one series. +# +# Measured on this container, cold and clean, before and after the grouping under +# the same adopted `split-debuginfo` profile: linked artifacts 147 -> 4, linked +# bytes 4.99 GB -> 234 MB, `target/debug` 7.76 GB -> 2.05 GB. A `verify` lap that +# CLOUD-1210 recorded consuming 25403 MB now consumes 20-1272 MB. +# +# So `count` moves to the live 152 with `measured`, as this block instructs, and +# THE FLOORS DELIBERATELY DO NOT MOVE. Both are now far above what the tree needs, +# and that is the safe direction on its own terms — the block already records that +# a floor too LOW fails silently while one too high only refuses laps. Re-deriving +# them downward needs the independent measurement this block names (a build from +# an empty `target` for cold, a minimal post-prune tree for warm) and is +# CLOUD-1158's, which CLOUD-1210's own §2 names as out of scope while predicting +# exactly this basis move. Refreshing the count without claiming a floor +# measurement I did not take is the honest half of the remedy. + [prune.warm.basis] -glob = "crates/batten/tests/*.rs" -count = 140 +glob = "crates/batten/tests/**/*.rs" +count = 152 tolerance = 10 [prune.cold.basis] -glob = "crates/batten/tests/*.rs" -count = 140 +glob = "crates/batten/tests/**/*.rs" +count = 152 tolerance = 10 # THE REGROWABLE ROOTS THE ESCALATION MAY DROP (CLOUD-1157), in the order it drops @@ -5951,6 +5988,23 @@ id = "R-OVERRIDE-PROSE-ONLY" kind = "override" precondition = "the prose IS the deliverable and cannot wait for the next change to these files" +[[verdict]] +id = "V-TEST-TARGET-ADDED" +gloss = "a new top-level crates/batten/tests/*.rs mints a second cargo test target" +class = """ +Cargo autodiscovers one test target per top-level `crates/batten/tests/*.rs`, and \ +rustc relinks the whole dependency closure into each one. Measured before \ +CLOUD-1210 grouped them: 144 targets, 147 linked artifacts, and roughly 36s of \ +the 48s a developer pays to rebuild after touching one `src/*.rs`. A file one \ +segment deeper — inside the group directory carrying `main.rs` — is a module and \ +mints no target, which is where a retirement's tier belongs. +""" + +[[verdict.route]] +id = "R-ADD-IT-TO-THE-GROUP" +kind = "command" +target = "git mv the file under crates/batten/tests/it/ and declare it in that group's main.rs" + [[verdict]] id = "V-FILED-UNREFINED" gloss = "a row this branch created was never groomed to Ready, so filing cost nothing" diff --git a/crates/batten/src/facts.rs b/crates/batten/src/facts.rs index 93aeb815e..f06b39abc 100644 --- a/crates/batten/src/facts.rs +++ b/crates/batten/src/facts.rs @@ -827,7 +827,7 @@ pub const INVOCATIONS: Class = Class::new(Cost::Read, Surface::Check); /// judgement. CLOUD-762's reversal condition says a bounded, nameable error count /// puts this tier here and an unbounded one sends it to `Cost::Effect` behind a /// delegated analyser. Over `crates/batten/src/**` the count is **four**, in two -/// classes, both re-exports — `crates/batten/tests/use_graph.rs` asserts it, so +/// classes, both re-exports — `crates/batten/tests/it/use_graph.rs` asserts it, so /// the number cannot rot into prose. /// /// **What a line predicate gets wrong, and in both directions.** `trust.rs` and diff --git a/crates/batten/src/hook.rs b/crates/batten/src/hook.rs index 75812d051..084ff822a 100644 --- a/crates/batten/src/hook.rs +++ b/crates/batten/src/hook.rs @@ -4840,7 +4840,7 @@ fn substitution_decision( /// which no first-class tool does at all. Denying the second told the caller "a /// first-class tool answers this directly", which is simply false, and a gate /// whose stated reason does not hold is a defect rather than a strict reading -/// (`crates/batten/tests/mediated_verbs.rs` caught it). +/// (`crates/batten/tests/it/mediated_verbs.rs` caught it). /// /// Mirrors the `requires_flag` qualifier the `[[verb]]` table already carries for /// the same distinction on `sed -i`, rather than inventing a second vocabulary diff --git a/crates/batten/src/lib.rs b/crates/batten/src/lib.rs index c44c96cec..5f12cab0d 100644 --- a/crates/batten/src/lib.rs +++ b/crates/batten/src/lib.rs @@ -3040,7 +3040,7 @@ impl SuiteReport { /// Run each registered module's own `test_` rules (CLOUD-835). /// -/// **The gap this closes.** `crates/batten/tests/policy_modules.rs` exercises +/// **The gap this closes.** `crates/batten/tests/it/policy_modules.rs` exercises /// the *evaluator*; nothing exercises a *module*. That is a blocker rather than /// a nicety because the retirement campaign has to move 1,570 of 2,485 bats /// cases onto policy rows, and CLOUD-202 measured the trap that makes an diff --git a/crates/batten/src/outputs.rs b/crates/batten/src/outputs.rs index ece52716d..f79c42981 100644 --- a/crates/batten/src/outputs.rs +++ b/crates/batten/src/outputs.rs @@ -50,7 +50,7 @@ //! a claim about **every** check. An emitter that leaked would have been caught //! here only if somebody thought to write the case here. //! -//! `crates/batten/tests/pointer_only.rs` decides it instead, at the process +//! `crates/batten/tests/it/pointer_only.rs` decides it instead, at the process //! boundary every emitter converges on: a corpus in which every byte a check can //! read is a canary, crossed with a census over the whole verb surface. A wrapped //! command's output is the likeliest place in this engine for a secret to appear, diff --git a/crates/batten/src/perf.rs b/crates/batten/src/perf.rs index 45ef88017..134bd0a25 100644 --- a/crates/batten/src/perf.rs +++ b/crates/batten/src/perf.rs @@ -1052,7 +1052,7 @@ impl std::fmt::Display for Sweep { /// compile and an evaluation beside each read, so the curve would price four /// things and get reported as one. Holding everything but the declared path count /// fixed is what leaves acquisition as the only term that moves. -/// `crates/batten/tests/document_read_count.rs::one_row_declaring_n_paths_acquires_n_documents` +/// `crates/batten/tests/it/document_read_count.rs::one_row_declaring_n_paths_acquires_n_documents` /// pins that the engine really does acquire once per declared path under exactly /// this shape, because a sweep over a variable the engine ignores would still /// draw a tidy curve. @@ -1678,7 +1678,7 @@ mod tests { } // The two cases over what `sweep_fixture` WRITES live in - // `crates/batten/tests/perf_acquire.rs` rather than here, and the reason is + // `crates/batten/tests/it/perf_acquire.rs` rather than here, and the reason is // the workspace lint rather than a preference: reading a file back is a // `Result`, and no module under `src/` waives `unwrap_used`. That builder is // public for exactly this, so the assertion loses nothing by moving. diff --git a/crates/batten/src/policy.rs b/crates/batten/src/policy.rs index 8102b5800..6d45d5442 100644 --- a/crates/batten/src/policy.rs +++ b/crates/batten/src/policy.rs @@ -44,7 +44,7 @@ //! (CLOUD-589's class, on the highest-consequence claim in the crate). Both //! halves are real now, and they answer different questions: //! -//! * `no_evaluator_feature_admits_io` in `crates/batten/tests/policy_modules.rs` +//! * `no_evaluator_feature_admits_io` in `crates/batten/tests/it/policy_modules.rs` //! is the BEHAVIOURAL half: it hands [`deny`] a module invoking `http.send` //! and asserts it does not answer. That asks *can a module reach the network* //! rather than testing a string in a manifest, so it stays true when the diff --git a/crates/batten/src/ready.rs b/crates/batten/src/ready.rs index c76883c54..b44613137 100644 --- a/crates/batten/src/ready.rs +++ b/crates/batten/src/ready.rs @@ -510,7 +510,7 @@ fn keys_in(grammar: &Grammar, text: &str) -> Vec { /// row cites nothing* becomes *could not look* — which is the very distinction /// `zero-is-a-count` exists on that column to preserve. Found by running this /// authority and `mise-tasks/ready-lint.sh` over one corpus -/// (`crates/batten/tests/authority_replay.rs`), which is what a replay is for and +/// (`crates/batten/tests/it/authority_replay.rs`), which is what a replay is for and /// what neither producer's own suite could see. fn emit_keys(grammar: &Grammar, label: &str, text: &str) -> String { format!("{label} {}", keys_in(grammar, text).join(" ")) diff --git a/crates/batten/src/rules.rs b/crates/batten/src/rules.rs index 1e5c447c3..aa94a65b8 100644 --- a/crates/batten/src/rules.rs +++ b/crates/batten/src/rules.rs @@ -7617,7 +7617,7 @@ fn policy_rule( // the last `mise-tasks/*.sh` and writes no Rust successor leaves both line // globs matching nothing, so the gate refusing an unmapped deletion is the // gate the deletion switches off. Measured on the fixtures in - // `crates/batten/tests/shell_retirement.rs`. + // `crates/batten/tests/it/shell_retirement.rs`. // // A resolved delta is what counts, not a non-empty one: an empty delta is // the row having looked and found nothing changed, which IS establishing @@ -10508,7 +10508,12 @@ mod tests { subsumed: "// subsumed:".to_owned(), changed: "// changed:".to_owned(), withdrawn: None, - declared_in: "crates/batten/tests/*.rs".to_owned(), + // `**/*.rs`, matching the committed row. The engine's globs use + // `literal_separator(true)`, so `*` stops at a `/` — and CLOUD-1210 + // moved every test file one segment deeper into `tests/it/`, which + // left the old spelling selecting NOTHING and this calibration + // reporting zero arms for every case it is supposed to find. + declared_in: "crates/batten/tests/**/*.rs".to_owned(), }; let files = crate_paths(); let claimed = claimed_cases(root, &conserves, &files); diff --git a/crates/batten/src/taskset.rs b/crates/batten/src/taskset.rs index d5034e6dd..e2740e620 100644 --- a/crates/batten/src/taskset.rs +++ b/crates/batten/src/taskset.rs @@ -116,7 +116,7 @@ fn record_path(root: &Path) -> Option { /// took a correction to state honestly: the key is recomputed here, so the /// manifest's bytes are read — what does not happen is a parse, a runner /// invocation, a binary probe or a tree walk. -/// `crates/batten/tests/task_receipt.rs`'s +/// `crates/batten/tests/it/task_receipt.rs`'s /// `the_mediated_call_digests_the_manifest_and_never_parses_it` is what /// discriminates the two: it records over a manifest that is not valid TOML, so a /// read that parsed would answer could-not-look and a read that digests answers. diff --git a/crates/batten/src/uses.rs b/crates/batten/src/uses.rs index aa37686d0..4132ef66f 100644 --- a/crates/batten/src/uses.rs +++ b/crates/batten/src/uses.rs @@ -19,7 +19,7 @@ //! **The CLASSES are the measurement; the site count is not** (CLOUD-1121). It //! was four when this was written and the phantom half grows with every module //! that imports `crate::Result` — three did in one change, describing nothing -//! that had changed about the tier. `crates/batten/tests/use_graph.rs` asserts +//! that had changed about the tier. `crates/batten/tests/it/use_graph.rs` asserts //! the classes and the root NAME behind each, and a count in this paragraph would //! be the prose-goes-stale failure the suite exists to replace. //! diff --git a/crates/batten/tests/acceptance_corpus.rs b/crates/batten/tests/it/acceptance_corpus.rs similarity index 99% rename from crates/batten/tests/acceptance_corpus.rs rename to crates/batten/tests/it/acceptance_corpus.rs index c42589a61..994d099f7 100644 --- a/crates/batten/tests/acceptance_corpus.rs +++ b/crates/batten/tests/it/acceptance_corpus.rs @@ -27,7 +27,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; @@ -163,8 +163,10 @@ const DISPOSITIONS: &[Disposition] = &[ /// workspace-relative path, and the test process's working directory is not the /// workspace root. fn this_file() -> String { - fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/acceptance_corpus.rs")) - .expect("read this suite's own source") + fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/it/acceptance_corpus.rs"), + ) + .expect("read this suite's own source") } /// The committed fixture ruleset and its tree. diff --git a/crates/batten/tests/acquisition_metric.rs b/crates/batten/tests/it/acquisition_metric.rs similarity index 99% rename from crates/batten/tests/acquisition_metric.rs rename to crates/batten/tests/it/acquisition_metric.rs index 6b4883f30..dd1f54994 100644 --- a/crates/batten/tests/acquisition_metric.rs +++ b/crates/batten/tests/it/acquisition_metric.rs @@ -37,7 +37,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; /// The default `perf-record` falls back to, and the one value this task must not /// carry. Spelled here rather than read out of the shell, because the point is diff --git a/crates/batten/tests/acquisition_sweep.rs b/crates/batten/tests/it/acquisition_sweep.rs similarity index 99% rename from crates/batten/tests/acquisition_sweep.rs rename to crates/batten/tests/it/acquisition_sweep.rs index b78e9a028..67cbd5746 100644 --- a/crates/batten/tests/acquisition_sweep.rs +++ b/crates/batten/tests/it/acquisition_sweep.rs @@ -53,7 +53,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use common::{Fixture, run, stderr, stdout}; diff --git a/crates/batten/tests/admission.rs b/crates/batten/tests/it/admission.rs similarity index 99% rename from crates/batten/tests/admission.rs rename to crates/batten/tests/it/admission.rs index 7e8d2bfe0..43f4f834f 100644 --- a/crates/batten/tests/admission.rs +++ b/crates/batten/tests/it/admission.rs @@ -22,7 +22,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/advisory_drain.rs b/crates/batten/tests/it/advisory_drain.rs similarity index 99% rename from crates/batten/tests/advisory_drain.rs rename to crates/batten/tests/it/advisory_drain.rs index cf888b2cf..702e3b13f 100644 --- a/crates/batten/tests/advisory_drain.rs +++ b/crates/batten/tests/it/advisory_drain.rs @@ -19,7 +19,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fmt::Write as _; use std::io::Write as _; diff --git a/crates/batten/tests/agent_facts.rs b/crates/batten/tests/it/agent_facts.rs similarity index 100% rename from crates/batten/tests/agent_facts.rs rename to crates/batten/tests/it/agent_facts.rs diff --git a/crates/batten/tests/ambient_authority.rs b/crates/batten/tests/it/ambient_authority.rs similarity index 99% rename from crates/batten/tests/ambient_authority.rs rename to crates/batten/tests/it/ambient_authority.rs index c1b3e4bf0..80e23fa02 100644 --- a/crates/batten/tests/ambient_authority.rs +++ b/crates/batten/tests/it/ambient_authority.rs @@ -16,7 +16,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::PathBuf; diff --git a/crates/batten/tests/attribution.rs b/crates/batten/tests/it/attribution.rs similarity index 99% rename from crates/batten/tests/attribution.rs rename to crates/batten/tests/it/attribution.rs index 104fd0b12..70b89682a 100644 --- a/crates/batten/tests/attribution.rs +++ b/crates/batten/tests/it/attribution.rs @@ -15,7 +15,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/authority_replay.rs b/crates/batten/tests/it/authority_replay.rs similarity index 99% rename from crates/batten/tests/authority_replay.rs rename to crates/batten/tests/it/authority_replay.rs index a0f4f0565..292cf48d8 100644 --- a/crates/batten/tests/authority_replay.rs +++ b/crates/batten/tests/it/authority_replay.rs @@ -2,7 +2,7 @@ //! //! CLOUD-909's obligation, applied to the one thing CLOUD-1100 actually changed //! about how a verdict is reached. The GRAMMAR's fidelity is already settled — -//! `crates/batten/tests/ready.rs` carries all 82 cases of `tests/ready-lint.bats` +//! `crates/batten/tests/it/ready.rs` carries all 82 cases of `tests/ready-lint.bats` //! onto the compiled binary, and this file neither adds to that mapping nor //! rewrites it. What is new is a **presentation**: three `[[recorder]]` columns //! that used to spawn `mise-tasks/ready-lint.sh` now ask diff --git a/crates/batten/tests/baseline.rs b/crates/batten/tests/it/baseline.rs similarity index 99% rename from crates/batten/tests/baseline.rs rename to crates/batten/tests/it/baseline.rs index 5e3138186..6bc884847 100644 --- a/crates/batten/tests/baseline.rs +++ b/crates/batten/tests/it/baseline.rs @@ -29,7 +29,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/bats_invocation.rs b/crates/batten/tests/it/bats_invocation.rs similarity index 96% rename from crates/batten/tests/bats_invocation.rs rename to crates/batten/tests/it/bats_invocation.rs index 23611fac1..e88f97a97 100644 --- a/crates/batten/tests/bats_invocation.rs +++ b/crates/batten/tests/it/bats_invocation.rs @@ -32,7 +32,7 @@ // CLOUD-908's case arms below by construction: a case arm's first field after the // marker is a QUOTED case name, and a file arm's is a path. // -// carried: tests/test-bats-parallel.bats policy/bats-invocation.rego crates/batten/tests/bats_invocation.rs +// carried: tests/test-bats-parallel.bats policy/bats-invocation.rego crates/batten/tests/it/bats_invocation.rs // CLOUD-908's case arms: every `@test` the retired suite declared, and where its // predicate now lives. Twelve carried and one changed — the change is stated @@ -51,12 +51,12 @@ // carried: "the parallel backend is named explicitly rather than left to bats' default probe" policy/bats-invocation.rego // carried: "the parallel backend is a pinned tool, so the fast path cannot depend on the host" policy/bats-invocation.rego // carried: "CI installs the parallel backend — an absent rush is a missing TOOL, not a slow suite" policy/bats-invocation.rego -// changed: "the test:bats invocation was found at all — this suite is not passing vacuously" crates/batten/tests/bats_invocation.rs the suite asserted its own subject exists, which a module cannot: a tree with no `test:bats` task is not-applicable rather than in violation, or the row fires on every fixture that copies this config (`command-task-defined` measured seven such findings). The property survives as `this_repository_is_clean_today` plus `a_tree_with_no_such_task_is_not_judged`, which together say the same thing about THIS tree without claiming it about every tree +// changed: "the test:bats invocation was found at all — this suite is not passing vacuously" crates/batten/tests/it/bats_invocation.rs the suite asserted its own subject exists, which a module cannot: a tree with no `test:bats` task is not-applicable rather than in violation, or the row fires on every fixture that copies this config (`command-task-defined` measured seven such findings). The property survives as `this_repository_is_clean_today` plus `a_tree_with_no_such_task_is_not_judged`, which together say the same thing about THIS tree without claiming it about every tree // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/board_receipts.rs b/crates/batten/tests/it/board_receipts.rs similarity index 89% rename from crates/batten/tests/board_receipts.rs rename to crates/batten/tests/it/board_receipts.rs index dcd645b84..0f7c54377 100644 --- a/crates/batten/tests/board_receipts.rs +++ b/crates/batten/tests/it/board_receipts.rs @@ -21,16 +21,16 @@ //! `tests/issue-search-guard.bats`, ten cases, every one placed. An unmapped case //! is a coverage loss wearing a retirement's clothes. //! -// carried: "creating an issue with no receipt is denied, and the denial names the fix" crates/batten/tests/board_receipts.rs -// carried: "all three live connector spellings are gated identically" crates/batten/tests/board_receipts.rs -// carried: "a tool that does not create an issue is never gated" crates/batten/tests/board_receipts.rs +// carried: "creating an issue with no receipt is denied, and the denial names the fix" crates/batten/tests/it/board_receipts.rs +// carried: "all three live connector spellings are gated identically" crates/batten/tests/it/board_receipts.rs +// carried: "a tool that does not create an issue is never gated" crates/batten/tests/it/board_receipts.rs //! //! SUBSUMED — the plumbing became the engine's, which is what a migration should //! produce. Each names the general property that now covers it. //! -// subsumed: "an unreadable or nameless payload fails open" crates/batten/tests/cli.rs +// subsumed: "an unreadable or nameless payload fails open" crates/batten/tests/it/cli.rs // subsumed: "outside a git repository the guard fails open rather than blocking every filing" crates/batten/src/receipt.rs kind:mechanism -// subsumed: "the emitted denial is the hook shape, and it parses" crates/batten/tests/advisory_drain.rs +// subsumed: "the emitted denial is the hook shape, and it parses" crates/batten/tests/it/advisory_drain.rs //! //! CHANGED — behaviour that diverges deliberately. //! @@ -58,10 +58,10 @@ //! base line the task really writes. What the bats fixture cannot express is the //! precondition, which is the same shape as row 2's mtime arms below. //! -// changed: "issue-search-guard.bats::creating an issue with a receipt is allowed" crates/batten/tests/board_receipts.rs the engine additionally requires the receipt to record the `origin/main` it was taken against (CLOUD-516); a bare-`git init` fixture has no such ref, so the receipt says `base -` and reads as unproven. Carried in `filing_without_a_search_is_refused_and_with_one_is_allowed`, which mints the base line -// changed: "issue-search-guard.bats::the CLOUD-504 over CLOUD-499 filing is refused, and allowed after the search" crates/batten/tests/board_receipts.rs same cause, same successor shape: the deny half replays, and the allow half needs a base line the fixture's repository cannot produce. Carried in `the_measured_duplicate_is_refused_and_then_allowed` -// changed: "issue-search-guard.bats::the bypass is honoured" crates/batten/tests/guardrail_bypass.rs BATTEN_ISSUE_SEARCH_BYPASS is gone; a mediated deny takes the engine's own hatch, which is the same consolidation CLOUD-442 and CLOUD-444 made when memory-guard and claim-guard retired -// changed: "issue-search-guard.bats::updating an existing issue is never gated, receipt or not" crates/batten/tests/board_receipts.rs the arm was true of row 1 alone and is now false of the config: row 2 below gates exactly that call on a RECENT read (CLOUD-508). The two rows are complements over one tool — `when_absent` and `when_present` on the same `input-id` — so this case's allow survives only where row 2 cannot key the subject, and `an_update_with_no_receipt_is_refused` is where the new answer is asserted +// changed: "issue-search-guard.bats::creating an issue with a receipt is allowed" crates/batten/tests/it/board_receipts.rs the engine additionally requires the receipt to record the `origin/main` it was taken against (CLOUD-516); a bare-`git init` fixture has no such ref, so the receipt says `base -` and reads as unproven. Carried in `filing_without_a_search_is_refused_and_with_one_is_allowed`, which mints the base line +// changed: "issue-search-guard.bats::the CLOUD-504 over CLOUD-499 filing is refused, and allowed after the search" crates/batten/tests/it/board_receipts.rs same cause, same successor shape: the deny half replays, and the allow half needs a base line the fixture's repository cannot produce. Carried in `the_measured_duplicate_is_refused_and_then_allowed` +// changed: "issue-search-guard.bats::the bypass is honoured" crates/batten/tests/it/guardrail_bypass.rs BATTEN_ISSUE_SEARCH_BYPASS is gone; a mediated deny takes the engine's own hatch, which is the same consolidation CLOUD-442 and CLOUD-444 made when memory-guard and claim-guard retired +// changed: "issue-search-guard.bats::updating an existing issue is never gated, receipt or not" crates/batten/tests/it/board_receipts.rs the arm was true of row 1 alone and is now false of the config: row 2 below gates exactly that call on a RECENT read (CLOUD-508). The two rows are complements over one tool — `when_absent` and `when_present` on the same `input-id` — so this case's allow survives only where row 2 cannot key the subject, and `an_update_with_no_receipt_is_refused` is where the new answer is asserted //! //! ─── CLOUD-909's REPLAY, row 1 ─────────────────────────────────────────────── //! @@ -81,21 +81,21 @@ //! arms of one tool. A bare arm here would have let row 1's case borrow row 2's //! verdict in whichever direction `replay.sh` looked first. //! -// carried: "issue-read-guard.bats::an update with no receipt is denied, and the denial names the fix" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::an update from a fresh read is allowed" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::a fresh read of one issue does not authorise an update to a different one" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::creating an issue is never gated here, receipt or not" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::all three live connector spellings are gated identically" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::a tool that does not save an issue is never gated" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::an id that is not an issue key fails open rather than denying" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::the denial carries no payload content" crates/batten/tests/board_receipts.rs -// carried: "issue-read-guard.bats::a receipt minted from the declared field set alone authorises the update" crates/batten/tests/board_receipts.rs +// carried: "issue-read-guard.bats::an update with no receipt is denied, and the denial names the fix" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::an update from a fresh read is allowed" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::a fresh read of one issue does not authorise an update to a different one" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::creating an issue is never gated here, receipt or not" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::all three live connector spellings are gated identically" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::a tool that does not save an issue is never gated" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::an id that is not an issue key fails open rather than denying" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::the denial carries no payload content" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-guard.bats::a receipt minted from the declared field set alone authorises the update" crates/batten/tests/it/board_receipts.rs //! //! SUBSUMED — the plumbing became the engine's, and one seam became the surviving //! half's own suite. //! -// subsumed: "issue-read-guard.bats::an unreadable or nameless payload fails open" crates/batten/tests/cli.rs -// subsumed: "issue-read-guard.bats::a payload too thin to mint a receipt leaves the update denied" crates/batten/tests/board_receipts.rs +// subsumed: "issue-read-guard.bats::an unreadable or nameless payload fails open" crates/batten/tests/it/cli.rs +// subsumed: "issue-read-guard.bats::a payload too thin to mint a receipt leaves the update denied" crates/batten/tests/it/board_receipts.rs //! //! CHANGED — and four of the five here share ONE cause, which is worth stating //! once (the fifth is the bypass, and it is row 1's consolidation again): the @@ -108,11 +108,11 @@ //! engine measures. A `changed` arm over the mechanism is not licence to drop the //! property, which is the one way this ledger could be used to launder a loss. //! -// changed: "issue-read-guard.bats::an update from a read older than the bound is denied" crates/batten/tests/board_receipts.rs the age is the receipt file's mtime, not a parsed field, so the suite's field-3 arithmetic backdates nothing for the engine; the property is carried in `a_read_older_than_the_bound_is_refused`, which backdates the mtime -// changed: "issue-read-guard.bats::a malformed receipt fails open rather than denying" crates/batten/tests/board_receipts.rs the engine parses no field of the receipt, so there is no malformed state to fail open on — `named_validity` answers existence and `max_age` reads the mtime, which is a narrower reader than the one that could half-read a line -// changed: "issue-read-guard.bats::a receipt stamped in the future fails open rather than authorising" crates/batten/tests/board_receipts.rs same cause: a stamp is not read at all. A clock that moved shows up as a future mtime, which `older_than` reports as not-older and so still allows — the same direction, reached without parsing -// changed: "issue-read-guard.bats::the bound is configurable, and honoured in both directions" crates/batten/tests/board_receipts.rs BATTEN_ISSUE_READ_MAX_AGE is gone: the bound is `max_age` on the row (CLOUD-988), so it is configured where every other property of the row is and a reader finds it without knowing an env var's name. Per-call override is deliberately not carried — an agent that can widen the bound at the call it is being gated on is not gated -// changed: "issue-read-guard.bats::the bypass is honoured" crates/batten/tests/guardrail_bypass.rs BATTEN_ISSUE_READ_BYPASS is gone; a mediated deny takes the engine's own hatch, the same consolidation row 1 records one section up +// changed: "issue-read-guard.bats::an update from a read older than the bound is denied" crates/batten/tests/it/board_receipts.rs the age is the receipt file's mtime, not a parsed field, so the suite's field-3 arithmetic backdates nothing for the engine; the property is carried in `a_read_older_than_the_bound_is_refused`, which backdates the mtime +// changed: "issue-read-guard.bats::a malformed receipt fails open rather than denying" crates/batten/tests/it/board_receipts.rs the engine parses no field of the receipt, so there is no malformed state to fail open on — `named_validity` answers existence and `max_age` reads the mtime, which is a narrower reader than the one that could half-read a line +// changed: "issue-read-guard.bats::a receipt stamped in the future fails open rather than authorising" crates/batten/tests/it/board_receipts.rs same cause: a stamp is not read at all. A clock that moved shows up as a future mtime, which `older_than` reports as not-older and so still allows — the same direction, reached without parsing +// changed: "issue-read-guard.bats::the bound is configurable, and honoured in both directions" crates/batten/tests/it/board_receipts.rs BATTEN_ISSUE_READ_MAX_AGE is gone: the bound is `max_age` on the row (CLOUD-988), so it is configured where every other property of the row is and a reader finds it without knowing an env var's name. Per-call override is deliberately not carried — an agent that can widen the bound at the call it is being gated on is not gated +// changed: "issue-read-guard.bats::the bypass is honoured" crates/batten/tests/it/guardrail_bypass.rs BATTEN_ISSUE_READ_BYPASS is gone; a mediated deny takes the engine's own hatch, the same consolidation row 1 records one section up //! //! ─── CLOUD-909's REPLAY, row 2 ─────────────────────────────────────────────── //! @@ -132,37 +132,37 @@ //! about the RECORD carry across unchanged, and every case about the STDIN //! CONTRACT is `changed`, because there is no stdin to have a contract with. //! -// carried: "issue-read-check.bats::a get_issue payload mints a receipt keyed by the issue" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::the receipt records the revision seen and the time it was seen" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::the recorded time is when the read happened, so a receipt can actually age" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::the receipt records a body hash that tracks the body and nothing else" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::a payload with no description records no baseline, rather than a digest of nothing" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::the empty-body digest 8b13789 is never written for an absent description" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::an explicitly null description records no baseline either" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::the receipt records the column the read saw" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::a column with a space is one field, not two" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::a payload with no status records no column, rather than one that reads as open" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::an explicitly null status records no column either" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::the body baseline and the column arm do not depend on each other" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::a payload carrying only the declared field set is accepted" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::the receipt carries no title and no body" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::a second read replaces the first rather than appending" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::reads of different issues do not authorise each other" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::a payload missing updatedAt is refused rather than minting a receipt that names no revision" crates/batten/tests/board_receipts.rs -// carried: "issue-read-check.bats::an id that is not an issue key is refused rather than filed under a made-up name" crates/batten/tests/board_receipts.rs -// carried: "issue-search-check.bats::a list_issues payload mints a receipt naming the ids that were seen" crates/batten/tests/board_receipts.rs -// carried: "issue-search-check.bats::a search that returned nothing is still a search" crates/batten/tests/board_receipts.rs -// carried: "issue-search-check.bats::a payload that is not a search cannot look, and mints nothing" crates/batten/tests/board_receipts.rs -// carried: "issue-search-check.bats::a detached HEAD cannot look rather than minting an unkeyed receipt" crates/batten/tests/board_receipts.rs +// carried: "issue-read-check.bats::a get_issue payload mints a receipt keyed by the issue" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::the receipt records the revision seen and the time it was seen" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::the recorded time is when the read happened, so a receipt can actually age" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::the receipt records a body hash that tracks the body and nothing else" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::a payload with no description records no baseline, rather than a digest of nothing" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::the empty-body digest 8b13789 is never written for an absent description" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::an explicitly null description records no baseline either" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::the receipt records the column the read saw" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::a column with a space is one field, not two" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::a payload with no status records no column, rather than one that reads as open" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::an explicitly null status records no column either" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::the body baseline and the column arm do not depend on each other" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::a payload carrying only the declared field set is accepted" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::the receipt carries no title and no body" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::a second read replaces the first rather than appending" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::reads of different issues do not authorise each other" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::a payload missing updatedAt is refused rather than minting a receipt that names no revision" crates/batten/tests/it/board_receipts.rs +// carried: "issue-read-check.bats::an id that is not an issue key is refused rather than filed under a made-up name" crates/batten/tests/it/board_receipts.rs +// carried: "issue-search-check.bats::a list_issues payload mints a receipt naming the ids that were seen" crates/batten/tests/it/board_receipts.rs +// carried: "issue-search-check.bats::a search that returned nothing is still a search" crates/batten/tests/it/board_receipts.rs +// carried: "issue-search-check.bats::a payload that is not a search cannot look, and mints nothing" crates/batten/tests/it/board_receipts.rs +// carried: "issue-search-check.bats::a detached HEAD cannot look rather than minting an unkeyed receipt" crates/batten/tests/it/board_receipts.rs //! //! CHANGED — the three whose subject was the stdin contract, which no longer //! exists. Each names what asks the same question on the new input, so the //! property is relocated rather than dropped — the laundering this ledger exists //! to refuse. //! -// changed: "issue-read-check.bats::a single-element array is accepted, so a list payload of one composes" crates/batten/tests/board_receipts.rs there is no stdin to normalise: the input is the tool result, and the wrapper that actually arrives is the connector's content-block envelope. `a_content_block_envelope_mints_exactly_as_a_bare_payload_does` is the same property over the shape the host really sends, asserted as equality with the unwrapped mint -// changed: "issue-read-check.bats::stdin that is not a get_issue payload is exit 2, not a silent mint" crates/batten/tests/board_receipts.rs the mint has no exit code to give — it runs on an event no host offers a deny channel for — so "not a usable read" is answered by minting NOTHING instead. `a_failed_or_errored_or_empty_result_mints_nothing` and `a_write_response_does_not_mint_a_read_receipt` are the two halves: a result that says nothing, and one that says the right shape from the wrong tool -// changed: "issue-search-check.bats::the {issues: [...]} envelope is accepted as well as a bare array" crates/batten/tests/board_receipts.rs only one of the two is a real shape now. Measured against the live connector, a search answers `{issues: [...], hasNextPage, cursor}`; a bare array was the projection a caller piped by hand, and there is no caller. The `requires` path `issues[].id` is what pins the surviving shape +// changed: "issue-read-check.bats::a single-element array is accepted, so a list payload of one composes" crates/batten/tests/it/board_receipts.rs there is no stdin to normalise: the input is the tool result, and the wrapper that actually arrives is the connector's content-block envelope. `a_content_block_envelope_mints_exactly_as_a_bare_payload_does` is the same property over the shape the host really sends, asserted as equality with the unwrapped mint +// changed: "issue-read-check.bats::stdin that is not a get_issue payload is exit 2, not a silent mint" crates/batten/tests/it/board_receipts.rs the mint has no exit code to give — it runs on an event no host offers a deny channel for — so "not a usable read" is answered by minting NOTHING instead. `a_failed_or_errored_or_empty_result_mints_nothing` and `a_write_response_does_not_mint_a_read_receipt` are the two halves: a result that says nothing, and one that says the right shape from the wrong tool +// changed: "issue-search-check.bats::the {issues: [...]} envelope is accepted as well as a bare array" crates/batten/tests/it/board_receipts.rs only one of the two is a real shape now. Measured against the live connector, a search answers `{issues: [...], hasNextPage, cursor}`; a bare array was the projection a caller piped by hand, and there is no caller. The `requires` path `issues[].id` is what pins the surviving shape //! //! ─── CLOUD-908's MAPPING, row 3 ────────────────────────────────────────────── //! @@ -170,20 +170,20 @@ //! qualified throughout, for the reason row 2's block gives: this is the THIRD arm //! over one tool and it shares titles with both its siblings. //! -// carried: "board-move-guard.bats::a move to In Review with no adjudication is denied, and the denial names graph-check" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::an adjudication that judged OTHER issues does not authorise this one" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::every other column is somebody else's question and is never gated here" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::a save_issue that sets no state at all is not a move" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::the column is read case- and space-insensitively" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::creating an issue is never gated here, even with a state" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::all three live connector spellings are gated identically" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::a tool that does not save an issue is never gated" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::an id that is not an issue key fails open rather than denying" crates/batten/tests/board_receipts.rs -// carried: "board-move-guard.bats::the denial carries no payload content" crates/batten/tests/board_receipts.rs +// carried: "board-move-guard.bats::a move to In Review with no adjudication is denied, and the denial names graph-check" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::an adjudication that judged OTHER issues does not authorise this one" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::every other column is somebody else's question and is never gated here" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::a save_issue that sets no state at all is not a move" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::the column is read case- and space-insensitively" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::creating an issue is never gated here, even with a state" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::all three live connector spellings are gated identically" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::a tool that does not save an issue is never gated" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::an id that is not an issue key fails open rather than denying" crates/batten/tests/it/board_receipts.rs +// carried: "board-move-guard.bats::the denial carries no payload content" crates/batten/tests/it/board_receipts.rs //! //! SUBSUMED — the plumbing became the engine's. //! -// subsumed: "board-move-guard.bats::an unreadable or nameless payload fails open" crates/batten/tests/cli.rs +// subsumed: "board-move-guard.bats::an unreadable or nameless payload fails open" crates/batten/tests/it/cli.rs //! //! CHANGED — and five of the six are ONE cause, which the receipt's new shape //! explains once: `graph-check` writes a file per judged id now, where it appended @@ -193,14 +193,14 @@ //! property those cases asserted is carried below; what is gone is the mechanism //! they were written against. //! -// changed: "board-move-guard.bats::a move covered by a fresh adjudication is allowed" crates/batten/tests/board_receipts.rs the ALLOW half cannot be expressed on the retiring fixture: the base rev's graph-check mints the aggregate `board-move` file, and the engine reads `board-move.`, so a fixture carrying the old shape is a fixture carrying no receipt this row can see. Carried in `a_move_with_no_adjudication_is_refused`, whose second half mints the shape the surviving task now writes +// changed: "board-move-guard.bats::a move covered by a fresh adjudication is allowed" crates/batten/tests/it/board_receipts.rs the ALLOW half cannot be expressed on the retiring fixture: the base rev's graph-check mints the aggregate `board-move` file, and the engine reads `board-move.`, so a fixture carrying the old shape is a fixture carrying no receipt this row can see. Carried in `a_move_with_no_adjudication_is_refused`, whose second half mints the shape the surviving task now writes // changed: "board-move-guard.bats::graph-check mints the receipt this guard reads, and only on a coherent board" tests/graph-check.bats same cause on the producing side: this case asserted the two ends agreed on ONE file, and they agree on a file per id now. The seam is asserted where the surviving half lives — `a coherent board records one receipt per id it judged`, which also asserts the aggregate is not left behind -// changed: "board-move-guard.bats::an adjudication older than the bound is denied, and the bound is configurable" crates/batten/tests/board_receipts.rs the deny half is carried in `an_adjudication_past_the_bound_is_refused`, which backdates the receipt's mtime; BATTEN_BOARD_MOVE_MAX_AGE is gone and the bound is `max_age` on the row (CLOUD-988), configured where every other property of the row is. Per-call override is deliberately not carried — an agent that can widen the bound at the call it is being gated on is not gated -// changed: "board-move-guard.bats::a stale line naming this issue plus a fresh line naming others is not an authorisation" crates/batten/tests/board_receipts.rs there are no lines: one file per id means a fresh adjudication of ANOTHER id cannot appear in this id's receipt at all, so the combination the case defends against is unconstructible rather than defended -// changed: "board-move-guard.bats::an id is matched whole, so a prefix does not authorise a longer key" crates/batten/tests/board_receipts.rs the `\b$key\b` anchoring that kept CLOUD-48 from reading as CLOUD-480 is structural now — a filename is matched whole by the filesystem. Carried anyway in `an_adjudication_of_one_row_does_not_authorise_another`, whose second subject is a prefix of the first -// changed: "board-move-guard.bats::a malformed receipt line is not an authorisation" crates/batten/tests/board_receipts.rs the engine parses no field of the receipt, so there is no malformed state to judge: `named_validity` answers existence and `max_age` reads the mtime -// changed: "board-move-guard.bats::a receipt stamped in the future fails open rather than authorising" crates/batten/tests/board_receipts.rs same cause — a stamp is not read. A clock that moved shows as a future mtime, which `older_than` reports as not-older and so still allows: the same direction, reached without parsing -// changed: "board-move-guard.bats::the bypass is honoured" crates/batten/tests/guardrail_bypass.rs BATTEN_BOARD_MOVE_BYPASS is gone; a mediated deny takes the engine's own hatch, the consolidation rows 1 and 2 record above +// changed: "board-move-guard.bats::an adjudication older than the bound is denied, and the bound is configurable" crates/batten/tests/it/board_receipts.rs the deny half is carried in `an_adjudication_past_the_bound_is_refused`, which backdates the receipt's mtime; BATTEN_BOARD_MOVE_MAX_AGE is gone and the bound is `max_age` on the row (CLOUD-988), configured where every other property of the row is. Per-call override is deliberately not carried — an agent that can widen the bound at the call it is being gated on is not gated +// changed: "board-move-guard.bats::a stale line naming this issue plus a fresh line naming others is not an authorisation" crates/batten/tests/it/board_receipts.rs there are no lines: one file per id means a fresh adjudication of ANOTHER id cannot appear in this id's receipt at all, so the combination the case defends against is unconstructible rather than defended +// changed: "board-move-guard.bats::an id is matched whole, so a prefix does not authorise a longer key" crates/batten/tests/it/board_receipts.rs the `\b$key\b` anchoring that kept CLOUD-48 from reading as CLOUD-480 is structural now — a filename is matched whole by the filesystem. Carried anyway in `an_adjudication_of_one_row_does_not_authorise_another`, whose second subject is a prefix of the first +// changed: "board-move-guard.bats::a malformed receipt line is not an authorisation" crates/batten/tests/it/board_receipts.rs the engine parses no field of the receipt, so there is no malformed state to judge: `named_validity` answers existence and `max_age` reads the mtime +// changed: "board-move-guard.bats::a receipt stamped in the future fails open rather than authorising" crates/batten/tests/it/board_receipts.rs same cause — a stamp is not read. A clock that moved shows as a future mtime, which `older_than` reports as not-older and so still allows: the same direction, reached without parsing +// changed: "board-move-guard.bats::the bypass is honoured" crates/batten/tests/it/guardrail_bypass.rs BATTEN_BOARD_MOVE_BYPASS is gone; a mediated deny takes the engine's own hatch, the consolidation rows 1 and 2 record above //! //! THE SURVIVING SUITE'S OWN RENAMES OWE ARMS TOO, and that is the column working //! rather than a nuisance: `graph-check.bats` keeps testing the minting side, but @@ -219,7 +219,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; @@ -233,7 +233,7 @@ use common::{Fixture, run_with_stdin, stderr}; /// the authority: a row edited in `batten.toml` is exercised by the next run /// rather than by whoever remembers to update a duplicate. fn repo(name: &str) -> PathBuf { - let staged = Fixture::new(name).config(include_str!("../../../batten.toml")); + let staged = Fixture::new(name).config(include_str!("../../../../batten.toml")); // The modules the committed config names, copied by ENUMERATION rather than by // name, and staged BEFORE the commit so they are tracked like the config is. // diff --git a/crates/batten/tests/board_record.rs b/crates/batten/tests/it/board_record.rs similarity index 93% rename from crates/batten/tests/board_record.rs rename to crates/batten/tests/it/board_record.rs index 468693f82..53e89c56f 100644 --- a/crates/batten/tests/board_record.rs +++ b/crates/batten/tests/it/board_record.rs @@ -25,39 +25,39 @@ //! surface under the gate's own definition (`policy/*.rego` OR //! `crates/batten/src/*.rs`) for exactly that case. //! -// carried: mise-tasks/board-write-record.sh crates/batten/src/recorder.rs kind:mechanism crates/batten/tests/board_record.rs -// carried: tests/board-write-record.bats crates/batten/src/recorder.rs kind:mechanism crates/batten/tests/board_record.rs +// carried: mise-tasks/board-write-record.sh crates/batten/src/recorder.rs kind:mechanism crates/batten/tests/it/board_record.rs +// carried: tests/board-write-record.bats crates/batten/src/recorder.rs kind:mechanism crates/batten/tests/it/board_record.rs //! //! # RETIREMENT LEDGER — `tests/board-write-record.bats`, 36 cases //! //! CARRIED — the property survives, proved here against the engine. //! -// carried: "a created row is recorded with its id, updatedAt and a green verdict" crates/batten/tests/board_record.rs -// carried: "an unrefined row records a verdict of unready rather than being refused" crates/batten/tests/board_record.rs -// carried: "a row whose body names a changed file records a non-zero overlap" crates/batten/tests/board_record.rs -// carried: "A PATH NAMED ONLY OUTSIDE §1 IS IN THE NAMED COLUMN AND NOT THE §1 ONE" crates/batten/tests/board_record.rs -// carried: "a path named IN §1 reaches the §1 column, so a real claim is still visible" crates/batten/tests/board_record.rs -// carried: "a row naming nothing tracked records a zero" crates/batten/tests/board_record.rs -// carried: "updating an existing row is never recorded" crates/batten/tests/board_record.rs -// carried: "a groom of a row THIS branch filed is recorded" crates/batten/tests/board_record.rs -// carried: "a groom of a row this branch did NOT file is still skipped" crates/batten/tests/board_record.rs -// carried: "an id that merely PREFIXES a filed one does not count as filed here" crates/batten/tests/board_record.rs -// carried: "a groom whose §8 cites a blocker is unjudgeable, not unready" crates/batten/tests/board_record.rs -// carried: "a groom of a genuinely unready body still records unready" crates/batten/tests/board_record.rs -// carried: "a write records the rows its stored body cites" crates/batten/tests/board_record.rs -// carried: "a write passing exactly the rows it cites records zero" crates/batten/tests/board_record.rs -// carried: "zero and could-not-look are distinguishable in the record" crates/batten/tests/board_record.rs -// carried: "the row's own key is not counted as an edge to anywhere" crates/batten/tests/board_record.rs -// carried: "a write the producer never ran for records a dash, never a zero" crates/batten/tests/board_record.rs -// carried: "a comment records the issue key its input names, not the comment uuid" crates/batten/tests/board_record.rs -// carried: "REGRESSION: a comment row never records a uuid" crates/batten/tests/board_record.rs -// carried: "a reply, or a comment on a non-issue parent, records a dash rather than a guess" crates/batten/tests/board_record.rs -// carried: "all three live connector spellings are recorded identically" crates/batten/tests/board_record.rs -// carried: "a tool that does not write to the board is never recorded" crates/batten/tests/board_record.rs -// carried: "POINTER, NEVER PAYLOAD: no byte of the description reaches the record" crates/batten/tests/board_record.rs -// carried: "POINTER, NEVER PAYLOAD: the citing sentence does not reach the record" crates/batten/tests/board_record.rs -// carried: "a comment on a row does not make a later update to it recordable" crates/batten/tests/board_record.rs -// carried: "nothing from the body but a tracked path reaches the record" crates/batten/tests/board_record.rs +// carried: "a created row is recorded with its id, updatedAt and a green verdict" crates/batten/tests/it/board_record.rs +// carried: "an unrefined row records a verdict of unready rather than being refused" crates/batten/tests/it/board_record.rs +// carried: "a row whose body names a changed file records a non-zero overlap" crates/batten/tests/it/board_record.rs +// carried: "A PATH NAMED ONLY OUTSIDE §1 IS IN THE NAMED COLUMN AND NOT THE §1 ONE" crates/batten/tests/it/board_record.rs +// carried: "a path named IN §1 reaches the §1 column, so a real claim is still visible" crates/batten/tests/it/board_record.rs +// carried: "a row naming nothing tracked records a zero" crates/batten/tests/it/board_record.rs +// carried: "updating an existing row is never recorded" crates/batten/tests/it/board_record.rs +// carried: "a groom of a row THIS branch filed is recorded" crates/batten/tests/it/board_record.rs +// carried: "a groom of a row this branch did NOT file is still skipped" crates/batten/tests/it/board_record.rs +// carried: "an id that merely PREFIXES a filed one does not count as filed here" crates/batten/tests/it/board_record.rs +// carried: "a groom whose §8 cites a blocker is unjudgeable, not unready" crates/batten/tests/it/board_record.rs +// carried: "a groom of a genuinely unready body still records unready" crates/batten/tests/it/board_record.rs +// carried: "a write records the rows its stored body cites" crates/batten/tests/it/board_record.rs +// carried: "a write passing exactly the rows it cites records zero" crates/batten/tests/it/board_record.rs +// carried: "zero and could-not-look are distinguishable in the record" crates/batten/tests/it/board_record.rs +// carried: "the row's own key is not counted as an edge to anywhere" crates/batten/tests/it/board_record.rs +// carried: "a write the producer never ran for records a dash, never a zero" crates/batten/tests/it/board_record.rs +// carried: "a comment records the issue key its input names, not the comment uuid" crates/batten/tests/it/board_record.rs +// carried: "REGRESSION: a comment row never records a uuid" crates/batten/tests/it/board_record.rs +// carried: "a reply, or a comment on a non-issue parent, records a dash rather than a guess" crates/batten/tests/it/board_record.rs +// carried: "all three live connector spellings are recorded identically" crates/batten/tests/it/board_record.rs +// carried: "a tool that does not write to the board is never recorded" crates/batten/tests/it/board_record.rs +// carried: "POINTER, NEVER PAYLOAD: no byte of the description reaches the record" crates/batten/tests/it/board_record.rs +// carried: "POINTER, NEVER PAYLOAD: the citing sentence does not reach the record" crates/batten/tests/it/board_record.rs +// carried: "a comment on a row does not make a later update to it recordable" crates/batten/tests/it/board_record.rs +// carried: "nothing from the body but a tracked path reaches the record" crates/batten/tests/it/board_record.rs //! //! SUBSUMED — the plumbing became the engine's, which is what a migration should //! produce. Each names the general property that now covers it. @@ -68,13 +68,13 @@ // subsumed: "FAIL OPEN: outside a git repository nothing is recorded and nothing is blocked" crates/batten/src/lib.rs kind:mechanism // subsumed: "the settings entry is wired, on a suffix-anchored PostToolUse matcher" mise-tasks/hooks-wiring-check.sh // subsumed: "the keys come from ready-lint's emission, not a second scan here" crates/batten/src/recorder.rs kind:mechanism -// subsumed: "a row whose §8 claims a blocker still records a green verdict" crates/batten/tests/board_record.rs -// subsumed: "A CREATE CITING A BLOCKER IT DID NOT PASS IS STILL UNREADY" crates/batten/tests/board_record.rs +// subsumed: "a row whose §8 claims a blocker still records a green verdict" crates/batten/tests/it/board_record.rs +// subsumed: "A CREATE CITING A BLOCKER IT DID NOT PASS IS STILL UNREADY" crates/batten/tests/it/board_record.rs //! //! CHANGED — behaviour that diverges deliberately, each with its reason. //! // changed: "board-write-record.bats::the bypass is honoured" crates/batten/src/recorder.rs kind:mechanism BATTEN_BOARD_WRITE_BYPASS is gone rather than ported: a bypass exists to let an author past a REFUSAL, and a recorder refuses nothing, so the only thing it could buy was a quieter record — the one direction the gate reading it cannot detect -// changed: "board-write-record.bats::A FILE THIS BRANCH HAS NOT TOUCHED IS STILL RECORDED" crates/batten/tests/board_record.rs the overlap column holds the paths the body NAMES, intersected by the gate later rather than here, so the case is carried under a name that says what it measures +// changed: "board-write-record.bats::A FILE THIS BRANCH HAS NOT TOUCHED IS STILL RECORDED" crates/batten/tests/it/board_record.rs the overlap column holds the paths the body NAMES, intersected by the gate later rather than here, so the case is carried under a name that says what it measures //! //! `BATTEN_BOARD_WRITE_BYPASS` is **gone rather than ported**, and that is a //! deliberate narrowing rather than an oversight. A bypass exists to let an @@ -109,7 +109,7 @@ #![cfg(unix)] #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/bundle.rs b/crates/batten/tests/it/bundle.rs similarity index 99% rename from crates/batten/tests/bundle.rs rename to crates/batten/tests/it/bundle.rs index 7c2f27a52..5810f3d5d 100644 --- a/crates/batten/tests/bundle.rs +++ b/crates/batten/tests/it/bundle.rs @@ -13,7 +13,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/bypass_scrub.rs b/crates/batten/tests/it/bypass_scrub.rs similarity index 99% rename from crates/batten/tests/bypass_scrub.rs rename to crates/batten/tests/it/bypass_scrub.rs index e2295de47..a46830102 100644 --- a/crates/batten/tests/bypass_scrub.rs +++ b/crates/batten/tests/it/bypass_scrub.rs @@ -32,7 +32,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::io::Write as _; use std::process::Stdio; diff --git a/crates/batten/tests/call_arguments.rs b/crates/batten/tests/it/call_arguments.rs similarity index 99% rename from crates/batten/tests/call_arguments.rs rename to crates/batten/tests/it/call_arguments.rs index ef9d78521..421f59109 100644 --- a/crates/batten/tests/call_arguments.rs +++ b/crates/batten/tests/it/call_arguments.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/call_background_flag.rs b/crates/batten/tests/it/call_background_flag.rs similarity index 99% rename from crates/batten/tests/call_background_flag.rs rename to crates/batten/tests/it/call_background_flag.rs index b5ae8fa0b..ca416def2 100644 --- a/crates/batten/tests/call_background_flag.rs +++ b/crates/batten/tests/it/call_background_flag.rs @@ -15,7 +15,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/call_ceiling.rs b/crates/batten/tests/it/call_ceiling.rs similarity index 99% rename from crates/batten/tests/call_ceiling.rs rename to crates/batten/tests/it/call_ceiling.rs index 28e06d6da..7a21f9d1e 100644 --- a/crates/batten/tests/call_ceiling.rs +++ b/crates/batten/tests/it/call_ceiling.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/capture_fidelity.rs b/crates/batten/tests/it/capture_fidelity.rs similarity index 98% rename from crates/batten/tests/capture_fidelity.rs rename to crates/batten/tests/it/capture_fidelity.rs index c1187394c..da08300a8 100644 --- a/crates/batten/tests/capture_fidelity.rs +++ b/crates/batten/tests/it/capture_fidelity.rs @@ -30,8 +30,8 @@ const ADMITTED: &[&str] = &["LexicalBytes", "SpillFile"]; /// The three that may not. const REFUSED: &[&str] = &["DecodedContent", "Prefix", "Unavailable"]; -const CAPTURE_SRC: &str = include_str!("../src/capture.rs"); -const HOOK_SRC: &str = include_str!("../src/hook.rs"); +const CAPTURE_SRC: &str = include_str!("../../src/capture.rs"); +const HOOK_SRC: &str = include_str!("../../src/hook.rs"); /// The doc paragraphs of a source file. /// diff --git a/crates/batten/tests/captured_facts.rs b/crates/batten/tests/it/captured_facts.rs similarity index 99% rename from crates/batten/tests/captured_facts.rs rename to crates/batten/tests/it/captured_facts.rs index 14e7901b3..cb154d297 100644 --- a/crates/batten/tests/captured_facts.rs +++ b/crates/batten/tests/it/captured_facts.rs @@ -24,7 +24,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/checks_green.rs b/crates/batten/tests/it/checks_green.rs similarity index 99% rename from crates/batten/tests/checks_green.rs rename to crates/batten/tests/it/checks_green.rs index aab5327e5..b18dc9759 100644 --- a/crates/batten/tests/checks_green.rs +++ b/crates/batten/tests/it/checks_green.rs @@ -28,8 +28,8 @@ // arms below by construction — a case arm's first field after the marker is a // QUOTED case name, a file arm's is a path. // -// carried: mise-tasks/checks-green.sh crates/batten/src/checks_green.rs kind:verb crates/batten/tests/checks_green.rs -// carried: tests/checks-green.bats crates/batten/src/checks_green.rs kind:verb crates/batten/tests/checks_green.rs +// carried: mise-tasks/checks-green.sh crates/batten/src/checks_green.rs kind:verb crates/batten/tests/it/checks_green.rs +// carried: tests/checks-green.bats crates/batten/src/checks_green.rs kind:verb crates/batten/tests/it/checks_green.rs // // CLOUD-908's case arms: every `@test` the retired suite declared, and where its // predicate lives now. Twenty-eight carried and two changed — both changes are @@ -71,7 +71,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::io::Write; use std::process::Stdio; diff --git a/crates/batten/tests/ci_hygiene.rs b/crates/batten/tests/it/ci_hygiene.rs similarity index 99% rename from crates/batten/tests/ci_hygiene.rs rename to crates/batten/tests/it/ci_hygiene.rs index 260d8675a..ae3aef077 100644 --- a/crates/batten/tests/ci_hygiene.rs +++ b/crates/batten/tests/it/ci_hygiene.rs @@ -34,7 +34,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/ci_parity.rs b/crates/batten/tests/it/ci_parity.rs similarity index 81% rename from crates/batten/tests/ci_parity.rs rename to crates/batten/tests/it/ci_parity.rs index 06b606516..c7cff524a 100644 --- a/crates/batten/tests/ci_parity.rs +++ b/crates/batten/tests/it/ci_parity.rs @@ -27,7 +27,7 @@ //! //! What a run COSTS — the draft guard, superseding, the concurrency group, the //! ready subscription — is the `ci-hygiene` preset's, because it is true of the -//! practice rather than of this repository. `crates/batten/tests/ci_hygiene.rs` +//! practice rather than of this repository. `crates/batten/tests/it/ci_hygiene.rs` //! is that half's tier. //! //! Whether the foreign-runner cargo invocation still matches the task's own is @@ -45,98 +45,98 @@ //! invocation is there too, reading the manifest directly, with the bound on //! that reading stated above. -// carried: mise-tasks/ci-local-parity.sh policy/ci-parity.rego crates/batten/tests/ci_parity.rs crates/batten/tests/ci_hygiene.rs -// carried: tests/ci-local-parity.bats policy/ci-parity.rego crates/batten/tests/ci_parity.rs crates/batten/tests/ci_hygiene.rs +// carried: mise-tasks/ci-local-parity.sh policy/ci-parity.rego crates/batten/tests/it/ci_parity.rs crates/batten/tests/it/ci_hygiene.rs +// carried: tests/ci-local-parity.bats policy/ci-parity.rego crates/batten/tests/it/ci_parity.rs crates/batten/tests/it/ci_hygiene.rs //! # RETIREMENT LEDGER — `tests/ci-local-parity.bats`, 101 cases //! //! CARRIED — the same assertion, in a new home. -// carried: "a draft-gated, self-superseding workflow running a verify task passes" crates/batten/tests/ci_hygiene.rs -// carried: "a job with no draft guard is refused, and named" crates/batten/tests/ci_hygiene.rs -// carried: "a workflow that does not supersede its own runs is refused" crates/batten/tests/ci_hygiene.rs -// carried: "a task CI runs that verify does not is refused" crates/batten/tests/ci_parity.rs -// carried: "a workflow not triggered by pull_request is out of scope for the landing-path properties" crates/batten/tests/ci_parity.rs -// carried: "a pull_request job missing from CI_REQUIRED_CHECKS is refused" crates/batten/tests/ci_parity.rs -// carried: "a required name matching no job is refused" crates/batten/tests/ci_parity.rs -// carried: "a matrix leg matches on its base name" crates/batten/tests/ci_parity.rs -// carried: "a manifest with no required set at all is a failure, not a pass" crates/batten/tests/ci_parity.rs -// carried: "a release config that does not open the release PR as a draft is refused" crates/batten/tests/ci_parity.rs -// carried: "a release config set to something other than true is refused" crates/batten/tests/ci_parity.rs -// carried: "a fan-in that enumerates only some of its needs is refused, and the omission named" crates/batten/tests/ci_hygiene.rs -// carried: "a fan-in asserting over needs.* passes, and stays passing when a leg is added" crates/batten/tests/ci_hygiene.rs -// carried: "an unquoted # that swallows an interpolation is refused, and named" crates/batten/tests/ci_hygiene.rs -// carried: "the same value quoted passes — the repair must not be refused" crates/batten/tests/ci_hygiene.rs -// carried: "a whole-line comment mentioning an interpolation passes" crates/batten/tests/ci_hygiene.rs -// carried: "a trailing comment with no interpolation after it passes" crates/batten/tests/ci_hygiene.rs -// carried: "a foreign-runner job that runs nothing is not a second spelling" crates/batten/tests/ci_parity.rs -// carried: "a cache-warm compile with no cache-hit guard is refused" crates/batten/tests/ci_hygiene.rs -// carried: "a guard naming a step id that does not exist is refused" crates/batten/tests/ci_hygiene.rs -// carried: "a guarded cache-warm compile passes" crates/batten/tests/ci_hygiene.rs -// carried: "the no-run exemption cannot be used to escape the property" crates/batten/tests/ci_parity.rs -// carried: "this repository's real workflows pass" crates/batten/tests/ci_hygiene.rs -// carried: "a job that starts without asking the landing lease is refused, and named" crates/batten/tests/ci_parity.rs -// carried: "the precondition must be FIRST — a job that asks after installing has already spent" crates/batten/tests/ci_parity.rs -// carried: "a fan-in is exempt, because it cannot start before its dependencies" crates/batten/tests/ci_parity.rs -// carried: "a scheduled workflow with no concurrency group is refused, and named" crates/batten/tests/ci_hygiene.rs -// carried: "a scheduled workflow that declares one passes, with cancel-in-progress false" crates/batten/tests/ci_hygiene.rs -// carried: "two workflows sharing a cron expression are refused, and both named" crates/batten/tests/ci_hygiene.rs -// carried: "a staggered pair passes" crates/batten/tests/ci_hygiene.rs -// carried: "an every-30-minutes schedule beside a weekly slot is not a collision" crates/batten/tests/ci_hygiene.rs -// carried: "a workflow_run job filtering on head_branch with no trigger filter is refused" crates/batten/tests/ci_hygiene.rs -// carried: "the same workflow with a trigger-level branches filter passes" crates/batten/tests/ci_hygiene.rs -// carried: "a workflow_run workflow with no branch condition at all is not asked for a filter" crates/batten/tests/ci_hygiene.rs -// carried: "no dependabot config is the passing state — the bot is retired (CLOUD-660)" crates/batten/tests/ci_parity.rs -// carried: "a dependabot config that comes back is refused, and named" crates/batten/tests/ci_parity.rs -// carried: "an empty dependabot config is still a config — presence is the predicate" crates/batten/tests/ci_parity.rs -// carried: "a renovate config carrying all five keys passes" crates/batten/tests/ci_parity.rs -// carried: "each of the five keys missing is refused, and named" crates/batten/tests/ci_parity.rs -// carried: "REVERTING rebaseWhen TO never IS REFUSED, because that is the regression (CLOUD-692)" crates/batten/tests/ci_parity.rs -// carried: "a key present with a value that is not the fix is the same defect" crates/batten/tests/ci_parity.rs -// carried: "all three ecosystems named in the one config passes" crates/batten/tests/ci_parity.rs -// carried: "an ecosystem missing from enabledManagers is refused, and named" crates/batten/tests/ci_parity.rs -// carried: "mise IS judged now — the one bot can read that file, so its absence is a drift" crates/batten/tests/ci_parity.rs -// carried: "a bot prefix with no workflow scoped to it is refused, and named" crates/batten/tests/ci_parity.rs -// carried: "a trigger-level branches filter is what satisfies it" crates/batten/tests/ci_parity.rs -// carried: "A JOB CONDITION IS NOT A SCOPE, which is property 10's finding reused" crates/batten/tests/ci_parity.rs -// carried: "the prefix is read from the config that owns it, not assumed" crates/batten/tests/ci_parity.rs -// carried: "a lane whose config is absent is not asked for a watcher" crates/batten/tests/ci_parity.rs -// carried: "a trigger no job condition admits is refused, and named" crates/batten/tests/ci_hygiene.rs -// carried: "the same workflow admitting both triggers passes" crates/batten/tests/ci_hygiene.rs -// carried: "workflow_run is admitted by reading its payload, not only by naming the event" crates/batten/tests/ci_hygiene.rs -// carried: "a job condition that mentions no event admits everything, so nothing is judged" crates/batten/tests/ci_hygiene.rs -// carried: "a workflow reading check-runs without checks-green is refused" crates/batten/tests/ci_parity.rs -// carried: "the same workflow deciding through checks-green passes" crates/batten/tests/ci_parity.rs -// carried: "a workflow that never reads check status is not asked for the predicate" crates/batten/tests/ci_parity.rs -// carried: "a Windows job may run a task verify does not — there is no local Windows to have caught it" crates/batten/tests/ci_parity.rs -// carried: "a macOS job is exempt on the same reasoning" crates/batten/tests/ci_parity.rs -// carried: "the identical step on a Linux runner is still refused" crates/batten/tests/ci_parity.rs -// carried: "a job declaring no runs-on is judged, not exempted" crates/batten/tests/ci_parity.rs -// carried: "an unclassified runner label is judged — the exemption is foreign labels, not non-Linux ones" crates/batten/tests/ci_parity.rs -// carried: "a Windows job running a task verify DOES run is still fine" crates/batten/tests/ci_parity.rs -// carried: "the exemption is per job, so a Linux job beside a Windows one is still judged" crates/batten/tests/ci_parity.rs -// carried: "a commit type inside packageRules passes" crates/batten/tests/ci_parity.rs -// carried: "no commit type anywhere is refused" crates/batten/tests/ci_parity.rs -// carried: "THE MEASURED DEFECT: a top-level commit type is refused, because a preset outranks it" crates/batten/tests/ci_parity.rs -// carried: "a config with no packageRules at all is refused, and says why" crates/batten/tests/ci_parity.rs -// carried: "a foreign-runner command matching the task passes" crates/batten/tests/ci_parity.rs -// carried: "a task that gained a flag the foreign runner did not is refused, and names both" crates/batten/tests/ci_parity.rs -// carried: "a foreign runner whose command drifted from the task is refused the same way" crates/batten/tests/ci_parity.rs -// carried: "a tree with no foreign-runner cargo job is refused, not passed" crates/batten/tests/ci_parity.rs -// carried: "a task yielding no cargo invocation is refused, not passed" crates/batten/tests/ci_parity.rs -// carried: "an anchored comment trigger that also reads draft state passes" crates/batten/tests/ci_hygiene.rs -// carried: "CLOUD-853: an UNANCHORED comment trigger is refused, because prose naming the token fires it" crates/batten/tests/ci_hygiene.rs -// carried: "CLOUD-853: a comment-triggered merge that never reads draft state is refused" crates/batten/tests/ci_hygiene.rs -// carried: "a comment-triggered workflow that does NOT merge is not asked the draft question" crates/batten/tests/ci_hygiene.rs -// carried: "a manifest with no CI_FANIN_CHECK is refused" crates/batten/tests/ci_parity.rs -// carried: "a manifest with no CI_FANIN_WORKFLOW is refused" crates/batten/tests/ci_parity.rs -// carried: "a fan-in that is not in the required roster is refused" crates/batten/tests/ci_parity.rs -// carried: "a fan-in workflow that is not a file is refused" crates/batten/tests/ci_parity.rs -// carried: "a fan-in workflow that declares no job of that name is refused" crates/batten/tests/ci_parity.rs -// carried: "an abandon task that restates the path instead of reading it is refused" crates/batten/tests/ci_parity.rs -// carried: "a missing abandon task is refused rather than passed" crates/batten/tests/ci_parity.rs -// carried: "THE ANTI-VACUITY TERM: a lander that never calls the abandon is refused" crates/batten/tests/ci_parity.rs -// carried: "a missing lander is refused rather than passed" crates/batten/tests/ci_parity.rs +// carried: "a draft-gated, self-superseding workflow running a verify task passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "a job with no draft guard is refused, and named" crates/batten/tests/it/ci_hygiene.rs +// carried: "a workflow that does not supersede its own runs is refused" crates/batten/tests/it/ci_hygiene.rs +// carried: "a task CI runs that verify does not is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a workflow not triggered by pull_request is out of scope for the landing-path properties" crates/batten/tests/it/ci_parity.rs +// carried: "a pull_request job missing from CI_REQUIRED_CHECKS is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a required name matching no job is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a matrix leg matches on its base name" crates/batten/tests/it/ci_parity.rs +// carried: "a manifest with no required set at all is a failure, not a pass" crates/batten/tests/it/ci_parity.rs +// carried: "a release config that does not open the release PR as a draft is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a release config set to something other than true is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a fan-in that enumerates only some of its needs is refused, and the omission named" crates/batten/tests/it/ci_hygiene.rs +// carried: "a fan-in asserting over needs.* passes, and stays passing when a leg is added" crates/batten/tests/it/ci_hygiene.rs +// carried: "an unquoted # that swallows an interpolation is refused, and named" crates/batten/tests/it/ci_hygiene.rs +// carried: "the same value quoted passes — the repair must not be refused" crates/batten/tests/it/ci_hygiene.rs +// carried: "a whole-line comment mentioning an interpolation passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "a trailing comment with no interpolation after it passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "a foreign-runner job that runs nothing is not a second spelling" crates/batten/tests/it/ci_parity.rs +// carried: "a cache-warm compile with no cache-hit guard is refused" crates/batten/tests/it/ci_hygiene.rs +// carried: "a guard naming a step id that does not exist is refused" crates/batten/tests/it/ci_hygiene.rs +// carried: "a guarded cache-warm compile passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "the no-run exemption cannot be used to escape the property" crates/batten/tests/it/ci_parity.rs +// carried: "this repository's real workflows pass" crates/batten/tests/it/ci_hygiene.rs +// carried: "a job that starts without asking the landing lease is refused, and named" crates/batten/tests/it/ci_parity.rs +// carried: "the precondition must be FIRST — a job that asks after installing has already spent" crates/batten/tests/it/ci_parity.rs +// carried: "a fan-in is exempt, because it cannot start before its dependencies" crates/batten/tests/it/ci_parity.rs +// carried: "a scheduled workflow with no concurrency group is refused, and named" crates/batten/tests/it/ci_hygiene.rs +// carried: "a scheduled workflow that declares one passes, with cancel-in-progress false" crates/batten/tests/it/ci_hygiene.rs +// carried: "two workflows sharing a cron expression are refused, and both named" crates/batten/tests/it/ci_hygiene.rs +// carried: "a staggered pair passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "an every-30-minutes schedule beside a weekly slot is not a collision" crates/batten/tests/it/ci_hygiene.rs +// carried: "a workflow_run job filtering on head_branch with no trigger filter is refused" crates/batten/tests/it/ci_hygiene.rs +// carried: "the same workflow with a trigger-level branches filter passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "a workflow_run workflow with no branch condition at all is not asked for a filter" crates/batten/tests/it/ci_hygiene.rs +// carried: "no dependabot config is the passing state — the bot is retired (CLOUD-660)" crates/batten/tests/it/ci_parity.rs +// carried: "a dependabot config that comes back is refused, and named" crates/batten/tests/it/ci_parity.rs +// carried: "an empty dependabot config is still a config — presence is the predicate" crates/batten/tests/it/ci_parity.rs +// carried: "a renovate config carrying all five keys passes" crates/batten/tests/it/ci_parity.rs +// carried: "each of the five keys missing is refused, and named" crates/batten/tests/it/ci_parity.rs +// carried: "REVERTING rebaseWhen TO never IS REFUSED, because that is the regression (CLOUD-692)" crates/batten/tests/it/ci_parity.rs +// carried: "a key present with a value that is not the fix is the same defect" crates/batten/tests/it/ci_parity.rs +// carried: "all three ecosystems named in the one config passes" crates/batten/tests/it/ci_parity.rs +// carried: "an ecosystem missing from enabledManagers is refused, and named" crates/batten/tests/it/ci_parity.rs +// carried: "mise IS judged now — the one bot can read that file, so its absence is a drift" crates/batten/tests/it/ci_parity.rs +// carried: "a bot prefix with no workflow scoped to it is refused, and named" crates/batten/tests/it/ci_parity.rs +// carried: "a trigger-level branches filter is what satisfies it" crates/batten/tests/it/ci_parity.rs +// carried: "A JOB CONDITION IS NOT A SCOPE, which is property 10's finding reused" crates/batten/tests/it/ci_parity.rs +// carried: "the prefix is read from the config that owns it, not assumed" crates/batten/tests/it/ci_parity.rs +// carried: "a lane whose config is absent is not asked for a watcher" crates/batten/tests/it/ci_parity.rs +// carried: "a trigger no job condition admits is refused, and named" crates/batten/tests/it/ci_hygiene.rs +// carried: "the same workflow admitting both triggers passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "workflow_run is admitted by reading its payload, not only by naming the event" crates/batten/tests/it/ci_hygiene.rs +// carried: "a job condition that mentions no event admits everything, so nothing is judged" crates/batten/tests/it/ci_hygiene.rs +// carried: "a workflow reading check-runs without checks-green is refused" crates/batten/tests/it/ci_parity.rs +// carried: "the same workflow deciding through checks-green passes" crates/batten/tests/it/ci_parity.rs +// carried: "a workflow that never reads check status is not asked for the predicate" crates/batten/tests/it/ci_parity.rs +// carried: "a Windows job may run a task verify does not — there is no local Windows to have caught it" crates/batten/tests/it/ci_parity.rs +// carried: "a macOS job is exempt on the same reasoning" crates/batten/tests/it/ci_parity.rs +// carried: "the identical step on a Linux runner is still refused" crates/batten/tests/it/ci_parity.rs +// carried: "a job declaring no runs-on is judged, not exempted" crates/batten/tests/it/ci_parity.rs +// carried: "an unclassified runner label is judged — the exemption is foreign labels, not non-Linux ones" crates/batten/tests/it/ci_parity.rs +// carried: "a Windows job running a task verify DOES run is still fine" crates/batten/tests/it/ci_parity.rs +// carried: "the exemption is per job, so a Linux job beside a Windows one is still judged" crates/batten/tests/it/ci_parity.rs +// carried: "a commit type inside packageRules passes" crates/batten/tests/it/ci_parity.rs +// carried: "no commit type anywhere is refused" crates/batten/tests/it/ci_parity.rs +// carried: "THE MEASURED DEFECT: a top-level commit type is refused, because a preset outranks it" crates/batten/tests/it/ci_parity.rs +// carried: "a config with no packageRules at all is refused, and says why" crates/batten/tests/it/ci_parity.rs +// carried: "a foreign-runner command matching the task passes" crates/batten/tests/it/ci_parity.rs +// carried: "a task that gained a flag the foreign runner did not is refused, and names both" crates/batten/tests/it/ci_parity.rs +// carried: "a foreign runner whose command drifted from the task is refused the same way" crates/batten/tests/it/ci_parity.rs +// carried: "a tree with no foreign-runner cargo job is refused, not passed" crates/batten/tests/it/ci_parity.rs +// carried: "a task yielding no cargo invocation is refused, not passed" crates/batten/tests/it/ci_parity.rs +// carried: "an anchored comment trigger that also reads draft state passes" crates/batten/tests/it/ci_hygiene.rs +// carried: "CLOUD-853: an UNANCHORED comment trigger is refused, because prose naming the token fires it" crates/batten/tests/it/ci_hygiene.rs +// carried: "CLOUD-853: a comment-triggered merge that never reads draft state is refused" crates/batten/tests/it/ci_hygiene.rs +// carried: "a comment-triggered workflow that does NOT merge is not asked the draft question" crates/batten/tests/it/ci_hygiene.rs +// carried: "a manifest with no CI_FANIN_CHECK is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a manifest with no CI_FANIN_WORKFLOW is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a fan-in that is not in the required roster is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a fan-in workflow that is not a file is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a fan-in workflow that declares no job of that name is refused" crates/batten/tests/it/ci_parity.rs +// carried: "an abandon task that restates the path instead of reading it is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a missing abandon task is refused rather than passed" crates/batten/tests/it/ci_parity.rs +// carried: "THE ANTI-VACUITY TERM: a lander that never calls the abandon is refused" crates/batten/tests/it/ci_parity.rs +// carried: "a missing lander is refused rather than passed" crates/batten/tests/it/ci_parity.rs //! //! SUBSUMED — a more general property covers it now. Every one of these is @@ -144,23 +144,23 @@ //! and a parsed value is one shape whatever its source formatting, so a class the //! shell had to exclude by hand cannot arise here at all. -// subsumed: "a task named only in a comment is not read as spend" crates/batten/tests/ci_parity.rs the reading is bounded to `run:` scalars and a YAML comment does not survive the parse, so prose cannot be read as spend by construction rather than by exclusion -// subsumed: "a multi-line run block is read too" crates/batten/tests/ci_parity.rs a parsed `run:` scalar carries its whole body whatever the block style, so the folded and literal forms are one shape rather than two -// subsumed: "a cron named only in a comment is not read as a schedule" crates/batten/tests/ci_hygiene.rs a YAML comment does not survive the parse at all, so the false-positive class this excluded by hand cannot arise over a parsed document -// subsumed: "a key named only in a comment does not satisfy the property" crates/batten/tests/ci_parity.rs a JSON5 comment does not survive the parse, so a key named in prose cannot answer for one that is set -// subsumed: "a manager list broken across lines reads the same as one on a single line" crates/batten/tests/ci_parity.rs a parsed array is one value whatever the source formatting, so a formatter's line choice cannot change the verdict by construction -// subsumed: "the endpoint named only in a comment does not demand the predicate" crates/batten/tests/ci_parity.rs comments do not survive the parse, so the endpoint named in prose cannot demand the predicate +// subsumed: "a task named only in a comment is not read as spend" crates/batten/tests/it/ci_parity.rs the reading is bounded to `run:` scalars and a YAML comment does not survive the parse, so prose cannot be read as spend by construction rather than by exclusion +// subsumed: "a multi-line run block is read too" crates/batten/tests/it/ci_parity.rs a parsed `run:` scalar carries its whole body whatever the block style, so the folded and literal forms are one shape rather than two +// subsumed: "a cron named only in a comment is not read as a schedule" crates/batten/tests/it/ci_hygiene.rs a YAML comment does not survive the parse at all, so the false-positive class this excluded by hand cannot arise over a parsed document +// subsumed: "a key named only in a comment does not satisfy the property" crates/batten/tests/it/ci_parity.rs a JSON5 comment does not survive the parse, so a key named in prose cannot answer for one that is set +// subsumed: "a manager list broken across lines reads the same as one on a single line" crates/batten/tests/it/ci_parity.rs a parsed array is one value whatever the source formatting, so a formatter's line choice cannot change the verdict by construction +// subsumed: "the endpoint named only in a comment does not demand the predicate" crates/batten/tests/it/ci_parity.rs comments do not survive the parse, so the endpoint named in prose cannot demand the predicate //! //! CHANGED — behaviour that diverges deliberately, each with its reason. -// changed: "the concurrency property judges every workflow, not only the pull_request ones" crates/batten/tests/ci_hygiene.rs it judges every workflow whose runs answer about ONE SUBJECT — pull_request, issue_comment, workflow_run and schedule — and no longer a push-only workflow, whose runs are each keyed to a different commit and are therefore two subjects rather than two answers. Every measured instance of the original defect is inside the narrowed set; what it gives up is a preset that refuses an ordinary minimal repository, which this tree's own shipped-config canary in tests/prebuilt-lint.bats is what surfaced -// changed: "a required check whose workflow cannot see ready_for_review is refused" crates/batten/tests/ci_hygiene.rs the preset scopes it to a workflow that DRAFT-GATES rather than to one producing a required check: a roster is a consumer fact and cannot live in a vendored preset (rule 1). Same condition read from the workflow itself, since a job that skips on a draft is one whose verdict can only arrive on the ready event -// changed: "a workflow producing no required check may omit ready_for_review" crates/batten/tests/ci_hygiene.rs the exemption is now 'does not draft-gate' rather than 'produces no required check', for the same rule-1 reason; a workflow whose jobs run on drafts has no skipped run to supersede -// changed: "finding no pull_request workflow at all is a failure, not a pass" crates/batten/tests/ci_parity.rs the engine distinguishes could-not-look from not-applicable through `input.tree.missing`, so an unreadable workflow raises V-CI-WORKFLOW-UNREAD while a tree that genuinely runs no such workflow is not-applicable. The shell had one channel for both and had to refuse the empty case to avoid a vacuous pass -// changed: "a missing release config is a failure, not a pass" crates/batten/tests/ci_parity.rs a consumer with no release automation is not-applicable rather than refused; the row's `sources` declares the file, so a declared-but-unparseable one raises the could-not-look verdict instead -// changed: "an empty workflow directory is refused rather than silently green" crates/batten/tests/ci_parity.rs the anti-vacuity term moved from the gate's own counter to the rule guards: each rule stands down on a tree carrying no workflow, and the compiled-binary tier's `this_repository_is_clean_today` is what proves the rules are not vacuous over the real tree -// changed: "a missing renovate config is a failure, not a pass" crates/batten/tests/ci_parity.rs same as the release config: absent is not-applicable and unparseable is loud, which is the distinction the shell could not draw +// changed: "the concurrency property judges every workflow, not only the pull_request ones" crates/batten/tests/it/ci_hygiene.rs it judges every workflow whose runs answer about ONE SUBJECT — pull_request, issue_comment, workflow_run and schedule — and no longer a push-only workflow, whose runs are each keyed to a different commit and are therefore two subjects rather than two answers. Every measured instance of the original defect is inside the narrowed set; what it gives up is a preset that refuses an ordinary minimal repository, which this tree's own shipped-config canary in tests/prebuilt-lint.bats is what surfaced +// changed: "a required check whose workflow cannot see ready_for_review is refused" crates/batten/tests/it/ci_hygiene.rs the preset scopes it to a workflow that DRAFT-GATES rather than to one producing a required check: a roster is a consumer fact and cannot live in a vendored preset (rule 1). Same condition read from the workflow itself, since a job that skips on a draft is one whose verdict can only arrive on the ready event +// changed: "a workflow producing no required check may omit ready_for_review" crates/batten/tests/it/ci_hygiene.rs the exemption is now 'does not draft-gate' rather than 'produces no required check', for the same rule-1 reason; a workflow whose jobs run on drafts has no skipped run to supersede +// changed: "finding no pull_request workflow at all is a failure, not a pass" crates/batten/tests/it/ci_parity.rs the engine distinguishes could-not-look from not-applicable through `input.tree.missing`, so an unreadable workflow raises V-CI-WORKFLOW-UNREAD while a tree that genuinely runs no such workflow is not-applicable. The shell had one channel for both and had to refuse the empty case to avoid a vacuous pass +// changed: "a missing release config is a failure, not a pass" crates/batten/tests/it/ci_parity.rs a consumer with no release automation is not-applicable rather than refused; the row's `sources` declares the file, so a declared-but-unparseable one raises the could-not-look verdict instead +// changed: "an empty workflow directory is refused rather than silently green" crates/batten/tests/it/ci_parity.rs the anti-vacuity term moved from the gate's own counter to the rule guards: each rule stands down on a tree carrying no workflow, and the compiled-binary tier's `this_repository_is_clean_today` is what proves the rules are not vacuous over the real tree +// changed: "a missing renovate config is a failure, not a pass" crates/batten/tests/it/ci_parity.rs same as the release config: absent is not-applicable and unparseable is loud, which is the distinction the shell could not draw //! //! WITHDRAWN — nothing replaced these, because nothing should. All three assert @@ -175,7 +175,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/ci_suite_lane.rs b/crates/batten/tests/it/ci_suite_lane.rs similarity index 99% rename from crates/batten/tests/ci_suite_lane.rs rename to crates/batten/tests/it/ci_suite_lane.rs index 6c1a9f587..83a957530 100644 --- a/crates/batten/tests/ci_suite_lane.rs +++ b/crates/batten/tests/it/ci_suite_lane.rs @@ -45,7 +45,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/claim.rs b/crates/batten/tests/it/claim.rs similarity index 92% rename from crates/batten/tests/claim.rs rename to crates/batten/tests/it/claim.rs index f02aa7a39..33bccd1a2 100644 --- a/crates/batten/tests/claim.rs +++ b/crates/batten/tests/it/claim.rs @@ -33,85 +33,85 @@ //! //! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads //! -// carried: mise-tasks/claim-check.sh crates/batten/src/claim.rs kind:verb crates/batten/tests/claim.rs -// carried: tests/claim-check.bats crates/batten/src/claim.rs kind:verb crates/batten/tests/claim.rs +// carried: mise-tasks/claim-check.sh crates/batten/src/claim.rs kind:verb crates/batten/tests/it/claim.rs +// carried: tests/claim-check.bats crates/batten/src/claim.rs kind:verb crates/batten/tests/it/claim.rs //! //! # RETIREMENT LEDGER — `tests/claim-check.bats`, 76 cases //! //! CARRIED — the property survives, proved here against the engine. //! -// carried: "a Todo issue with nobody on it is pullable" crates/batten/tests/claim.rs -// carried: "the pullable message says to claim it, because the automation will not" crates/batten/tests/claim.rs -// carried: "an issue already In Progress is not pullable" crates/batten/tests/claim.rs -// carried: "In Review and Done are not pullable either" crates/batten/tests/claim.rs -// carried: "a Todo issue someone has already assigned is flagged" crates/batten/tests/claim.rs -// carried: "a Todo issue with a PR already attached is flagged, with the PR number" crates/batten/tests/claim.rs -// carried: "a non-PR attachment is not a claim" crates/batten/tests/claim.rs -// carried: "output is pointer-only — the issue id and the rule, never a body" crates/batten/tests/claim.rs -// carried: "a set of issues is judged as a set, and one bad apple blocks" crates/batten/tests/claim.rs -// carried: "a JSON array is accepted as well as a stream, matching graph-check" crates/batten/tests/claim.rs -// carried: "unreadable stdin is exit 2, distinct from a failing check" crates/batten/tests/claim.rs -// carried: "empty stdin is exit 2, not a silent pass" crates/batten/tests/claim.rs -// carried: "a payload missing status is unreadable rather than assumed Todo" crates/batten/tests/claim.rs -// carried: "not-todo, assigned and has-pr are each reachable on a payload with no description" crates/batten/tests/claim.rs -// carried: "a bodyless payload nothing else refuses is exit 2 naming description, never a pass" crates/batten/tests/claim.rs -// carried: "the pullable path mints a receipt for the current branch" crates/batten/tests/claim.rs -// carried: "a NOT-pullable issue mints nothing — the receipt is the claim, not the attempt" crates/batten/tests/claim.rs -// carried: "the id list stays line 1 with a clause recorded" crates/batten/tests/claim.rs -// carried: "unreadable stdin mints nothing either" crates/batten/tests/claim.rs -// carried: "outside a checkout the verdict still stands — the receipt is a side effect" crates/batten/tests/claim.rs -// carried: "THE INCIDENT REPLAY: an issue refined inside this session is refused at the claim" crates/batten/tests/claim.rs -// carried: "the legitimate path is not prompted, delayed or refused" crates/batten/tests/claim.rs -// carried: "a block ready-lint refuses mints no receipt" crates/batten/tests/claim.rs -// carried: "the refusal is pointer-only — the rule id, never the block it read" crates/batten/tests/claim.rs -// carried: "CLOUD-597 REPLAY: a row whose updatedAt moved but whose BODY did not is pullable" crates/batten/tests/claim.rs -// carried: "CLOUD-615 REPLAY: a body rewritten under this clone is refused even when the stamp is NEWER" crates/batten/tests/claim.rs -// carried: "a receipt minted the way the engine mints it is accepted" crates/batten/tests/claim.rs -// carried: "the baseline refusal is pointer-only — never a line of the body it compared" crates/batten/tests/claim.rs -// carried: "a missing session stamp REFUSES rather than passing" crates/batten/tests/claim.rs -// carried: "A DELETED READ RECEIPT IS A REFUSAL, never a fall-through to the clock" crates/batten/tests/claim.rs -// carried: "the refusal names its remedy, and it is one command over the payload in hand" crates/batten/tests/claim.rs -// carried: "a HOLLOW receipt is absence, not a weaker yes" crates/batten/tests/claim.rs -// carried: "OUTSIDE a checkout the question stays not-applicable, exactly as the stamp does" crates/batten/tests/claim.rs -// carried: "a receipt store this process cannot read is exit 2 — could not look, not absent" crates/batten/tests/claim.rs -// carried: "the bypass clears the absent baseline too, and says so in the receipt" crates/batten/tests/claim.rs -// carried: "--takeover does NOT clear an absent baseline" crates/batten/tests/claim.rs -// carried: "the bypass mints a receipt in BOTH refused cases, and says so" crates/batten/tests/claim.rs -// carried: "the receipt records the verdict and the revision it was taken against" crates/batten/tests/claim.rs -// carried: "the receipt records the origin/main it was claimed against" crates/batten/tests/claim.rs -// carried: "a bypassed claim says so IN the receipt, not only on stderr" crates/batten/tests/claim.rs -// carried: "an occupied issue is refused when no takeover is asked for" crates/batten/tests/claim.rs -// carried: "THE TAKEOVER: an occupied issue is claimable deliberately, and mints a receipt" crates/batten/tests/claim.rs -// carried: "a takeover receipt NAMES the refusals it overrode, never a bare flag" crates/batten/tests/claim.rs -// carried: "a clean claim records no takeover line" crates/batten/tests/claim.rs -// carried: "a sequence refusal is NOT cleared by --takeover" crates/batten/tests/claim.rs -// carried: "the sequence refusal names the bypass, not the takeover" crates/batten/tests/claim.rs -// carried: "the bypass DOES clear a sequence refusal, so the two hatches stay distinct" crates/batten/tests/claim.rs -// carried: "narrowing the takeover does not break it: a competitor refusal still clears" crates/batten/tests/claim.rs -// carried: "the takeover does not silence the refusals — they are still reported" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause a — a MERGED pull request is a predecessor, not a competitor" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause a — the SAME payload without the state still refuses" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause b — a CLOSED unmerged pull request does not refuse either" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause b — the merged BOOLEAN alone is enough, without a state string" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause c — an OPEN pull request still refuses — the rule is not deleted" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause d — a malformed state refuses rather than reading as merged" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause d — the state is read case-insensitively, as the API spells it" crates/batten/tests/claim.rs -// carried: "CLOUD-520 clause e — a non-PR attachment carrying a state is still ignored" crates/batten/tests/claim.rs -// carried: "CLOUD-520 remedy — the refusal names the remedy, not merely the refusal" crates/batten/tests/claim.rs -// carried: "the minted receipt records the branch it was minted for" crates/batten/tests/claim.rs -// carried: "A RENAMED BRANCH RECOVERS ITS CLAIM WITH --adopt" crates/batten/tests/claim.rs -// carried: "WITHOUT --adopt the rename is still unrecovered — the recovery is opt-in" crates/batten/tests/claim.rs -// carried: "the adoption is recorded, never silent" crates/batten/tests/claim.rs -// carried: "a receipt whose branch still exists is not adopted" crates/batten/tests/claim.rs -// carried: "adopting onto a branch that already has a receipt is refused" crates/batten/tests/claim.rs -// carried: "a receipt with no branch line is not adoptable, never grandfathered" crates/batten/tests/claim.rs -// carried: "two orphans refuse and name both rather than guessing" crates/batten/tests/claim.rs -// carried: "--adopt-from picks one when two orphans are present" crates/batten/tests/claim.rs -// carried: "a detached HEAD has no name to adopt onto" crates/batten/tests/claim.rs -// carried: "THE TAKEOVER AS A FLAG: reachable where an env-var bypass is classified" crates/batten/tests/claim.rs -// carried: "an unknown flag is a usage error, not a silent pull" crates/batten/tests/claim.rs -// carried: "--adopt-from with no value is refused, and does not hang" crates/batten/tests/claim.rs -// carried: "--adopt-from with an empty value is refused rather than silently defaulted" crates/batten/tests/claim.rs +// carried: "a Todo issue with nobody on it is pullable" crates/batten/tests/it/claim.rs +// carried: "the pullable message says to claim it, because the automation will not" crates/batten/tests/it/claim.rs +// carried: "an issue already In Progress is not pullable" crates/batten/tests/it/claim.rs +// carried: "In Review and Done are not pullable either" crates/batten/tests/it/claim.rs +// carried: "a Todo issue someone has already assigned is flagged" crates/batten/tests/it/claim.rs +// carried: "a Todo issue with a PR already attached is flagged, with the PR number" crates/batten/tests/it/claim.rs +// carried: "a non-PR attachment is not a claim" crates/batten/tests/it/claim.rs +// carried: "output is pointer-only — the issue id and the rule, never a body" crates/batten/tests/it/claim.rs +// carried: "a set of issues is judged as a set, and one bad apple blocks" crates/batten/tests/it/claim.rs +// carried: "a JSON array is accepted as well as a stream, matching graph-check" crates/batten/tests/it/claim.rs +// carried: "unreadable stdin is exit 2, distinct from a failing check" crates/batten/tests/it/claim.rs +// carried: "empty stdin is exit 2, not a silent pass" crates/batten/tests/it/claim.rs +// carried: "a payload missing status is unreadable rather than assumed Todo" crates/batten/tests/it/claim.rs +// carried: "not-todo, assigned and has-pr are each reachable on a payload with no description" crates/batten/tests/it/claim.rs +// carried: "a bodyless payload nothing else refuses is exit 2 naming description, never a pass" crates/batten/tests/it/claim.rs +// carried: "the pullable path mints a receipt for the current branch" crates/batten/tests/it/claim.rs +// carried: "a NOT-pullable issue mints nothing — the receipt is the claim, not the attempt" crates/batten/tests/it/claim.rs +// carried: "the id list stays line 1 with a clause recorded" crates/batten/tests/it/claim.rs +// carried: "unreadable stdin mints nothing either" crates/batten/tests/it/claim.rs +// carried: "outside a checkout the verdict still stands — the receipt is a side effect" crates/batten/tests/it/claim.rs +// carried: "THE INCIDENT REPLAY: an issue refined inside this session is refused at the claim" crates/batten/tests/it/claim.rs +// carried: "the legitimate path is not prompted, delayed or refused" crates/batten/tests/it/claim.rs +// carried: "a block ready-lint refuses mints no receipt" crates/batten/tests/it/claim.rs +// carried: "the refusal is pointer-only — the rule id, never the block it read" crates/batten/tests/it/claim.rs +// carried: "CLOUD-597 REPLAY: a row whose updatedAt moved but whose BODY did not is pullable" crates/batten/tests/it/claim.rs +// carried: "CLOUD-615 REPLAY: a body rewritten under this clone is refused even when the stamp is NEWER" crates/batten/tests/it/claim.rs +// carried: "a receipt minted the way the engine mints it is accepted" crates/batten/tests/it/claim.rs +// carried: "the baseline refusal is pointer-only — never a line of the body it compared" crates/batten/tests/it/claim.rs +// carried: "a missing session stamp REFUSES rather than passing" crates/batten/tests/it/claim.rs +// carried: "A DELETED READ RECEIPT IS A REFUSAL, never a fall-through to the clock" crates/batten/tests/it/claim.rs +// carried: "the refusal names its remedy, and it is one command over the payload in hand" crates/batten/tests/it/claim.rs +// carried: "a HOLLOW receipt is absence, not a weaker yes" crates/batten/tests/it/claim.rs +// carried: "OUTSIDE a checkout the question stays not-applicable, exactly as the stamp does" crates/batten/tests/it/claim.rs +// carried: "a receipt store this process cannot read is exit 2 — could not look, not absent" crates/batten/tests/it/claim.rs +// carried: "the bypass clears the absent baseline too, and says so in the receipt" crates/batten/tests/it/claim.rs +// carried: "--takeover does NOT clear an absent baseline" crates/batten/tests/it/claim.rs +// carried: "the bypass mints a receipt in BOTH refused cases, and says so" crates/batten/tests/it/claim.rs +// carried: "the receipt records the verdict and the revision it was taken against" crates/batten/tests/it/claim.rs +// carried: "the receipt records the origin/main it was claimed against" crates/batten/tests/it/claim.rs +// carried: "a bypassed claim says so IN the receipt, not only on stderr" crates/batten/tests/it/claim.rs +// carried: "an occupied issue is refused when no takeover is asked for" crates/batten/tests/it/claim.rs +// carried: "THE TAKEOVER: an occupied issue is claimable deliberately, and mints a receipt" crates/batten/tests/it/claim.rs +// carried: "a takeover receipt NAMES the refusals it overrode, never a bare flag" crates/batten/tests/it/claim.rs +// carried: "a clean claim records no takeover line" crates/batten/tests/it/claim.rs +// carried: "a sequence refusal is NOT cleared by --takeover" crates/batten/tests/it/claim.rs +// carried: "the sequence refusal names the bypass, not the takeover" crates/batten/tests/it/claim.rs +// carried: "the bypass DOES clear a sequence refusal, so the two hatches stay distinct" crates/batten/tests/it/claim.rs +// carried: "narrowing the takeover does not break it: a competitor refusal still clears" crates/batten/tests/it/claim.rs +// carried: "the takeover does not silence the refusals — they are still reported" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause a — a MERGED pull request is a predecessor, not a competitor" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause a — the SAME payload without the state still refuses" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause b — a CLOSED unmerged pull request does not refuse either" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause b — the merged BOOLEAN alone is enough, without a state string" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause c — an OPEN pull request still refuses — the rule is not deleted" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause d — a malformed state refuses rather than reading as merged" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause d — the state is read case-insensitively, as the API spells it" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 clause e — a non-PR attachment carrying a state is still ignored" crates/batten/tests/it/claim.rs +// carried: "CLOUD-520 remedy — the refusal names the remedy, not merely the refusal" crates/batten/tests/it/claim.rs +// carried: "the minted receipt records the branch it was minted for" crates/batten/tests/it/claim.rs +// carried: "A RENAMED BRANCH RECOVERS ITS CLAIM WITH --adopt" crates/batten/tests/it/claim.rs +// carried: "WITHOUT --adopt the rename is still unrecovered — the recovery is opt-in" crates/batten/tests/it/claim.rs +// carried: "the adoption is recorded, never silent" crates/batten/tests/it/claim.rs +// carried: "a receipt whose branch still exists is not adopted" crates/batten/tests/it/claim.rs +// carried: "adopting onto a branch that already has a receipt is refused" crates/batten/tests/it/claim.rs +// carried: "a receipt with no branch line is not adoptable, never grandfathered" crates/batten/tests/it/claim.rs +// carried: "two orphans refuse and name both rather than guessing" crates/batten/tests/it/claim.rs +// carried: "--adopt-from picks one when two orphans are present" crates/batten/tests/it/claim.rs +// carried: "a detached HEAD has no name to adopt onto" crates/batten/tests/it/claim.rs +// carried: "THE TAKEOVER AS A FLAG: reachable where an env-var bypass is classified" crates/batten/tests/it/claim.rs +// carried: "an unknown flag is a usage error, not a silent pull" crates/batten/tests/it/claim.rs +// carried: "--adopt-from with no value is refused, and does not hang" crates/batten/tests/it/claim.rs +// carried: "--adopt-from with an empty value is refused rather than silently defaulted" crates/batten/tests/it/claim.rs //! //! SUBSUMED — the plumbing became the engine's, which is what a migration should //! produce. The base-absence case is here rather than above because this @@ -131,12 +131,12 @@ //! the flag half already carried the whole decision. `BATTEN_CLAIM_TAKEOVER` and //! `BATTEN_CLAIM_CHECK_BYPASS` are `--takeover` and `--bypass-sequence`. //! -// changed: "the flag and the env var record the identical line" crates/batten/tests/claim.rs the env var is gone rather than ported, so there is no second spelling for a receipt to record identically and the case describes a pair that no longer exists +// changed: "the flag and the env var record the identical line" crates/batten/tests/it/claim.rs the env var is gone rather than ported, so there is no second spelling for a receipt to record identically and the case describes a pair that no longer exists // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/claim_receipt.rs b/crates/batten/tests/it/claim_receipt.rs similarity index 99% rename from crates/batten/tests/claim_receipt.rs rename to crates/batten/tests/it/claim_receipt.rs index 6a4303d73..54e7e6e6d 100644 --- a/crates/batten/tests/claim_receipt.rs +++ b/crates/batten/tests/it/claim_receipt.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/cli.rs b/crates/batten/tests/it/cli.rs similarity index 99% rename from crates/batten/tests/cli.rs rename to crates/batten/tests/it/cli.rs index 2f23ee1a2..79c75381e 100644 --- a/crates/batten/tests/cli.rs +++ b/crates/batten/tests/it/cli.rs @@ -7,7 +7,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fmt::Write as _; use std::fs; @@ -4977,7 +4977,7 @@ const CENSUS_FLAGS: &[(&str, &[&str])] = &[ // receipt store, which a scratch fixture has no honest way to populate — a // hand-written baseline would be the fixture agreeing with the reader while // neither agrees with the writer, which is the class CLOUD-1121 measured. - // `crates/batten/tests/claim.rs` is where that predicate is exercised, over + // `crates/batten/tests/it/claim.rs` is where that predicate is exercised, over // a store the engine itself minted. ( "claim check", @@ -6067,7 +6067,7 @@ fn the_committed_portability_rules_fire_on_every_banned_shape() { // // Unlike that test, the banned literals CAN be written as source text here: // both globs are anchored at a first segment (`mise-tasks/`, `tests/`) that - // this file, at `crates/batten/tests/cli.rs`, does not sit under. + // this file, at `crates/batten/tests/it/cli.rs`, does not sit under. let committed = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../batten.toml"); let contents = fs::read_to_string(&committed).expect("read batten.toml"); diff --git a/crates/batten/tests/commit.rs b/crates/batten/tests/it/commit.rs similarity index 99% rename from crates/batten/tests/commit.rs rename to crates/batten/tests/it/commit.rs index 76cb1878e..b776497bc 100644 --- a/crates/batten/tests/commit.rs +++ b/crates/batten/tests/it/commit.rs @@ -13,7 +13,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/commit_admission.rs b/crates/batten/tests/it/commit_admission.rs similarity index 99% rename from crates/batten/tests/commit_admission.rs rename to crates/batten/tests/it/commit_admission.rs index dcb6d2a26..a4ed5d53f 100644 --- a/crates/batten/tests/commit_admission.rs +++ b/crates/batten/tests/it/commit_admission.rs @@ -31,7 +31,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/commit_meta_facts.rs b/crates/batten/tests/it/commit_meta_facts.rs similarity index 99% rename from crates/batten/tests/commit_meta_facts.rs rename to crates/batten/tests/it/commit_meta_facts.rs index a531170d8..fd7b99d2a 100644 --- a/crates/batten/tests/commit_meta_facts.rs +++ b/crates/batten/tests/it/commit_meta_facts.rs @@ -18,7 +18,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/common/mod.rs b/crates/batten/tests/it/common/mod.rs similarity index 100% rename from crates/batten/tests/common/mod.rs rename to crates/batten/tests/it/common/mod.rs diff --git a/crates/batten/tests/config_authority_boundary.rs b/crates/batten/tests/it/config_authority_boundary.rs similarity index 100% rename from crates/batten/tests/config_authority_boundary.rs rename to crates/batten/tests/it/config_authority_boundary.rs diff --git a/crates/batten/tests/config_base_ref_reading.rs b/crates/batten/tests/it/config_base_ref_reading.rs similarity index 99% rename from crates/batten/tests/config_base_ref_reading.rs rename to crates/batten/tests/it/config_base_ref_reading.rs index eea0c90b5..419d6ff01 100644 --- a/crates/batten/tests/config_base_ref_reading.rs +++ b/crates/batten/tests/it/config_base_ref_reading.rs @@ -20,7 +20,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/config_deprecations.rs b/crates/batten/tests/it/config_deprecations.rs similarity index 93% rename from crates/batten/tests/config_deprecations.rs rename to crates/batten/tests/it/config_deprecations.rs index 2bc563d55..b7c673a59 100644 --- a/crates/batten/tests/config_deprecations.rs +++ b/crates/batten/tests/it/config_deprecations.rs @@ -41,7 +41,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; @@ -53,8 +53,8 @@ use common::{batten, git_in, scratch}; // claim a conservation nobody checked. The suite's arm names its declared // `# subject:` too (CLOUD-1130), which this same delta retires. // -// carried: mise-tasks/config-deprecations.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/config_deprecations.rs -// carried: tests/config-deprecations.bats mise-tasks/config-deprecations.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/config_deprecations.rs +// carried: mise-tasks/config-deprecations.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/it/config_deprecations.rs +// carried: tests/config-deprecations.bats mise-tasks/config-deprecations.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/it/config_deprecations.rs // // CLOUD-908's case arms: every `@test` the retired suite declared. Seven carried // and one changed. Arms are suite-qualified because a case TITLE is not unique @@ -63,15 +63,15 @@ use common::{batten, git_in, scratch}; // whichever suite looked it up first (the resolution order `rules.rs` records at // `unconserved_cases`). // -// carried: "config-deprecations.bats::a schema that lost no key exits 0" crates/batten/tests/config_deprecations.rs -// carried: "config-deprecations.bats::an unannounced removal is reported rather than passed" crates/batten/tests/config_deprecations.rs -// carried: "config-deprecations.bats::no release tag is exit 3 rather than a clean pass" crates/batten/tests/config_deprecations.rs -// carried: "config-deprecations.bats::a tag carrying no published schema is exit 3 rather than a clean pass" crates/batten/tests/config_deprecations.rs -// carried: "config-deprecations.bats::the baseline is the newest tag by version order, not by creation time" crates/batten/tests/config_deprecations.rs -// carried: "config-deprecations.bats::output is pointer-only — no schema body echoed" crates/batten/tests/config_deprecations.rs -// carried: "config-deprecations.bats::the gate leaves the tree it judges unmodified" crates/batten/tests/config_deprecations.rs +// carried: "config-deprecations.bats::a schema that lost no key exits 0" crates/batten/tests/it/config_deprecations.rs +// carried: "config-deprecations.bats::an unannounced removal is reported rather than passed" crates/batten/tests/it/config_deprecations.rs +// carried: "config-deprecations.bats::no release tag is exit 3 rather than a clean pass" crates/batten/tests/it/config_deprecations.rs +// carried: "config-deprecations.bats::a tag carrying no published schema is exit 3 rather than a clean pass" crates/batten/tests/it/config_deprecations.rs +// carried: "config-deprecations.bats::the baseline is the newest tag by version order, not by creation time" crates/batten/tests/it/config_deprecations.rs +// carried: "config-deprecations.bats::output is pointer-only — no schema body echoed" crates/batten/tests/it/config_deprecations.rs +// carried: "config-deprecations.bats::the gate leaves the tree it judges unmodified" crates/batten/tests/it/config_deprecations.rs // -// changed: "config-deprecations.bats::this repo's own schema has lost no key since its last release — the gate on the real tree" crates/batten/tests/config_deprecations.rs the retired suite asserted exit 0 unconditionally over the real tree, which is only answerable in a clone that FETCHED TAGS — and a filtered or shallow clone has none, where the program itself exits 3. The case is carried as `this_repositorys_schema_has_lost_no_key_since_its_last_release`, which asserts the same verdict where a baseline resolves and asserts the could-not-look answer where none does, so it can no longer pass by having compared nothing +// changed: "config-deprecations.bats::this repo's own schema has lost no key since its last release — the gate on the real tree" crates/batten/tests/it/config_deprecations.rs the retired suite asserted exit 0 unconditionally over the real tree, which is only answerable in a clone that FETCHED TAGS — and a filtered or shallow clone has none, where the program itself exits 3. The case is carried as `this_repositorys_schema_has_lost_no_key_since_its_last_release`, which asserts the same verdict where a baseline resolves and asserts the could-not-look answer where none does, so it can no longer pass by having compared nothing /// The latest release tag by VERSION order, never by creation date: a re-cut tag /// would otherwise reorder the baseline. diff --git a/crates/batten/tests/config_epoch.rs b/crates/batten/tests/it/config_epoch.rs similarity index 99% rename from crates/batten/tests/config_epoch.rs rename to crates/batten/tests/it/config_epoch.rs index f026999c3..1ea13c740 100644 --- a/crates/batten/tests/config_epoch.rs +++ b/crates/batten/tests/it/config_epoch.rs @@ -17,7 +17,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/config_in_directory.rs b/crates/batten/tests/it/config_in_directory.rs similarity index 99% rename from crates/batten/tests/config_in_directory.rs rename to crates/batten/tests/it/config_in_directory.rs index 1cc56ecc4..d4c26f551 100644 --- a/crates/batten/tests/config_in_directory.rs +++ b/crates/batten/tests/it/config_in_directory.rs @@ -40,7 +40,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/config_lint.rs b/crates/batten/tests/it/config_lint.rs similarity index 99% rename from crates/batten/tests/config_lint.rs rename to crates/batten/tests/it/config_lint.rs index d6d397691..a9999f61b 100644 --- a/crates/batten/tests/config_lint.rs +++ b/crates/batten/tests/it/config_lint.rs @@ -12,7 +12,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/config_provenance.rs b/crates/batten/tests/it/config_provenance.rs similarity index 99% rename from crates/batten/tests/config_provenance.rs rename to crates/batten/tests/it/config_provenance.rs index 346f55a0e..f3bfeee27 100644 --- a/crates/batten/tests/config_provenance.rs +++ b/crates/batten/tests/it/config_provenance.rs @@ -16,7 +16,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeMap; use std::path::Path; diff --git a/crates/batten/tests/config_schema.rs b/crates/batten/tests/it/config_schema.rs similarity index 98% rename from crates/batten/tests/config_schema.rs rename to crates/batten/tests/it/config_schema.rs index d41c9b173..16c7797bc 100644 --- a/crates/batten/tests/config_schema.rs +++ b/crates/batten/tests/it/config_schema.rs @@ -15,7 +15,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::PathBuf; @@ -30,8 +30,8 @@ use common::{Fixture, at_root, batten, scratch}; // checked. The suite's arm names its declared `# subject:` too (CLOUD-1130), // which this same delta retires. // -// carried: mise-tasks/schema-check.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/config_schema.rs -// carried: tests/schema-check.bats mise-tasks/schema-check.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/config_schema.rs +// carried: mise-tasks/schema-check.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/it/config_schema.rs +// carried: tests/schema-check.bats mise-tasks/schema-check.sh crates/batten/src/config.rs kind:mechanism crates/batten/tests/it/config_schema.rs // // CLOUD-908's case arms: every `@test` the retired suite declared, all nine // carried. Arms are suite-qualified because a case TITLE is not unique across @@ -40,15 +40,15 @@ use common::{Fixture, at_root, batten, scratch}; // suite looked it up first (the resolution order `rules.rs` records at // `unconserved_cases`). // -// carried: "schema-check.bats::a committed schema matching the config types exits 0" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::a drifted override schema is reported with its own pointer" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::a missing override schema is reported rather than silently skipped" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::both surfaces are judged in one run, not just the first to fail" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::a drifted schema is reported with a pointer" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::a missing schema is reported rather than silently skipped" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::output is pointer-only — no schema body echoed" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::the gate leaves the tree it judges unmodified" crates/batten/tests/config_schema.rs -// carried: "schema-check.bats::this repo's committed schema matches its config types — the gate on the real tree" crates/batten/tests/config_schema.rs +// carried: "schema-check.bats::a committed schema matching the config types exits 0" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::a drifted override schema is reported with its own pointer" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::a missing override schema is reported rather than silently skipped" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::both surfaces are judged in one run, not just the first to fail" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::a drifted schema is reported with a pointer" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::a missing schema is reported rather than silently skipped" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::output is pointer-only — no schema body echoed" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::the gate leaves the tree it judges unmodified" crates/batten/tests/it/config_schema.rs +// carried: "schema-check.bats::this repo's committed schema matches its config types — the gate on the real tree" crates/batten/tests/it/config_schema.rs /// The schema as the binary derives it, parsed. fn derived_schema() -> serde_json::Value { diff --git a/crates/batten/tests/config_show.rs b/crates/batten/tests/it/config_show.rs similarity index 99% rename from crates/batten/tests/config_show.rs rename to crates/batten/tests/it/config_show.rs index 2c708034f..278d49983 100644 --- a/crates/batten/tests/config_show.rs +++ b/crates/batten/tests/it/config_show.rs @@ -20,7 +20,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeMap; use std::path::Path; diff --git a/crates/batten/tests/config_trust.rs b/crates/batten/tests/it/config_trust.rs similarity index 99% rename from crates/batten/tests/config_trust.rs rename to crates/batten/tests/it/config_trust.rs index c19e325b1..d53f5512b 100644 --- a/crates/batten/tests/config_trust.rs +++ b/crates/batten/tests/it/config_trust.rs @@ -17,7 +17,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/connector_allow_door.rs b/crates/batten/tests/it/connector_allow_door.rs similarity index 99% rename from crates/batten/tests/connector_allow_door.rs rename to crates/batten/tests/it/connector_allow_door.rs index c0a622f5c..f3b2a76c0 100644 --- a/crates/batten/tests/connector_allow_door.rs +++ b/crates/batten/tests/it/connector_allow_door.rs @@ -42,7 +42,7 @@ #![cfg(unix)] #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/connector_not_granted.rs b/crates/batten/tests/it/connector_not_granted.rs similarity index 98% rename from crates/batten/tests/connector_not_granted.rs rename to crates/batten/tests/it/connector_not_granted.rs index f3e5ad561..ce37d8fee 100644 --- a/crates/batten/tests/connector_not_granted.rs +++ b/crates/batten/tests/it/connector_not_granted.rs @@ -34,7 +34,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; @@ -42,7 +42,7 @@ use std::process::Output; use common::{batten, git_in, scratch, stderr, stdout, write}; /// The shipped predicate, never a copy of it. -const MODULE: &str = include_str!("../../../policy/connector-not-granted.rego"); +const MODULE: &str = include_str!("../../../../policy/connector-not-granted.rego"); /// Registers the shipped module over the dotfile, with the class it raises. /// diff --git a/crates/batten/tests/connector_verbs.rs b/crates/batten/tests/it/connector_verbs.rs similarity index 86% rename from crates/batten/tests/connector_verbs.rs rename to crates/batten/tests/it/connector_verbs.rs index 77007798f..fe44675fd 100644 --- a/crates/batten/tests/connector_verbs.rs +++ b/crates/batten/tests/it/connector_verbs.rs @@ -18,32 +18,32 @@ //! //! `tests/connector-verb-guard.bats`, eighteen cases, every one placed. //! -// carried: "connector-verb-guard.bats::a subscribe under the readable name is denied" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::send_later under the readable name is denied" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::create_trigger under the readable name is denied" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::a subscribe under a UUID server name is denied" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::send_later under a UUID server name is denied" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::an unsubscribe under the readable name is left undecided" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::an unsubscribe under a UUID server name is left undecided" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::the unsubscribe suffix is not swallowed by the subscribe suffix" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::a verb with no server prefix is still decided" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::a tool merely CONTAINING a decided verb is not decided" crates/batten/tests/connector_verbs.rs -// carried: "connector-verb-guard.bats::an unrelated tool gets no decision" crates/batten/tests/connector_verbs.rs +// carried: "connector-verb-guard.bats::a subscribe under the readable name is denied" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::send_later under the readable name is denied" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::create_trigger under the readable name is denied" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::a subscribe under a UUID server name is denied" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::send_later under a UUID server name is denied" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::an unsubscribe under the readable name is left undecided" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::an unsubscribe under a UUID server name is left undecided" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::the unsubscribe suffix is not swallowed by the subscribe suffix" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::a verb with no server prefix is still decided" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::a tool merely CONTAINING a decided verb is not decided" crates/batten/tests/it/connector_verbs.rs +// carried: "connector-verb-guard.bats::an unrelated tool gets no decision" crates/batten/tests/it/connector_verbs.rs // carried: "connector-verb-guard.bats::every deny rule in the committed settings names a covered suffix" tests/mcp-allow-check.bats //! //! SUBSUMED — the plumbing became the engine's, which is what a migration should //! produce. //! -// subsumed: "connector-verb-guard.bats::a payload with no tool_name gets no decision" crates/batten/tests/cli.rs -// subsumed: "connector-verb-guard.bats::unparseable stdin gets no decision rather than a deny" crates/batten/tests/cli.rs +// subsumed: "connector-verb-guard.bats::a payload with no tool_name gets no decision" crates/batten/tests/it/cli.rs +// subsumed: "connector-verb-guard.bats::unparseable stdin gets no decision rather than a deny" crates/batten/tests/it/cli.rs //! //! CHANGED — four, and three of them are one cause: the coverage flags are gone, //! because the fact they published is the engine's now (`batten policy tools`). //! -// changed: "connector-verb-guard.bats::--covers prints every suffix the guard decides, and nothing else" crates/batten/tests/cli.rs the guard's `--covers` is `batten policy tools`, which reads the committed rows rather than a script's `case` arms — one authority for the fact instead of two. Asserted over the real config in `policy_tools_names_every_mediated_selector` +// changed: "connector-verb-guard.bats::--covers prints every suffix the guard decides, and nothing else" crates/batten/tests/it/cli.rs the guard's `--covers` is `batten policy tools`, which reads the committed rows rather than a script's `case` arms — one authority for the fact instead of two. Asserted over the real config in `policy_tools_names_every_mediated_selector` // changed: "connector-verb-guard.bats::--covers-allow publishes the arm a connector control can override" tests/mcp-allow-check.bats the engine has no allow arm to publish: a row is `deny` or `warn`, so the set this flag existed to expose is empty BY CONSTRUCTION rather than by measurement. `mcp-allow-check` still probes any surviving guard for it, and its own stand-in case is what keeps that half exercised -// changed: "connector-verb-guard.bats::no suffix is published as pre-approved and denied at once" crates/batten/tests/cli.rs unconstructible now, for the reason above: with no allow arm there is no second set to contradict the deny set. The property it protected — one verdict per verb — is the rule table's own, since two rows cannot both select one tool and disagree about severity without `config-lint` reporting it -// changed: "connector-verb-guard.bats::the bypass silences every arm" crates/batten/tests/guardrail_bypass.rs BATTEN_CONNECTOR_VERB_BYPASS is gone; a mediated deny takes the engine's own hatch, the consolidation rows 1-3 and 6 record +// changed: "connector-verb-guard.bats::no suffix is published as pre-approved and denied at once" crates/batten/tests/it/cli.rs unconstructible now, for the reason above: with no allow arm there is no second set to contradict the deny set. The property it protected — one verdict per verb — is the rule table's own, since two rows cannot both select one tool and disagree about severity without `config-lint` reporting it +// changed: "connector-verb-guard.bats::the bypass silences every arm" crates/batten/tests/it/guardrail_bypass.rs BATTEN_CONNECTOR_VERB_BYPASS is gone; a mediated deny takes the engine's own hatch, the consolidation rows 1-3 and 6 record //! //! THE SURVIVING GATE'S OWN RENAME OWES AN ARM, for the reason row 3's block //! records: a renamed case is a deleted case to anything reading names. @@ -68,7 +68,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; @@ -76,7 +76,7 @@ use common::{Fixture, run_with_stdin, stderr}; /// This repository's own rows, as committed — never a fixture rewriting them. fn repo(name: &str) -> PathBuf { - let staged = Fixture::new(name).config(include_str!("../../../batten.toml")); + let staged = Fixture::new(name).config(include_str!("../../../../batten.toml")); let modules = staged.path().join("policy"); std::fs::create_dir_all(&modules).expect("the fixture's policy directory is creatable"); let committed = Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/batten/tests/contract_drift.rs b/crates/batten/tests/it/contract_drift.rs similarity index 95% rename from crates/batten/tests/contract_drift.rs rename to crates/batten/tests/it/contract_drift.rs index 6ae92ef9f..cc3c3eefd 100644 --- a/crates/batten/tests/contract_drift.rs +++ b/crates/batten/tests/it/contract_drift.rs @@ -16,7 +16,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::Path; use std::process::{Output, Stdio}; @@ -480,20 +480,20 @@ fn the_mediation_hatch_does_not_silence_the_advisory() { // closed above rather than filed, and both arms below point at the test that had // to be written to make the claim true. // -// carried: "the first call is the session's start: silent, and it writes a snapshot" crates/batten/tests/contract_drift.rs +// carried: "the first call is the session's start: silent, and it writes a snapshot" crates/batten/tests/it/contract_drift.rs // carried: "an unchanged surface produces no output" crates/batten/src/contract.rs kind:mechanism -// carried: "THE GAP: a modified AGENTS.md is reported, naming the file" crates/batten/tests/contract_drift.rs -// carried: "it names the event it was called on, so one body serves both wirings" crates/batten/tests/contract_drift.rs -// carried: "ONCE PER CHANGE-SET: the very next call is silent" crates/batten/tests/contract_drift.rs -// carried: "a SECOND change-set is reported again — quiet is not permanent" crates/batten/tests/contract_drift.rs -// carried: "a newly tracked contract file is drift" crates/batten/tests/contract_drift.rs -// carried: "a contract file that stopped being tracked is drift too" crates/batten/tests/contract_drift.rs -// carried: "a file outside the surface does not fire it" crates/batten/tests/contract_drift.rs -// carried: "each session gets its own snapshot, so a session that started AFTER the change is not nudged" crates/batten/tests/contract_drift.rs +// carried: "THE GAP: a modified AGENTS.md is reported, naming the file" crates/batten/tests/it/contract_drift.rs +// carried: "it names the event it was called on, so one body serves both wirings" crates/batten/tests/it/contract_drift.rs +// carried: "ONCE PER CHANGE-SET: the very next call is silent" crates/batten/tests/it/contract_drift.rs +// carried: "a SECOND change-set is reported again — quiet is not permanent" crates/batten/tests/it/contract_drift.rs +// carried: "a newly tracked contract file is drift" crates/batten/tests/it/contract_drift.rs +// carried: "a contract file that stopped being tracked is drift too" crates/batten/tests/it/contract_drift.rs +// carried: "a file outside the surface does not fire it" crates/batten/tests/it/contract_drift.rs +// carried: "each session gets its own snapshot, so a session that started AFTER the change is not nudged" crates/batten/tests/it/contract_drift.rs // carried: "a session id carrying path characters cannot escape the snapshot store" crates/batten/src/contract.rs kind:mechanism -// carried: "the reminder carries no byte of the changed file's content" crates/batten/tests/contract_drift.rs -// carried: "when settings.json moved it says a new hook may not be loaded in this session" crates/batten/tests/contract_drift.rs -// carried: "outside a checkout there is no surface to judge" crates/batten/tests/contract_drift.rs +// carried: "the reminder carries no byte of the changed file's content" crates/batten/tests/it/contract_drift.rs +// carried: "when settings.json moved it says a new hook may not be loaded in this session" crates/batten/tests/it/contract_drift.rs +// carried: "outside a checkout there is no surface to judge" crates/batten/tests/it/contract_drift.rs // // SUBSUMED — the plumbing the case tested became the engine's rather than the // script's, which is what a migration should produce. Each names the general @@ -501,13 +501,13 @@ fn the_mediation_hatch_does_not_silence_the_advisory() { // // subsumed: "the snapshot is one line per tracked contract file, hash and path" crates/batten/src/contract.rs kind:mechanism // subsumed: "a payload with no session_id still works, on a shared key" crates/batten/src/contract.rs kind:mechanism -// subsumed: "unparseable input fails open" crates/batten/tests/cli.rs -// subsumed: "empty input fails open" crates/batten/tests/cli.rs +// subsumed: "unparseable input fails open" crates/batten/tests/it/cli.rs +// subsumed: "empty input fails open" crates/batten/tests/it/cli.rs // subsumed: "it emits a count as well as the paths" crates/batten/src/contract.rs kind:mechanism -// subsumed: "the emitted document is the hook shape, and it parses" crates/batten/tests/advisory_drain.rs +// subsumed: "the emitted document is the hook shape, and it parses" crates/batten/tests/it/advisory_drain.rs // // CHANGED — behaviour that diverges deliberately. The bats suite asserted the // opposite of each of these, and nothing in the tree marked the change until now. // -// changed: "an untracked file under mise-tasks is not contract" crates/batten/tests/contract_drift.rs the surface is globs now, so a newly added gate IS the drift — `[epoch] tracked`'s literal paths structurally cannot see a file that postdates the list -// changed: "the bypass is honoured" crates/batten/tests/contract_drift.rs BATTEN_CONTRACT_DRIFT_BYPASS is gone and the engine's hatch deliberately does not reach an advisory: it carries no verdict and refuses nothing, so a switch over it would only suppress news +// changed: "an untracked file under mise-tasks is not contract" crates/batten/tests/it/contract_drift.rs the surface is globs now, so a newly added gate IS the drift — `[epoch] tracked`'s literal paths structurally cannot see a file that postdates the list +// changed: "the bypass is honoured" crates/batten/tests/it/contract_drift.rs BATTEN_CONTRACT_DRIFT_BYPASS is gone and the engine's hatch deliberately does not reach an advisory: it carries no verdict and refuses nothing, so a switch over it would only suppress news diff --git a/crates/batten/tests/decision_record.rs b/crates/batten/tests/it/decision_record.rs similarity index 99% rename from crates/batten/tests/decision_record.rs rename to crates/batten/tests/it/decision_record.rs index 8f90c13ab..e543b7b75 100644 --- a/crates/batten/tests/decision_record.rs +++ b/crates/batten/tests/it/decision_record.rs @@ -20,7 +20,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::Path; diff --git a/crates/batten/tests/defects.rs b/crates/batten/tests/it/defects.rs similarity index 99% rename from crates/batten/tests/defects.rs rename to crates/batten/tests/it/defects.rs index 13a5399c2..567e6aac1 100644 --- a/crates/batten/tests/defects.rs +++ b/crates/batten/tests/it/defects.rs @@ -13,7 +13,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/derived_facts.rs b/crates/batten/tests/it/derived_facts.rs similarity index 99% rename from crates/batten/tests/derived_facts.rs rename to crates/batten/tests/it/derived_facts.rs index e8336539a..8838c9f9e 100644 --- a/crates/batten/tests/derived_facts.rs +++ b/crates/batten/tests/it/derived_facts.rs @@ -13,7 +13,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use common::{Fixture, stderr, stdout}; diff --git a/crates/batten/tests/design_audit.rs b/crates/batten/tests/it/design_audit.rs similarity index 99% rename from crates/batten/tests/design_audit.rs rename to crates/batten/tests/it/design_audit.rs index 7851b62d3..3e3337949 100644 --- a/crates/batten/tests/design_audit.rs +++ b/crates/batten/tests/it/design_audit.rs @@ -20,7 +20,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/dev_profile.rs b/crates/batten/tests/it/dev_profile.rs similarity index 99% rename from crates/batten/tests/dev_profile.rs rename to crates/batten/tests/it/dev_profile.rs index 14b658efe..3c48c3411 100644 --- a/crates/batten/tests/dev_profile.rs +++ b/crates/batten/tests/it/dev_profile.rs @@ -53,7 +53,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; /// What the adopted arm sets for WORKSPACE code. Spelled as an integer because /// that is how cargo reads it; `debug = true` is `2` and `debug = false` is `0`, diff --git a/crates/batten/tests/doctor.rs b/crates/batten/tests/it/doctor.rs similarity index 99% rename from crates/batten/tests/doctor.rs rename to crates/batten/tests/it/doctor.rs index 90a4b5a8c..7abb694e3 100644 --- a/crates/batten/tests/doctor.rs +++ b/crates/batten/tests/it/doctor.rs @@ -14,7 +14,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/document_facts.rs b/crates/batten/tests/it/document_facts.rs similarity index 99% rename from crates/batten/tests/document_facts.rs rename to crates/batten/tests/it/document_facts.rs index cdc651ef4..d9f8266bd 100644 --- a/crates/batten/tests/document_facts.rs +++ b/crates/batten/tests/it/document_facts.rs @@ -12,7 +12,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::PathBuf; diff --git a/crates/batten/tests/document_read_count.rs b/crates/batten/tests/it/document_read_count.rs similarity index 99% rename from crates/batten/tests/document_read_count.rs rename to crates/batten/tests/it/document_read_count.rs index b43b1e346..d3402c9ba 100644 --- a/crates/batten/tests/document_read_count.rs +++ b/crates/batten/tests/it/document_read_count.rs @@ -17,7 +17,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/done_not_landed.rs b/crates/batten/tests/it/done_not_landed.rs similarity index 99% rename from crates/batten/tests/done_not_landed.rs rename to crates/batten/tests/it/done_not_landed.rs index b31ebc47a..dc435ebd0 100644 --- a/crates/batten/tests/done_not_landed.rs +++ b/crates/batten/tests/it/done_not_landed.rs @@ -16,7 +16,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/enforce_journal.rs b/crates/batten/tests/it/enforce_journal.rs similarity index 99% rename from crates/batten/tests/enforce_journal.rs rename to crates/batten/tests/it/enforce_journal.rs index f82ee3b7d..9fba4a7b8 100644 --- a/crates/batten/tests/enforce_journal.rs +++ b/crates/batten/tests/it/enforce_journal.rs @@ -21,7 +21,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/extension_surfaces.rs b/crates/batten/tests/it/extension_surfaces.rs similarity index 98% rename from crates/batten/tests/extension_surfaces.rs rename to crates/batten/tests/it/extension_surfaces.rs index cb04c6934..629ff68b0 100644 --- a/crates/batten/tests/extension_surfaces.rs +++ b/crates/batten/tests/it/extension_surfaces.rs @@ -21,7 +21,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::PathBuf; @@ -258,11 +258,11 @@ fn the_doc_states_pointer_only_as_a_law_over_every_surface_not_one_adapter() { // (rule 2). A stated law whose gate the reader cannot find is one they // cannot tell from an aspiration. assert!( - prose.contains("crates/batten/tests/pointer_only.rs"), + prose.contains("crates/batten/tests/it/pointer_only.rs"), "the doc must name the gate that decides the law it just stated" ); assert!( - at_root("crates/batten/tests/pointer_only.rs").exists(), + at_root("crates/batten/tests/it/pointer_only.rs").exists(), "and that gate must exist — a doc citing a deleted suite is the coverage-outlives-the- \ thing failure this file exists to catch" ); diff --git a/crates/batten/tests/external_facts.rs b/crates/batten/tests/it/external_facts.rs similarity index 99% rename from crates/batten/tests/external_facts.rs rename to crates/batten/tests/it/external_facts.rs index a82064dfd..5fc5a6d0b 100644 --- a/crates/batten/tests/external_facts.rs +++ b/crates/batten/tests/it/external_facts.rs @@ -31,7 +31,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/extracted_facts.rs b/crates/batten/tests/it/extracted_facts.rs similarity index 99% rename from crates/batten/tests/extracted_facts.rs rename to crates/batten/tests/it/extracted_facts.rs index fde77ce58..3d12c4e5a 100644 --- a/crates/batten/tests/extracted_facts.rs +++ b/crates/batten/tests/it/extracted_facts.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/facts.rs b/crates/batten/tests/it/facts.rs similarity index 100% rename from crates/batten/tests/facts.rs rename to crates/batten/tests/it/facts.rs diff --git a/crates/batten/tests/fail_on_warning.rs b/crates/batten/tests/it/fail_on_warning.rs similarity index 99% rename from crates/batten/tests/fail_on_warning.rs rename to crates/batten/tests/it/fail_on_warning.rs index f11d138c6..449d1e439 100644 --- a/crates/batten/tests/fail_on_warning.rs +++ b/crates/batten/tests/it/fail_on_warning.rs @@ -13,7 +13,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/filed_here.rs b/crates/batten/tests/it/filed_here.rs similarity index 92% rename from crates/batten/tests/filed_here.rs rename to crates/batten/tests/it/filed_here.rs index 6f352f40b..7921884a3 100644 --- a/crates/batten/tests/filed_here.rs +++ b/crates/batten/tests/it/filed_here.rs @@ -19,47 +19,47 @@ //! naming both a policy surface and a compiled-binary test, because either alone //! is satisfiable by a port that does nothing. //! -// carried: mise-tasks/filed-here-check.sh policy/filed-here.rego crates/batten/tests/filed_here.rs -// carried: tests/filed-here-check.bats policy/filed-here.rego crates/batten/tests/filed_here.rs +// carried: mise-tasks/filed-here-check.sh policy/filed-here.rego crates/batten/tests/it/filed_here.rs +// carried: tests/filed-here-check.bats policy/filed-here.rego crates/batten/tests/it/filed_here.rs //! //! # RETIREMENT LEDGER — `tests/filed-here-check.bats`, 47 cases //! //! CARRIED — the property survives, proved here or in the module's own suite. //! -// carried: "a create recorded with an unready verdict stops the lap, and the refusal names the id" crates/batten/tests/filed_here.rs +// carried: "a create recorded with an unready verdict stops the lap, and the refusal names the id" crates/batten/tests/it/filed_here.rs // carried: "the same row passes once its recorded verdict is green" policy/filed-here.rego // carried: "a groom recorded after the create supersedes it" policy/filed-here.rego // carried: "a later unready supersedes an earlier ready" policy/filed-here.rego -// carried: "superseding is per id: one row groomed leaves another's refusal standing" crates/batten/tests/filed_here.rs +// carried: "superseding is per id: one row groomed leaves another's refusal standing" crates/batten/tests/it/filed_here.rs // carried: "a recorded comment is never gated, whatever its verdict column says" policy/filed-here.rego // carried: "a create the recorder could not lint is not a refusal" policy/filed-here.rego -// carried: "one unrefined row among refined ones is reported, and only that one" crates/batten/tests/filed_here.rs -// carried: "a branch that filed nothing passes untouched" crates/batten/tests/filed_here.rs -// carried: "an empty record passes" crates/batten/tests/filed_here.rs -// carried: "a record belonging to another branch is not read" crates/batten/tests/filed_here.rs -// carried: "a branch name with a slash finds its record, matching the recorder's spelling" crates/batten/tests/filed_here.rs -// carried: "the refusal carries the id and no prose from the row" crates/batten/tests/filed_here.rs +// carried: "one unrefined row among refined ones is reported, and only that one" crates/batten/tests/it/filed_here.rs +// carried: "a branch that filed nothing passes untouched" crates/batten/tests/it/filed_here.rs +// carried: "an empty record passes" crates/batten/tests/it/filed_here.rs +// carried: "a record belonging to another branch is not read" crates/batten/tests/it/filed_here.rs +// carried: "a branch name with a slash finds its record, matching the recorder's spelling" crates/batten/tests/it/filed_here.rs +// carried: "the refusal carries the id and no prose from the row" crates/batten/tests/it/filed_here.rs // carried: "a malformed line is skipped rather than judged" policy/filed-here.rego -// carried: "a row naming a file this branch is changing stops the lap" crates/batten/tests/filed_here.rs +// carried: "a row naming a file this branch is changing stops the lap" crates/batten/tests/it/filed_here.rs // carried: "a row naming only untouched files passes" policy/filed-here.rego // carried: "a row the recorder could not measure passes" policy/filed-here.rego -// carried: "a four-field line predating the column is not refused" crates/batten/tests/filed_here.rs -// carried: "A ROW RECORDED BEFORE THIS BRANCH'S BASE IS NOT A PUNT OVER ITS DIFF" crates/batten/tests/filed_here.rs -// carried: "a row recorded after the base, whose §1 names the diff, still refuses" crates/batten/tests/filed_here.rs +// carried: "a four-field line predating the column is not refused" crates/batten/tests/it/filed_here.rs +// carried: "A ROW RECORDED BEFORE THIS BRANCH'S BASE IS NOT A PUNT OVER ITS DIFF" crates/batten/tests/it/filed_here.rs +// carried: "a row recorded after the base, whose §1 names the diff, still refuses" crates/batten/tests/it/filed_here.rs // carried: "A PATH CITED AS EVIDENCE IS NOT A CLAIM ON IT — §1 decides the subject" policy/filed-here.rego // carried: "a row whose §1 names the diff is refused even when it cites other paths too" policy/filed-here.rego -// carried: "a six-field record with no §1 column is judged exactly as before" crates/batten/tests/filed_here.rs -// carried: "every overlapping path is named, one pointer per line" crates/batten/tests/filed_here.rs +// carried: "a six-field record with no §1 column is judged exactly as before" crates/batten/tests/it/filed_here.rs +// carried: "every overlapping path is named, one pointer per line" crates/batten/tests/it/filed_here.rs // carried: "a row that is both unrefined and over the diff reports both" policy/filed-here.rego // carried: "a later reading with no overlap supersedes an earlier one" policy/filed-here.rego // carried: "and a later reading WITH an overlap supersedes a clean one" policy/filed-here.rego // carried: "a comment is never gated on the diff either" policy/filed-here.rego -// carried: "A ROW RECORDED BEFORE THE FILE WAS TOUCHED IS STILL CAUGHT" crates/batten/tests/filed_here.rs -// carried: "a recorded path the branch does not change is not reported" crates/batten/tests/filed_here.rs +// carried: "A ROW RECORDED BEFORE THE FILE WAS TOUCHED IS STILL CAUGHT" crates/batten/tests/it/filed_here.rs +// carried: "a recorded path the branch does not change is not reported" crates/batten/tests/it/filed_here.rs // carried: "a row naming only files this branch leaves alone passes" policy/filed-here.rego -// carried: "A ROW THE PR CLOSES IS EXEMPT — filing then fixing is the point, not the punt" crates/batten/tests/filed_here.rs -// carried: "closing a different row does not exempt this one" crates/batten/tests/filed_here.rs -// carried: "the diff refusal carries the id and one path and nothing else" crates/batten/tests/filed_here.rs policy/filed-here.rego +// carried: "A ROW THE PR CLOSES IS EXEMPT — filing then fixing is the point, not the punt" crates/batten/tests/it/filed_here.rs +// carried: "closing a different row does not exempt this one" crates/batten/tests/it/filed_here.rs +// carried: "the diff refusal carries the id and one path and nothing else" crates/batten/tests/it/filed_here.rs policy/filed-here.rego //! //! SUBSUMED — the plumbing became the engine's, which is what a migration should //! produce. Each names the general property that now covers it. @@ -76,10 +76,10 @@ //! CHANGED — behaviour that diverges deliberately, each with its reason. //! // changed: "filed-here-check.bats::the bypass is honoured" crates/batten/src/rules.rs kind:mechanism BATTEN_FILED_HERE_BYPASS is gone: this is a `[[rule]]` row now, so the engine's own hatch is the one switch, and a per-gate variable would be a second one nobody can find -// changed: "filed-here-check.bats::the override lets the diff refusal through" crates/batten/tests/admission.rs the override is an ISSUED admission rather than a variable somebody knows (CLOUD-1051), so the case moves to the suite that drives `batten override request` end to end -// changed: "filed-here-check.bats::the override records which rows it overrode" crates/batten/tests/admission.rs same cause: what an admission records is the store's property, asserted where the store is -// changed: "filed-here-check.bats::the override does not excuse an unrefined row" crates/batten/tests/admission.rs same cause, and the narrowing is structural now: an admission is keyed to one subject, so it cannot reach a second predicate at all -// changed: "filed-here-check.bats::the override records nothing when there was nothing to override" crates/batten/tests/admission.rs same cause: an unspent admission leaves the store untouched, which is the store's own case +// changed: "filed-here-check.bats::the override lets the diff refusal through" crates/batten/tests/it/admission.rs the override is an ISSUED admission rather than a variable somebody knows (CLOUD-1051), so the case moves to the suite that drives `batten override request` end to end +// changed: "filed-here-check.bats::the override records which rows it overrode" crates/batten/tests/it/admission.rs same cause: what an admission records is the store's property, asserted where the store is +// changed: "filed-here-check.bats::the override does not excuse an unrefined row" crates/batten/tests/it/admission.rs same cause, and the narrowing is structural now: an admission is keyed to one subject, so it cannot reach a second predicate at all +// changed: "filed-here-check.bats::the override records nothing when there was nothing to override" crates/batten/tests/it/admission.rs same cause: an unspent admission leaves the store untouched, which is the store's own case //! //! `BATTEN_FILED_HERE_BYPASS` and `BATTEN_FILED_HERE_OVERLAP` are **gone rather //! than ported**, which is the whole of CLOUD-1051's first half: a knowable @@ -102,7 +102,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/fixture_repos.rs b/crates/batten/tests/it/fixture_repos.rs similarity index 99% rename from crates/batten/tests/fixture_repos.rs rename to crates/batten/tests/it/fixture_repos.rs index 76af446ef..86e1bdc76 100644 --- a/crates/batten/tests/fixture_repos.rs +++ b/crates/batten/tests/it/fixture_repos.rs @@ -27,7 +27,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/forge_facts.rs b/crates/batten/tests/it/forge_facts.rs similarity index 99% rename from crates/batten/tests/forge_facts.rs rename to crates/batten/tests/it/forge_facts.rs index 6d0e132b3..4542c1653 100644 --- a/crates/batten/tests/forge_facts.rs +++ b/crates/batten/tests/it/forge_facts.rs @@ -14,7 +14,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/fuzz_corpus.rs b/crates/batten/tests/it/fuzz_corpus.rs similarity index 84% rename from crates/batten/tests/fuzz_corpus.rs rename to crates/batten/tests/it/fuzz_corpus.rs index c9bc19ac6..28a4ec368 100644 --- a/crates/batten/tests/fuzz_corpus.rs +++ b/crates/batten/tests/it/fuzz_corpus.rs @@ -24,11 +24,24 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] +// `fuzz/properties.rs` is `include!`d below and is SHARED with the `fuzz` crate, +// where its entry points must be `pub` for libFuzzer to reach them. Standalone +// this file was a crate root, so those items were reachable; inside the one +// grouped target (CLOUD-1210) it is a module, and they are not. The allowance +// belongs here rather than in the shared file, which cannot narrow them. +// +// `expect` rather than `allow`, per the workspace's own `unfulfilled_lint_expectations +// = "deny"`: if the include ever stops producing unreachable `pub` items, this +// line is red rather than quietly stale. +#![expect( + unreachable_pub, + reason = "the included fuzz properties are `pub` for the `fuzz` crate's libFuzzer entry points, and this file is a module rather than a crate root since CLOUD-1210" +)] use std::fs; use std::path::{Path, PathBuf}; -include!("../../../fuzz/properties.rs"); +include!("../../../../fuzz/properties.rs"); /// The fuzz tree, from this crate's manifest rather than `file!()` — the same /// resolution `tests/acceptance_corpus.rs` documents. diff --git a/crates/batten/tests/gh_guard.rs b/crates/batten/tests/it/gh_guard.rs similarity index 99% rename from crates/batten/tests/gh_guard.rs rename to crates/batten/tests/it/gh_guard.rs index 102e11684..37266dddf 100644 --- a/crates/batten/tests/gh_guard.rs +++ b/crates/batten/tests/it/gh_guard.rs @@ -73,7 +73,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/git_facts.rs b/crates/batten/tests/it/git_facts.rs similarity index 99% rename from crates/batten/tests/git_facts.rs rename to crates/batten/tests/it/git_facts.rs index 1b8503912..27c4d84c5 100644 --- a/crates/batten/tests/git_facts.rs +++ b/crates/batten/tests/it/git_facts.rs @@ -35,7 +35,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/glob_exclusion.rs b/crates/batten/tests/it/glob_exclusion.rs similarity index 99% rename from crates/batten/tests/glob_exclusion.rs rename to crates/batten/tests/it/glob_exclusion.rs index df36a321f..1b757e96f 100644 --- a/crates/batten/tests/glob_exclusion.rs +++ b/crates/batten/tests/it/glob_exclusion.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/guardrail_bypass.rs b/crates/batten/tests/it/guardrail_bypass.rs similarity index 99% rename from crates/batten/tests/guardrail_bypass.rs rename to crates/batten/tests/it/guardrail_bypass.rs index fc05b0def..4b2faef4c 100644 --- a/crates/batten/tests/guardrail_bypass.rs +++ b/crates/batten/tests/it/guardrail_bypass.rs @@ -20,7 +20,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/harness_grant.rs b/crates/batten/tests/it/harness_grant.rs similarity index 98% rename from crates/batten/tests/harness_grant.rs rename to crates/batten/tests/it/harness_grant.rs index 89ebd1b79..cce0e96dd 100644 --- a/crates/batten/tests/harness_grant.rs +++ b/crates/batten/tests/it/harness_grant.rs @@ -36,7 +36,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; @@ -44,7 +44,7 @@ use std::process::Output; use common::{batten, git_in, scratch, stderr, stdout, write}; /// The shipped predicate, never a copy of it. -const MODULE: &str = include_str!("../../../policy/harness-grant.rego"); +const MODULE: &str = include_str!("../../../../policy/harness-grant.rego"); /// Registers the shipped module over the dotfile, with the two classes it raises. /// diff --git a/crates/batten/tests/history_facts.rs b/crates/batten/tests/it/history_facts.rs similarity index 99% rename from crates/batten/tests/history_facts.rs rename to crates/batten/tests/it/history_facts.rs index 5f9783b8a..0aab0c635 100644 --- a/crates/batten/tests/history_facts.rs +++ b/crates/batten/tests/it/history_facts.rs @@ -16,7 +16,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/hk_fix_selection.rs b/crates/batten/tests/it/hk_fix_selection.rs similarity index 99% rename from crates/batten/tests/hk_fix_selection.rs rename to crates/batten/tests/it/hk_fix_selection.rs index 8a7930454..50ed7a112 100644 --- a/crates/batten/tests/hk_fix_selection.rs +++ b/crates/batten/tests/it/hk_fix_selection.rs @@ -39,7 +39,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/hook_profile.rs b/crates/batten/tests/it/hook_profile.rs similarity index 99% rename from crates/batten/tests/hook_profile.rs rename to crates/batten/tests/it/hook_profile.rs index 3c5915b26..a007943e1 100644 --- a/crates/batten/tests/hook_profile.rs +++ b/crates/batten/tests/it/hook_profile.rs @@ -52,7 +52,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/hook_worktree_root.rs b/crates/batten/tests/it/hook_worktree_root.rs similarity index 99% rename from crates/batten/tests/hook_worktree_root.rs rename to crates/batten/tests/it/hook_worktree_root.rs index 967514819..d2c60cc42 100644 --- a/crates/batten/tests/hook_worktree_root.rs +++ b/crates/batten/tests/it/hook_worktree_root.rs @@ -27,7 +27,7 @@ //! subdirectory of it — the case the launcher was actually written for. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::io::Write as _; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/identity_churn.rs b/crates/batten/tests/it/identity_churn.rs similarity index 99% rename from crates/batten/tests/identity_churn.rs rename to crates/batten/tests/it/identity_churn.rs index 22d539f88..164cdc3a4 100644 --- a/crates/batten/tests/identity_churn.rs +++ b/crates/batten/tests/it/identity_churn.rs @@ -25,7 +25,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeMap; use std::fs; diff --git a/crates/batten/tests/identity_precedence.rs b/crates/batten/tests/it/identity_precedence.rs similarity index 99% rename from crates/batten/tests/identity_precedence.rs rename to crates/batten/tests/it/identity_precedence.rs index e6e8b97df..fa50ff4c3 100644 --- a/crates/batten/tests/identity_precedence.rs +++ b/crates/batten/tests/it/identity_precedence.rs @@ -31,7 +31,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/init.rs b/crates/batten/tests/it/init.rs similarity index 99% rename from crates/batten/tests/init.rs rename to crates/batten/tests/it/init.rs index 8b28c4765..824850c18 100644 --- a/crates/batten/tests/init.rs +++ b/crates/batten/tests/it/init.rs @@ -9,7 +9,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/inverted_board_cases.rs b/crates/batten/tests/it/inverted_board_cases.rs similarity index 99% rename from crates/batten/tests/inverted_board_cases.rs rename to crates/batten/tests/it/inverted_board_cases.rs index f9933b1ec..fcf222c31 100644 --- a/crates/batten/tests/inverted_board_cases.rs +++ b/crates/batten/tests/it/inverted_board_cases.rs @@ -36,7 +36,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/issue_key.rs b/crates/batten/tests/it/issue_key.rs similarity index 99% rename from crates/batten/tests/issue_key.rs rename to crates/batten/tests/it/issue_key.rs index a71ba954d..be66b980e 100644 --- a/crates/batten/tests/issue_key.rs +++ b/crates/batten/tests/it/issue_key.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/judge_kind.rs b/crates/batten/tests/it/judge_kind.rs similarity index 99% rename from crates/batten/tests/judge_kind.rs rename to crates/batten/tests/it/judge_kind.rs index 4798143b5..bbaaab961 100644 --- a/crates/batten/tests/judge_kind.rs +++ b/crates/batten/tests/it/judge_kind.rs @@ -15,7 +15,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/it/main.rs b/crates/batten/tests/it/main.rs new file mode 100644 index 000000000..5342de395 --- /dev/null +++ b/crates/batten/tests/it/main.rs @@ -0,0 +1,188 @@ +//! The one integration test target (CLOUD-1210). +//! +//! # Why one target rather than 144 +//! +//! Cargo autodiscovers a test target per top-level `tests/*.rs`, and rustc +//! relinks the whole dependency closure — gix, regorus, syn, clap, jsonschema, +//! hyper/rustls — into each one. Measured on this container before this change: +//! `target/debug/deps` held **147 extension-less artifacts**, and a rebuild after +//! editing one `src/*.rs` spent roughly **144 targets x ~1.0s at 4-wide ~ 36s of +//! linking** out of 48s total. +//! +//! matklad states the defect and the remedy in *Delete Cargo Integration Tests*: +//! "rustc needs to repeatedly re-link the library crate with each of the +//! integration tests", and the recommended layout for a large codebase is exactly +//! this file plus one module per former file. Cargo's own repository made the +//! same move and measured test compile time down 3x and on-disk artifacts down +//! 5x. +//! +//! # What it does NOT change, said here because the row withdrew two over-claims +//! +//! Nothing a test can observe. nextest runs **each test in a separate process**, +//! so isolation is a property of the runner rather than of the target boundary — +//! `target_consolidation.rs` asserts that rather than resting on the citation. +//! It does not reduce the number of test PROCESSES, and it does not touch the run +//! phase, which is separately measured at 4.01x parallel efficiency on 4 cores. +//! This is a build-time and a bytes change, and nothing else. +//! +//! # Adding a test file +//! +//! Add it HERE, as a `mod` line, never as a new top-level `crates/batten/tests/*.rs` +//! — that would mint a second target and undo this. `policy/test-targets.rego` +//! refuses one, and `.claude/rules/toolchain.md`'s retirement shape now lands its +//! tier in this group. + +// Panicking on setup failure is the idiomatic way for a test to fail loudly, and +// the former per-file allowances are preserved on each module below. +#![allow(clippy::unwrap_used, clippy::expect_used)] + +mod common; + +mod acceptance_corpus; +mod acquisition_metric; +mod acquisition_sweep; +mod admission; +mod advisory_drain; +mod agent_facts; +mod ambient_authority; +mod attribution; +mod authority_replay; +mod baseline; +mod bats_invocation; +mod board_receipts; +mod board_record; +mod bundle; +mod bypass_scrub; +mod call_arguments; +mod call_background_flag; +mod call_ceiling; +mod capture_fidelity; +mod captured_facts; +mod checks_green; +mod ci_hygiene; +mod ci_parity; +mod ci_suite_lane; +mod claim; +mod claim_receipt; +mod cli; +mod commit; +mod commit_admission; +mod commit_meta_facts; +mod config_authority_boundary; +mod config_base_ref_reading; +mod config_deprecations; +mod config_epoch; +mod config_in_directory; +mod config_lint; +mod config_provenance; +mod config_schema; +mod config_show; +mod config_trust; +mod connector_allow_door; +mod connector_not_granted; +mod connector_verbs; +mod contract_drift; +mod decision_record; +mod defects; +mod derived_facts; +mod design_audit; +mod dev_profile; +mod doctor; +mod document_facts; +mod document_read_count; +mod done_not_landed; +mod enforce_journal; +mod extension_surfaces; +mod external_facts; +mod extracted_facts; +mod facts; +mod fail_on_warning; +mod filed_here; +mod fixture_repos; +mod forge_facts; +mod fuzz_corpus; +mod gh_guard; +mod git_facts; +mod glob_exclusion; +mod guardrail_bypass; +mod harness_grant; +mod history_facts; +mod hk_fix_selection; +mod hook_profile; +mod hook_worktree_root; +mod identity_churn; +mod identity_precedence; +mod init; +mod inverted_board_cases; +mod issue_key; +mod judge_kind; +mod mcp_dispatch; +mod mediated_admission; +mod mediated_verbs; +mod memories; +mod memory_injection; +mod mise_pin_agreement; +mod narrow_adoption; +mod perf_pair; +mod pinned_programs; +mod pipeline_shapes; +mod pointer_only; +mod policy_engine_count; +mod policy_input_narrowing; +mod policy_input_schema; +mod policy_presets; +mod policy_severity; +mod policy_test_suite; +mod policy_tree; +mod policy_whole_set; +mod pr_watch; +mod prebuilt_lint; +mod preset_segments; +mod primitives; +mod privileged_lane; +mod process_group; +mod prose_only; +mod prospective_facts; +mod provision; +mod ratchet; +mod ready; +mod reference_coverage; +mod remedy_authorship; +mod retirement_doctrine; +mod review_answered; +mod rule_cost_census; +mod rules_builtin_claims; +mod rules_drift; +mod run_shape; +mod run_shape_guard_door; +mod runner_verdict; +mod scanner_taxonomy; +mod secrets_kind; +mod semver_gate; +mod shell_retirement; +mod shell_write_advisory; +mod sinks; +mod skill_contract; +mod sleep_ban; +mod snapshots; +mod spawn_ceilings; +mod spawn_census; +mod staged_facts; +mod stop_posture; +mod submodule; +mod suite_subjects; +mod surface; +mod symbols; +mod target_prune; +mod task_prose; +mod task_receipt; +mod test_targets; +mod todo_promotion; +mod tool_selector; +mod tool_verdict_facts; +mod use_graph; +mod verdict_registry; +mod waivers; +mod walker; +mod wiring_reclaim; +mod zero_config; diff --git a/crates/batten/tests/mcp_dispatch.rs b/crates/batten/tests/it/mcp_dispatch.rs similarity index 99% rename from crates/batten/tests/mcp_dispatch.rs rename to crates/batten/tests/it/mcp_dispatch.rs index 300ef3630..8b7a4f0ae 100644 --- a/crates/batten/tests/mcp_dispatch.rs +++ b/crates/batten/tests/it/mcp_dispatch.rs @@ -38,7 +38,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; diff --git a/crates/batten/tests/mediated_admission.rs b/crates/batten/tests/it/mediated_admission.rs similarity index 99% rename from crates/batten/tests/mediated_admission.rs rename to crates/batten/tests/it/mediated_admission.rs index e1273f6a2..ce84ca9d7 100644 --- a/crates/batten/tests/mediated_admission.rs +++ b/crates/batten/tests/it/mediated_admission.rs @@ -24,7 +24,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/mediated_verbs.rs b/crates/batten/tests/it/mediated_verbs.rs similarity index 99% rename from crates/batten/tests/mediated_verbs.rs rename to crates/batten/tests/it/mediated_verbs.rs index 8367a96df..fcb2c1959 100644 --- a/crates/batten/tests/mediated_verbs.rs +++ b/crates/batten/tests/it/mediated_verbs.rs @@ -30,7 +30,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/memories.rs b/crates/batten/tests/it/memories.rs similarity index 99% rename from crates/batten/tests/memories.rs rename to crates/batten/tests/it/memories.rs index 72ca81de3..6c8b3bfdf 100644 --- a/crates/batten/tests/memories.rs +++ b/crates/batten/tests/it/memories.rs @@ -41,7 +41,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::Path; diff --git a/crates/batten/tests/memory_injection.rs b/crates/batten/tests/it/memory_injection.rs similarity index 99% rename from crates/batten/tests/memory_injection.rs rename to crates/batten/tests/it/memory_injection.rs index 00599ed32..7e833511e 100644 --- a/crates/batten/tests/memory_injection.rs +++ b/crates/batten/tests/it/memory_injection.rs @@ -38,7 +38,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/mise_pin_agreement.rs b/crates/batten/tests/it/mise_pin_agreement.rs similarity index 92% rename from crates/batten/tests/mise_pin_agreement.rs rename to crates/batten/tests/it/mise_pin_agreement.rs index ba84585fb..cbf807301 100644 --- a/crates/batten/tests/mise_pin_agreement.rs +++ b/crates/batten/tests/it/mise_pin_agreement.rs @@ -35,8 +35,8 @@ // from CLOUD-908's case arms below by construction: a case arm's first field // after the marker is a QUOTED case name, and a file arm's is a path. // -// carried: mise-tasks/mise-pin-agreement.sh policy/mise-pin-agreement.rego crates/batten/tests/mise_pin_agreement.rs -// carried: tests/mise-pin-agreement.bats policy/mise-pin-agreement.rego crates/batten/tests/mise_pin_agreement.rs +// carried: mise-tasks/mise-pin-agreement.sh policy/mise-pin-agreement.rego crates/batten/tests/it/mise_pin_agreement.rs +// carried: tests/mise-pin-agreement.bats policy/mise-pin-agreement.rego crates/batten/tests/it/mise_pin_agreement.rs // THE REPLAY DECLARATION (CLOUD-909), beside the mapping because the two // describe one migration and a translation in a second file is a second @@ -55,7 +55,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; @@ -218,8 +218,8 @@ fn clean(root: &Path) { // module that refuses everything. // --------------------------------------------------------------------------- -// carried: "a scoped launch whose version matches mise.toml passes" crates/batten/tests/mise_pin_agreement.rs -// carried: "a shimmed launch that IS scoped passes, and its pin is still read" crates/batten/tests/mise_pin_agreement.rs +// carried: "a scoped launch whose version matches mise.toml passes" crates/batten/tests/it/mise_pin_agreement.rs +// carried: "a shimmed launch that IS scoped passes, and its pin is still read" crates/batten/tests/it/mise_pin_agreement.rs #[test] fn a_scoped_launch_whose_version_matches_the_authority_passes() { // The two bats cases collapse into one here because they were already one @@ -238,7 +238,7 @@ fn a_scoped_launch_whose_version_matches_the_authority_passes() { // The refusals. // --------------------------------------------------------------------------- -// carried: "a version .mcp.json names that mise.toml does not pin fails, naming both" crates/batten/tests/mise_pin_agreement.rs +// carried: "a version .mcp.json names that mise.toml does not pin fails, naming both" crates/batten/tests/it/mise_pin_agreement.rs #[test] fn a_version_the_authority_pins_differently_is_refused() { let root = fixture( @@ -248,7 +248,7 @@ fn a_version_the_authority_pins_differently_is_refused() { denied(&root); } -// carried: "a tool mise.toml does not carry at all fails" crates/batten/tests/mise_pin_agreement.rs +// carried: "a tool mise.toml does not carry at all fails" crates/batten/tests/it/mise_pin_agreement.rs #[test] fn a_tool_the_authority_does_not_carry_is_refused() { let root = fixture( @@ -275,7 +275,7 @@ fn a_tool_the_authority_does_not_carry_is_refused() { // both readers, and this is CLOUD-1037's "two readers disagree on its grammar" // reaching the tree arm. Spelled for `conserves`, which is the deny gate on the // landing path; recorded on CLOUD-1115, which owns the other reader. -// carried: "a bare \`mise exec\` fails even though it names no version to compare" crates/batten/tests/mise_pin_agreement.rs +// carried: "a bare \`mise exec\` fails even though it names no version to compare" crates/batten/tests/it/mise_pin_agreement.rs #[test] fn a_bare_exec_is_refused_even_though_it_names_no_version() { let root = fixture( @@ -294,7 +294,7 @@ fn a_bare_exec_is_refused_even_though_it_names_no_version() { // THE SELECTOR IS ARGV, NOT THE COMMAND NAME (CLOUD-714). Keying the scoped-exec // check on `command == "mise"` would have made every shimmed server exempt — the // gate green while the property it exists for went unchecked. -// carried: "A SHIMMED LAUNCH IS STILL CHECKED — the selector is argv, not the command name" crates/batten/tests/mise_pin_agreement.rs +// carried: "A SHIMMED LAUNCH IS STILL CHECKED — the selector is argv, not the command name" crates/batten/tests/it/mise_pin_agreement.rs #[test] fn a_shimmed_bare_exec_is_still_refused() { let root = fixture( @@ -314,7 +314,7 @@ fn a_shimmed_bare_exec_is_still_refused() { // The boundaries, which is where a gate nobody can keep green comes from. // --------------------------------------------------------------------------- -// carried: "a server not launched through mise is left alone" crates/batten/tests/mise_pin_agreement.rs +// carried: "a server not launched through mise is left alone" crates/batten/tests/it/mise_pin_agreement.rs #[test] fn a_server_not_launched_through_mise_is_left_alone() { let root = fixture( @@ -330,7 +330,7 @@ fn a_server_not_launched_through_mise_is_left_alone() { clean(&root); } -// carried: "a missing .mcp.json is nothing to check" crates/batten/tests/mise_pin_agreement.rs +// carried: "a missing .mcp.json is nothing to check" crates/batten/tests/it/mise_pin_agreement.rs #[test] fn an_absent_server_manifest_is_nothing_to_check() { // A tree with no server manifest has nothing to check — the bash's own @@ -361,7 +361,7 @@ fn an_absent_server_manifest_is_nothing_to_check() { // // Not shipped red, and not shipped asserting the current behaviour either — that // would bake the defect in as the contract and go green forever, which is exactly -// what `crates/batten/tests/privileged_lane.rs` records for the same channel. The +// what `crates/batten/tests/it/privileged_lane.rs` records for the same channel. The // anti-vacuity partner ("no manifest either, so stay silent") goes with it: with // the channel dead both inputs are silent, so it would pass against a module that // decides nothing. @@ -404,8 +404,8 @@ fn a_table_valued_pin_reads_as_undeclared() { // above, `input.tree.missing` is empty for an absent declared path. So this case // diverges twice over — once by contract, once because the channel is unfilled — // and both reasons are on the row. -// changed: "a missing mise.toml cannot be compared against — exit 2" crates/batten/tests/mise_pin_agreement.rs the shell's exit 2 is could-not-look and the engine's 2 is the policy verdict (house-style §7), so the code cannot be carried through an identity; and the successor clause `V-PIN-AUTHORITY-UNREADABLE` is unreachable today because `input.tree.missing` is never populated for an absent declared path — measured here, recorded on CLOUD-1049, which owns restoring the case +// changed: "a missing mise.toml cannot be compared against — exit 2" crates/batten/tests/it/mise_pin_agreement.rs the shell's exit 2 is could-not-look and the engine's 2 is the policy verdict (house-style §7), so the code cannot be carried through an identity; and the successor clause `V-PIN-AUTHORITY-UNREADABLE` is unreachable today because `input.tree.missing` is never populated for an absent declared path — measured here, recorded on CLOUD-1049, which owns restoring the case // // THE SAME CHANNEL, THE OTHER INPUT. An unparseable `.mcp.json` is today // indistinguishable from an absent one and is silent, where the bash exited 2. -// changed: "an unparseable .mcp.json is exit 2, never a clean pass" crates/batten/tests/mise_pin_agreement.rs CLOUD-1049: `input.tree.missing` is not populated for a declared document that exists and fails to parse, so the could-not-look clause the module already carries cannot see this input. Not shipped red and not shipped asserting the current behaviour; CLOUD-1049 owns restoring it here +// changed: "an unparseable .mcp.json is exit 2, never a clean pass" crates/batten/tests/it/mise_pin_agreement.rs CLOUD-1049: `input.tree.missing` is not populated for a declared document that exists and fails to parse, so the could-not-look clause the module already carries cannot see this input. Not shipped red and not shipped asserting the current behaviour; CLOUD-1049 owns restoring it here diff --git a/crates/batten/tests/narrow_adoption.rs b/crates/batten/tests/it/narrow_adoption.rs similarity index 95% rename from crates/batten/tests/narrow_adoption.rs rename to crates/batten/tests/it/narrow_adoption.rs index e9114b058..ea2d0f86a 100644 --- a/crates/batten/tests/narrow_adoption.rs +++ b/crates/batten/tests/it/narrow_adoption.rs @@ -16,13 +16,13 @@ //! forbids the edge `hook -> fetch` over the RESOLVED `use` graph, which is a //! `deny` in `batten check` rather than a text match; //! * *never multi-thread, never `tokio::signal`* — -//! `crates/batten/tests/spawn_census.rs` reads the `tokio` feature list out of +//! `crates/batten/tests/it/spawn_census.rs` reads the `tokio` feature list out of //! the manifest, so both are compile errors rather than lint findings; -//! * *the lock is still `fs4`* — `crates/batten/tests/bundle.rs` asserts the +//! * *the lock is still `fs4`* — `crates/batten/tests/it/bundle.rs` asserts the //! behaviour the choice was made for, that a `SIGKILL`ed writer leaves a //! reader a defined answer; //! * *`tree_files` is byte-identical across runs* — -//! `crates/batten/tests/walker.rs`. +//! `crates/batten/tests/it/walker.rs`. //! //! What was left with no sensor is the drain clause, and this file is that //! sensor. It is a **text** assertion over `exec.rs`, which is the weakest of @@ -35,7 +35,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/perf_pair.rs b/crates/batten/tests/it/perf_pair.rs similarity index 98% rename from crates/batten/tests/perf_pair.rs rename to crates/batten/tests/it/perf_pair.rs index 668c697ff..25687315c 100644 --- a/crates/batten/tests/perf_pair.rs +++ b/crates/batten/tests/it/perf_pair.rs @@ -39,8 +39,8 @@ // The file granularity: each deleted path, and the two successors that hold what // it held. // -// changed: mise-tasks/perf-pair.sh crates/batten/src/perf.rs kind:verb crates/batten/tests/perf_pair.rs -// changed: tests/perf-pair.bats crates/batten/src/perf.rs kind:verb crates/batten/tests/perf_pair.rs +// changed: mise-tasks/perf-pair.sh crates/batten/src/perf.rs kind:verb crates/batten/tests/it/perf_pair.rs +// changed: tests/perf-pair.bats crates/batten/src/perf.rs kind:verb crates/batten/tests/it/perf_pair.rs // // The case granularity. Six of the eleven were TEXT ASSERTIONS OVER SHELL — they // grepped the task's own source for a spelling — and a Rust port does not merely @@ -60,7 +60,7 @@ // pair every one of them — but the assertion moved from counting `^pair ` lines // in a shell file to the plan the module builds. // -// carried: "every path perf-assert budgets is paired here" crates/batten/tests/perf_pair.rs +// carried: "every path perf-assert budgets is paired here" crates/batten/tests/it/perf_pair.rs // // The worktree recovery, and this pair is the most interesting entry in the // ledger. Both cases guarded a MEASURED defect (2026-08-14): `git worktree add` @@ -88,7 +88,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use common::{Fixture, run, stdout}; diff --git a/crates/batten/tests/pinned_programs.rs b/crates/batten/tests/it/pinned_programs.rs similarity index 99% rename from crates/batten/tests/pinned_programs.rs rename to crates/batten/tests/it/pinned_programs.rs index 3438fbf65..2f21bbaec 100644 --- a/crates/batten/tests/pinned_programs.rs +++ b/crates/batten/tests/it/pinned_programs.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeSet; use std::path::{Path, PathBuf}; @@ -41,7 +41,7 @@ use common::{Fixture, run_with_stdin, stderr}; /// one this repository actually enables. fn repo(name: &str) -> PathBuf { let staged = Fixture::new(name) - .config(include_str!("../../../batten.toml")) + .config(include_str!("../../../../batten.toml")) .file("mise.toml", "[tools]\njq = \"1.7\"\n"); // The in-repo modules that config registers, copied by ENUMERATION rather // than by name — `board_receipts.rs`'s reasoning, and naming them would put a diff --git a/crates/batten/tests/pipeline_shapes.rs b/crates/batten/tests/it/pipeline_shapes.rs similarity index 99% rename from crates/batten/tests/pipeline_shapes.rs rename to crates/batten/tests/it/pipeline_shapes.rs index 721c8e499..a60bbcd4c 100644 --- a/crates/batten/tests/pipeline_shapes.rs +++ b/crates/batten/tests/it/pipeline_shapes.rs @@ -26,7 +26,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/pointer_only.rs b/crates/batten/tests/it/pointer_only.rs similarity index 99% rename from crates/batten/tests/pointer_only.rs rename to crates/batten/tests/it/pointer_only.rs index 5ccc488f3..559161fb1 100644 --- a/crates/batten/tests/pointer_only.rs +++ b/crates/batten/tests/it/pointer_only.rs @@ -49,7 +49,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::io::Write as _; use std::path::PathBuf; @@ -922,7 +922,7 @@ const CENSUS: &[Verb] = &[ // command line off somebody's home directory, so every byte it reports is a // count plus the harness and event to look under — not a path, and not even // the offending command's basename. The at-load record it writes obeys the - // same rule, which `crates/batten/tests/wiring_reclaim.rs` asserts over the + // same rule, which `crates/batten/tests/it/wiring_reclaim.rs` asserts over the // file itself. // // Driven with `-n`, which is the only invocation that reads the surfaces and diff --git a/crates/batten/tests/policy_engine_count.rs b/crates/batten/tests/it/policy_engine_count.rs similarity index 100% rename from crates/batten/tests/policy_engine_count.rs rename to crates/batten/tests/it/policy_engine_count.rs diff --git a/crates/batten/tests/policy_input_narrowing.rs b/crates/batten/tests/it/policy_input_narrowing.rs similarity index 100% rename from crates/batten/tests/policy_input_narrowing.rs rename to crates/batten/tests/it/policy_input_narrowing.rs diff --git a/crates/batten/tests/policy_input_schema.rs b/crates/batten/tests/it/policy_input_schema.rs similarity index 100% rename from crates/batten/tests/policy_input_schema.rs rename to crates/batten/tests/it/policy_input_schema.rs diff --git a/crates/batten/tests/policy_presets.rs b/crates/batten/tests/it/policy_presets.rs similarity index 99% rename from crates/batten/tests/policy_presets.rs rename to crates/batten/tests/it/policy_presets.rs index 493806c36..c84556ea0 100644 --- a/crates/batten/tests/policy_presets.rs +++ b/crates/batten/tests/it/policy_presets.rs @@ -45,7 +45,7 @@ fn preset_row(id: &str, preset: &str) -> Rule { /// The whitespace split here is NOT a second tokenizer and must not grow into /// one: every command below is a single unquoted element, where splitting on /// spaces and `hook::segments` agree by inspection. Anything with a quote or a -/// list operator belongs in `crates/batten/tests/preset_segments.rs`, which +/// list operator belongs in `crates/batten/tests/it/preset_segments.rs`, which /// drives the real projection through the compiled binary over a real envelope /// — the tier `.claude/rules/policy-modules.md` says a `with input as` case /// cannot stand in for. diff --git a/crates/batten/tests/policy_severity.rs b/crates/batten/tests/it/policy_severity.rs similarity index 99% rename from crates/batten/tests/policy_severity.rs rename to crates/batten/tests/it/policy_severity.rs index a51d5dcc5..45bef6dd8 100644 --- a/crates/batten/tests/policy_severity.rs +++ b/crates/batten/tests/it/policy_severity.rs @@ -36,7 +36,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::io::Write as _; diff --git a/crates/batten/tests/policy_test_suite.rs b/crates/batten/tests/it/policy_test_suite.rs similarity index 99% rename from crates/batten/tests/policy_test_suite.rs rename to crates/batten/tests/it/policy_test_suite.rs index ae951b03d..a72ae0733 100644 --- a/crates/batten/tests/policy_test_suite.rs +++ b/crates/batten/tests/it/policy_test_suite.rs @@ -39,7 +39,7 @@ use batten::facts::Look; use batten::policy::{self, Suite}; use batten::rules::Rule; -mod common; +use crate::common; use common::{Fixture, run, stdout}; diff --git a/crates/batten/tests/policy_tree.rs b/crates/batten/tests/it/policy_tree.rs similarity index 99% rename from crates/batten/tests/policy_tree.rs rename to crates/batten/tests/it/policy_tree.rs index 623be5fb4..e8e330662 100644 --- a/crates/batten/tests/policy_tree.rs +++ b/crates/batten/tests/it/policy_tree.rs @@ -18,7 +18,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/policy_whole_set.rs b/crates/batten/tests/it/policy_whole_set.rs similarity index 100% rename from crates/batten/tests/policy_whole_set.rs rename to crates/batten/tests/it/policy_whole_set.rs diff --git a/crates/batten/tests/pr_watch.rs b/crates/batten/tests/it/pr_watch.rs similarity index 99% rename from crates/batten/tests/pr_watch.rs rename to crates/batten/tests/it/pr_watch.rs index 5384dc7f0..85a1c8ae6 100644 --- a/crates/batten/tests/pr_watch.rs +++ b/crates/batten/tests/it/pr_watch.rs @@ -28,8 +28,8 @@ // and its suite are separate subjects, and one arm covering both would claim a // conservation nobody checked. // -// carried: mise-tasks/ci-wait.sh crates/batten/src/pr_watch.rs kind:verb crates/batten/tests/pr_watch.rs -// carried: tests/ci-wait.bats crates/batten/src/pr_watch.rs kind:verb crates/batten/tests/pr_watch.rs +// carried: mise-tasks/ci-wait.sh crates/batten/src/pr_watch.rs kind:verb crates/batten/tests/it/pr_watch.rs +// carried: tests/ci-wait.bats crates/batten/src/pr_watch.rs kind:verb crates/batten/tests/it/pr_watch.rs // // CLOUD-908's case arms: every `@test` the retired suite declared. Nine carried // and four changed, and each change is a SEAM the port moved rather than a @@ -53,7 +53,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/prebuilt_lint.rs b/crates/batten/tests/it/prebuilt_lint.rs similarity index 95% rename from crates/batten/tests/prebuilt_lint.rs rename to crates/batten/tests/it/prebuilt_lint.rs index 740aa2f4c..161dd1006 100644 --- a/crates/batten/tests/prebuilt_lint.rs +++ b/crates/batten/tests/it/prebuilt_lint.rs @@ -30,7 +30,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; @@ -122,7 +122,7 @@ fn workflow_with(root: &Path, step: &str) { // The successor file is named in the reason rather than the target, because a case // may carry exactly one arm. // -// carried: "this repository is clean today — the rule is green on the tree it governs" batten.toml the two rows now run over this checkout from crates/batten/tests/prebuilt_lint.rs, one rule set at a time rather than the whole config in a fixture +// carried: "this repository is clean today — the rule is green on the tree it governs" batten.toml the two rows now run over this checkout from crates/batten/tests/it/prebuilt_lint.rs, one rule set at a time rather than the whole config in a fixture #[test] fn this_repository_is_clean_today() { // The half that a narrowed pattern, or a rule matching nothing, would also @@ -141,14 +141,14 @@ fn this_repository_is_clean_today() { // `no-source-built-tool`: the mistake, and the shape that is not it. // --------------------------------------------------------------------------- -// carried: "a cargo: backend in mise.toml is a violation, named and located" crates/batten/tests/prebuilt_lint.rs +// carried: "a cargo: backend in mise.toml is a violation, named and located" crates/batten/tests/it/prebuilt_lint.rs #[test] fn a_cargo_backend_in_the_manifest_is_a_violation_named_and_located() { let root = tools_with("cargo-backend", "\"cargo:cargo-hack\" = \"0.6\""); assert_eq!(findings(&root), vec!["mise.toml:3 no-source-built-tool"]); } -// carried: "a prebuilt backend is not a violation — the rule bans compiling, not installing" crates/batten/tests/prebuilt_lint.rs +// carried: "a prebuilt backend is not a violation — the rule bans compiling, not installing" crates/batten/tests/it/prebuilt_lint.rs #[test] fn a_prebuilt_backend_is_not_a_violation() { // The other direction, and the one that keeps the rule from reading as "no @@ -165,7 +165,7 @@ fn a_prebuilt_backend_is_not_a_violation() { // `no-cargo-install-in-ci`: the same pair, spelled by hand in a workflow step. // --------------------------------------------------------------------------- -// carried: "cargo install in a workflow is a violation" crates/batten/tests/prebuilt_lint.rs +// carried: "cargo install in a workflow is a violation" crates/batten/tests/it/prebuilt_lint.rs #[test] fn cargo_install_in_a_workflow_is_a_violation() { let root = tools_with("workflow-cargo-install", "hk = \"1.54.0\""); @@ -176,7 +176,7 @@ fn cargo_install_in_a_workflow_is_a_violation() { ); } -// carried: "a prebuilt install-action step is not a violation" crates/batten/tests/prebuilt_lint.rs +// carried: "a prebuilt install-action step is not a violation" crates/batten/tests/it/prebuilt_lint.rs #[test] fn a_prebuilt_install_action_step_is_not_a_violation() { let root = tools_with("workflow-prebuilt", "hk = \"1.54.0\""); @@ -195,9 +195,9 @@ fn a_prebuilt_install_action_step_is_not_a_violation() { // the policy surface named is the code that decides a `forbid` row. Converting the // rows to a `policy` module would be a larger change than the shape asks for. // -// carried: tests/prebuilt-lint.bats crates/batten/src/rules.rs kind:mechanism crates/batten/tests/prebuilt_lint.rs +// carried: tests/prebuilt-lint.bats crates/batten/src/rules.rs kind:mechanism crates/batten/tests/it/prebuilt_lint.rs // // The four waiver cases are NOT here: they were about the waiver SURFACE, generic -// over which `forbid` row it suppresses, and `crates/batten/tests/waivers.rs` +// over which `forbid` row it suppresses, and `crates/batten/tests/it/waivers.rs` // already drives the compiled binary over exactly that. Their arms sit there. // --------------------------------------------------------------------------- diff --git a/crates/batten/tests/preset_segments.rs b/crates/batten/tests/it/preset_segments.rs similarity index 98% rename from crates/batten/tests/preset_segments.rs rename to crates/batten/tests/it/preset_segments.rs index e81f66cc7..000822db6 100644 --- a/crates/batten/tests/preset_segments.rs +++ b/crates/batten/tests/it/preset_segments.rs @@ -22,7 +22,7 @@ //! drift a corpus over the real config exists to catch. //! //! **The refusal's ATTRIBUTION is asserted, never just the exit code**, and that -//! is the lesson `crates/batten/tests/run_shape_guard_door.rs`'s header records: this +//! is the lesson `crates/batten/tests/it/run_shape_guard_door.rs`'s header records: this //! repository's own rows refuse commands in the same family, so an exit 2 alone //! would let some other row's verdict stand in for the preset's — coverage that //! has stopped testing the thing it names. @@ -30,7 +30,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/primitives.rs b/crates/batten/tests/it/primitives.rs similarity index 99% rename from crates/batten/tests/primitives.rs rename to crates/batten/tests/it/primitives.rs index 284b0ce47..d6f5030dd 100644 --- a/crates/batten/tests/primitives.rs +++ b/crates/batten/tests/it/primitives.rs @@ -14,7 +14,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::cell::Cell; use std::fs; diff --git a/crates/batten/tests/privileged_lane.rs b/crates/batten/tests/it/privileged_lane.rs similarity index 97% rename from crates/batten/tests/privileged_lane.rs rename to crates/batten/tests/it/privileged_lane.rs index 1d37660ae..c951e3043 100644 --- a/crates/batten/tests/privileged_lane.rs +++ b/crates/batten/tests/it/privileged_lane.rs @@ -29,12 +29,12 @@ // reader can match the other's shape, which is what lets one marker carry two // ledgers without a second convention. // -// carried: tests/privileged-lane.bats policy/privileged-lane.rego crates/batten/tests/privileged_lane.rs +// carried: tests/privileged-lane.bats policy/privileged-lane.rego crates/batten/tests/it/privileged_lane.rs // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; @@ -124,7 +124,7 @@ fn clean(root: &Path) { ); } -// carried: "a bot lane selecting by branch prefix is denied" crates/batten/tests/privileged_lane.rs +// carried: "a bot lane selecting by branch prefix is denied" crates/batten/tests/it/privileged_lane.rs #[test] fn a_bot_lane_selecting_by_branch_prefix_is_denied() { // The defect CLOUD-867 was filed for: the head is chosen by a string the PR @@ -139,7 +139,7 @@ fn a_bot_lane_selecting_by_branch_prefix_is_denied() { denied(&root); } -// carried: "the same lane testing the head origin is clean" crates/batten/tests/privileged_lane.rs +// carried: "the same lane testing the head origin is clean" crates/batten/tests/it/privileged_lane.rs #[test] fn the_same_lane_testing_the_head_origin_is_clean() { // The discriminating half. Same trigger, same grant, same job — only the @@ -154,7 +154,7 @@ fn the_same_lane_testing_the_head_origin_is_clean() { clean(&root); } -// carried: "a scheduled writer that resolves no outside head is not a subject" crates/batten/tests/privileged_lane.rs +// carried: "a scheduled writer that resolves no outside head is not a subject" crates/batten/tests/it/privileged_lane.rs #[test] fn a_scheduled_writer_that_resolves_no_outside_head_is_not_a_subject() { // THE FALSE POSITIVE THE THIRD CONJUNCT EXISTS FOR: `perf.yml` is scheduled, @@ -171,7 +171,7 @@ fn a_scheduled_writer_that_resolves_no_outside_head_is_not_a_subject() { clean(&root); } -// carried: "an outsider-reachable writer that resolves no outside head is not a subject" crates/batten/tests/privileged_lane.rs +// carried: "an outsider-reachable writer that resolves no outside head is not a subject" crates/batten/tests/it/privileged_lane.rs #[test] fn an_outsider_reachable_writer_that_resolves_no_outside_head_is_not_a_subject() { // THE CASE THAT ACTUALLY DISCRIMINATES THE THIRD CONJUNCT. It exists because @@ -199,7 +199,7 @@ fn an_outsider_reachable_writer_that_resolves_no_outside_head_is_not_a_subject() clean(&root); } -// carried: "a read-only lane is not a subject" crates/batten/tests/privileged_lane.rs +// carried: "a read-only lane is not a subject" crates/batten/tests/it/privileged_lane.rs #[test] fn a_read_only_lane_is_not_a_subject() { let root = fixture( diff --git a/crates/batten/tests/process_group.rs b/crates/batten/tests/it/process_group.rs similarity index 99% rename from crates/batten/tests/process_group.rs rename to crates/batten/tests/it/process_group.rs index 26a55ba6c..61ad49d49 100644 --- a/crates/batten/tests/process_group.rs +++ b/crates/batten/tests/it/process_group.rs @@ -21,7 +21,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::os::unix::process::ExitStatusExt as _; diff --git a/crates/batten/tests/prose_only.rs b/crates/batten/tests/it/prose_only.rs similarity index 92% rename from crates/batten/tests/prose_only.rs rename to crates/batten/tests/it/prose_only.rs index 7599e9913..da88110e3 100644 --- a/crates/batten/tests/prose_only.rs +++ b/crates/batten/tests/it/prose_only.rs @@ -30,13 +30,13 @@ // CLOUD-908's case arms below by construction: a case arm's first field after the // marker is a QUOTED case name, and a file arm's is a path. // -// carried: mise-tasks/prose-only-check.sh policy/prose-only.rego crates/batten/tests/prose_only.rs -// carried: tests/prose-only-check.bats policy/prose-only.rego crates/batten/tests/prose_only.rs +// carried: mise-tasks/prose-only-check.sh policy/prose-only.rego crates/batten/tests/it/prose_only.rs +// carried: tests/prose-only-check.bats policy/prose-only.rego crates/batten/tests/it/prose_only.rs // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; @@ -149,7 +149,7 @@ const CODE: &str = "// a doc line\nfn main() {\n let x = 1;\n let y = 2;\n // module that fires on nothing. // --------------------------------------------------------------------------- -// carried: "a comment-only diff with no test change is refused" crates/batten/tests/prose_only.rs +// carried: "a comment-only diff with no test change is refused" crates/batten/tests/it/prose_only.rs #[test] fn a_branch_whose_whole_diff_is_comment_lines_is_refused() { // The measured instance CLOUD-827 was filed for: two rewritten sentences of @@ -168,7 +168,7 @@ fn a_branch_whose_whole_diff_is_comment_lines_is_refused() { refused(&root); } -// carried: "a comment change plus any code line is admitted" crates/batten/tests/prose_only.rs +// carried: "a comment change plus any code line is admitted" crates/batten/tests/it/prose_only.rs #[test] fn one_changed_line_of_code_admits_the_branch() { // The discriminating half of the case above. Same file, same comment edit, @@ -188,7 +188,7 @@ fn one_changed_line_of_code_admits_the_branch() { admitted(&root); } -// carried: "a comment change plus a test change is admitted — the PR #604 shape" crates/batten/tests/prose_only.rs +// carried: "a comment change plus a test change is admitted — the PR #604 shape" crates/batten/tests/it/prose_only.rs #[test] fn a_comment_change_plus_a_test_change_is_admitted() { // The conjunct that makes doc work possible rather than obstructed: the @@ -222,7 +222,7 @@ fn a_comment_change_plus_a_test_change_is_admitted() { // remainders rather than diff lines. // --------------------------------------------------------------------------- -// carried: "a shell comment counts as prose, and code in the same file does not" crates/batten/tests/prose_only.rs +// carried: "a shell comment counts as prose, and code in the same file does not" crates/batten/tests/it/prose_only.rs #[test] fn a_block_of_code_moved_within_a_file_is_not_a_code_change() { // THE FALSE POSITIVE THE LINE CLASSIFIER PRODUCED. Reordering two statements @@ -249,7 +249,7 @@ fn a_block_of_code_moved_within_a_file_is_not_a_code_change() { admitted(&root); } -// carried: "a reflowed comment block with blank lines is still prose" crates/batten/tests/prose_only.rs +// carried: "a reflowed comment block with blank lines is still prose" crates/batten/tests/it/prose_only.rs #[test] fn a_comment_reflowed_across_a_line_boundary_is_still_prose_only() { // The other direction, and the one that cost a matrix. Rewrapping a doc @@ -279,7 +279,7 @@ fn a_comment_reflowed_across_a_line_boundary_is_still_prose_only() { // Deletions, which the shell dropped wholesale and could not tell apart. // --------------------------------------------------------------------------- -// changed: "a deleted file is not read as a comment change" crates/batten/tests/prose_only.rs the shell excluded every deletion wholesale because it could not classify one; the engine compares remainders, so a module deletion is still refused while a pure-prose deletion is admitted +// changed: "a deleted file is not read as a comment change" crates/batten/tests/it/prose_only.rs the shell excluded every deletion wholesale because it could not classify one; the engine compares remainders, so a module deletion is still refused while a pure-prose deletion is admitted #[test] fn deleting_a_module_is_not_prose_only() { // The case `--diff-filter=d` existed to protect against, stated in the @@ -297,7 +297,7 @@ fn deleting_a_module_is_not_prose_only() { admitted(&root); } -// changed: "a .md-only diff is refused — the whole file is prose" crates/batten/tests/prose_only.rs the shell could only reach this for an EDITED markdown file; the deletion half was excluded, and this case is the half that exclusion cost +// changed: "a .md-only diff is refused — the whole file is prose" crates/batten/tests/it/prose_only.rs the shell could only reach this for an EDITED markdown file; the deletion half was excluded, and this case is the half that exclusion cost #[test] fn deleting_a_pure_prose_file_is_prose_only() { // The half the blanket exclusion cost. Deleting a `.md` file IS a prose @@ -318,7 +318,7 @@ fn deleting_a_pure_prose_file_is_prose_only() { // The admitting direction on everything it cannot classify. // --------------------------------------------------------------------------- -// carried: "an unrecognised extension admits the branch" crates/batten/tests/prose_only.rs +// carried: "an unrecognised extension admits the branch" crates/batten/tests/it/prose_only.rs #[test] fn an_unrecognised_extension_admits_the_branch() { // The failure direction is deliberate and is the shell's: this gate spends @@ -337,7 +337,7 @@ fn an_unrecognised_extension_admits_the_branch() { admitted(&root); } -// carried: "an empty diff is not judged" crates/batten/tests/prose_only.rs +// carried: "an empty diff is not judged" crates/batten/tests/it/prose_only.rs #[test] fn an_empty_branch_is_not_a_subject() { // Refusing one would fire on every freshly-cut branch before a line is @@ -353,7 +353,7 @@ fn an_empty_branch_is_not_a_subject() { admitted(&root); } -// carried: "no base to diff against is not judged, rather than refused" crates/batten/tests/prose_only.rs +// carried: "no base to diff against is not judged, rather than refused" crates/batten/tests/it/prose_only.rs #[test] fn an_unresolvable_base_says_nothing_rather_than_refusing() { // COULD-NOT-LOOK IS NOT A VERDICT, and here the vacuous direction would be a @@ -370,7 +370,7 @@ fn an_unresolvable_base_says_nothing_rather_than_refusing() { admitted(&root); } -// subsumed: "a Rust block comment is NOT read as prose" crates/batten/tests/prose_only.rs +// subsumed: "a Rust block comment is NOT read as prose" crates/batten/tests/it/prose_only.rs #[test] fn a_shell_program_carrying_no_extension_is_read_as_shell() { // `mise-tasks/` programs carry no extension (CLOUD-865 renamed most to @@ -394,7 +394,7 @@ fn a_shell_program_carrying_no_extension_is_read_as_shell() { refused(&root); } -// changed: "the refusal names paths and a count, never a line of the diff" crates/batten/tests/prose_only.rs the shell printed every changed path beside the count; the port emits the count alone, because a diff is content nobody has published +// changed: "the refusal names paths and a count, never a line of the diff" crates/batten/tests/it/prose_only.rs the shell printed every changed path beside the count; the port emits the count alone, because a diff is content nobody has published #[test] fn the_finding_carries_a_count_and_never_a_path() { // Non-negotiable rule 4, and it does real work here: a diff is content @@ -455,7 +455,7 @@ fn the_finding_carries_a_count_and_never_a_path() { // // The override is no longer an environment variable, so there is nothing here to // assert about one: it is an issued, content-addressed, single-use record, and -// `crates/batten/tests/admission.rs` is where every clause of it is tested — +// `crates/batten/tests/it/admission.rs` is where every clause of it is tested — // including the recording half, which is now the record's existence rather than // an append to a log. // @@ -463,7 +463,7 @@ fn the_finding_carries_a_count_and_never_a_path() { // `V-PROSE-ONLY-DIFF`'s declared `R-BATCH-IT` route, and `verdict::validate` // refuses a class that declares no route at all — so "the refusal names // something to run" stopped being a property of this gate's message and became a -// property of the registry. `crates/batten/tests/verdict_registry.rs` holds it. +// property of the registry. `crates/batten/tests/it/verdict_registry.rs` holds it. // -// subsumed: "the override admits the branch and records which one it admitted" crates/batten/tests/admission.rs -// subsumed: "the remedy names where the content should go, not merely a flag" crates/batten/tests/verdict_registry.rs +// subsumed: "the override admits the branch and records which one it admitted" crates/batten/tests/it/admission.rs +// subsumed: "the remedy names where the content should go, not merely a flag" crates/batten/tests/it/verdict_registry.rs diff --git a/crates/batten/tests/prospective_facts.rs b/crates/batten/tests/it/prospective_facts.rs similarity index 99% rename from crates/batten/tests/prospective_facts.rs rename to crates/batten/tests/it/prospective_facts.rs index d5086cfb1..051a394ba 100644 --- a/crates/batten/tests/prospective_facts.rs +++ b/crates/batten/tests/it/prospective_facts.rs @@ -18,7 +18,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::Path; use std::process::{Output, Stdio}; diff --git a/crates/batten/tests/provision.rs b/crates/batten/tests/it/provision.rs similarity index 99% rename from crates/batten/tests/provision.rs rename to crates/batten/tests/it/provision.rs index 0aef7d426..ccc9ad905 100644 --- a/crates/batten/tests/provision.rs +++ b/crates/batten/tests/it/provision.rs @@ -14,7 +14,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/ratchet.rs b/crates/batten/tests/it/ratchet.rs similarity index 99% rename from crates/batten/tests/ratchet.rs rename to crates/batten/tests/it/ratchet.rs index 20b06d55c..904680e7c 100644 --- a/crates/batten/tests/ratchet.rs +++ b/crates/batten/tests/it/ratchet.rs @@ -14,7 +14,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/ready.rs b/crates/batten/tests/it/ready.rs similarity index 96% rename from crates/batten/tests/ready.rs rename to crates/batten/tests/it/ready.rs index 3cbdb80c4..23140e663 100644 --- a/crates/batten/tests/ready.rs +++ b/crates/batten/tests/it/ready.rs @@ -55,98 +55,98 @@ //! //! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads //! -// carried: mise-tasks/ready-lint.sh crates/batten/src/ready.rs kind:verb crates/batten/tests/ready.rs -// carried: tests/ready-lint.bats crates/batten/src/ready.rs kind:verb crates/batten/tests/ready.rs +// carried: mise-tasks/ready-lint.sh crates/batten/src/ready.rs kind:verb crates/batten/tests/it/ready.rs +// carried: tests/ready-lint.bats crates/batten/src/ready.rs kind:verb crates/batten/tests/it/ready.rs //! //! # RETIREMENT LEDGER — `tests/ready-lint.bats`, 80 cases //! //! CARRIED — the property survives, proved here against the engine. //! -// carried: "a well-formed block passes" crates/batten/tests/ready.rs -// carried: "omitted clauses are not a violation" crates/batten/tests/ready.rs -// carried: "a blocker cited in §8 with no relation is reported" crates/batten/tests/ready.rs -// carried: "the same citation passes when the relation actually exists" crates/batten/tests/ready.rs -// carried: "a blocker noted as closed still needs its relation" crates/batten/tests/ready.rs -// carried: "a blocker noted as closed passes when the relation is there, which it is" crates/batten/tests/ready.rs -// carried: "the body's cited keys are emitted before any verdict" crates/batten/tests/ready.rs -// carried: "THE §6 DECLARATION IS EMITTED, and none reaches the consumer as one token" crates/batten/tests/ready.rs -// carried: "a releasable type is emitted as what it declares, not as none" crates/batten/tests/ready.rs -// carried: "a body with no §6 clause emits no bump line at all — did not say is not none" crates/batten/tests/ready.rs -// carried: "an unrefined body still emits its cited keys" crates/batten/tests/ready.rs -// carried: "a body citing nothing emits the line and no keys" crates/batten/tests/ready.rs -// carried: "the §8 span's keys are emitted as their own set" crates/batten/tests/ready.rs -// carried: "§8 None is an explicit, valid answer" crates/batten/tests/ready.rs -// carried: "a relatedTo mention on the §8 line is not a claim" crates/batten/tests/ready.rs -// carried: "a house-style (§6) cross-reference is not the commit clause" crates/batten/tests/ready.rs -// carried: "§6 none is an explicit, valid no-commit declaration" crates/batten/tests/ready.rs -// carried: "a closed blocker in Linear's rendered-mention form is judged like any other" crates/batten/tests/ready.rs -// carried: "a rendered-mention blockedBy claim without a relation is still flagged" crates/batten/tests/ready.rs -// carried: "a cross-reference after the claim sentence is not a claim" crates/batten/tests/ready.rs -// carried: "feat to patch agrees below 0.1.0" crates/batten/tests/ready.rs -// carried: "a bump promising the retired arrow is reported below 0.1.0" crates/batten/tests/ready.rs -// carried: "a breaking change promising major is reported below 0.1.0" crates/batten/tests/ready.rs -// carried: "a breaking change declaring patch agrees below 0.1.0" crates/batten/tests/ready.rs -// carried: "a §6 clause denying a break is not read as declaring one" crates/batten/tests/ready.rs -// carried: "the marker on the type token still declares a break" crates/batten/tests/ready.rs -// carried: "a BREAKING CHANGE footer still declares a break" crates/batten/tests/ready.rs -// carried: "a §6 clause denying a break without naming a surface is refused" crates/batten/tests/ready.rs -// carried: "a denial qualified as consumer-facing passes" crates/batten/tests/ready.rs -// carried: "a denial qualified as the library API passes" crates/batten/tests/ready.rs -// carried: "a §6 clause making no breakage claim is untouched" crates/batten/tests/ready.rs -// carried: "CLOUD-832's §6 as written reproduces the refusal" crates/batten/tests/ready.rs -// carried: "the refusal carries no prose from the clause" crates/batten/tests/ready.rs -// carried: "a no-bump type does not collapse to patch below 0.1.0" crates/batten/tests/ready.rs -// carried: "an earlier code span whose prefix spells a type is not the declared type" crates/batten/tests/ready.rs -// carried: "the verdict follows the declared type, not the prefix that precedes it" crates/batten/tests/ready.rs -// carried: "a coincidental prefix no longer decides an honest no-bump line" crates/batten/tests/ready.rs -// carried: "a scoped commit type is still recognised" crates/batten/tests/ready.rs -// carried: "a disagreeing declaration beside a code span is still refused" crates/batten/tests/ready.rs -// carried: "the arrows fire again at 0.1.0 and above" crates/batten/tests/ready.rs -// carried: "patch under a released version is the disagreement" crates/batten/tests/ready.rs -// carried: "an unreadable workspace version exits 2, not a guessed verdict" crates/batten/tests/ready.rs -// carried: "an issue with no §6 clause needs no workspace version" crates/batten/tests/ready.rs -// carried: "a §6 clause naming no commit type is reported" crates/batten/tests/ready.rs -// carried: "an open-questions marker blocks Ready" crates/batten/tests/ready.rs -// carried: "the retired (clause N) dialect is reported, not silently accepted" crates/batten/tests/ready.rs -// carried: "an issue with no Ready block at all is reported" crates/batten/tests/ready.rs -// carried: "a parent's refinement-gate heading is a Ready block" crates/batten/tests/ready.rs -// carried: "a deeper refinement-gate heading is a Ready block too" crates/batten/tests/ready.rs -// carried: "clauses inside a parent block are still checked" crates/batten/tests/ready.rs -// carried: "a parent's §8 claim is held to the board like a leaf's" crates/batten/tests/ready.rs -// carried: "prose merely discussing refinement is not a Ready block" crates/batten/tests/ready.rs -// carried: "unparseable stdin exits 2, not 1" crates/batten/tests/ready.rs -// carried: "output is pointer-only — no issue prose echoed" crates/batten/tests/ready.rs -// carried: "a blocker claimed under a §8 HEADING with no relation is reported" crates/batten/tests/ready.rs -// carried: "the same claim with the relation present passes" crates/batten/tests/ready.rs -// carried: "a §8 heading claiming nothing is not a violation" crates/batten/tests/ready.rs -// carried: "the span stops at the next heading, so a later section is not §8 text" crates/batten/tests/ready.rs -// carried: "the span stops at the paragraph end, so a following paragraph is not the claim" crates/batten/tests/ready.rs -// carried: "a block that is only a refinement note carries no clause and is reported" crates/batten/tests/ready.rs -// carried: "a house-style cross-reference in prose does not satisfy the floor" crates/batten/tests/ready.rs -// carried: "a block carrying only §1 clears the floor — it is a floor, not a checklist" crates/batten/tests/ready.rs -// carried: "a heading-form label counts as a clause" crates/batten/tests/ready.rs -// carried: "a clause-free parent block is exempt from the floor" crates/batten/tests/ready.rs -// carried: "the non-canonical ready opener is reported, not treated as no block" crates/batten/tests/ready.rs -// carried: "a non-canonical opener still has its content judged" crates/batten/tests/ready.rs -// carried: "a payload with no description is exit 2 naming the field, never a verdict" crates/batten/tests/ready.rs -// carried: "a payload carrying only the declared field set is judged on its merits" crates/batten/tests/ready.rs -// carried: "(a) no relations key is a gap, never blocker-cited-without-relation" crates/batten/tests/ready.rs -// carried: "(b) relations present and empty is an answer, so the citation still reports" crates/batten/tests/ready.rs -// carried: "(c) relations present and carrying the cited id passes" crates/batten/tests/ready.rs -// carried: "(d) a judgeable violation outranks the gap: exit 1, not 2" crates/batten/tests/ready.rs -// carried: "the deferral rule has the same gap, and it reached further" crates/batten/tests/ready.rs -// carried: "a missing key costs nothing when the block cites nothing" crates/batten/tests/ready.rs -// carried: "a §7 introducing a deny gate with no replay is refused" crates/batten/tests/ready.rs -// carried: "a deny gate that reports its replay passes" crates/batten/tests/ready.rs -// carried: "a block declaring warn is not gated" crates/batten/tests/ready.rs -// carried: "a fenced [[rule]] at deny is a gate introduction too" crates/batten/tests/ready.rs -// carried: "a block introducing no gate is untouched by the replay clause" crates/batten/tests/ready.rs -// carried: "the deny-without-replay report carries no line of the block" crates/batten/tests/ready.rs +// carried: "a well-formed block passes" crates/batten/tests/it/ready.rs +// carried: "omitted clauses are not a violation" crates/batten/tests/it/ready.rs +// carried: "a blocker cited in §8 with no relation is reported" crates/batten/tests/it/ready.rs +// carried: "the same citation passes when the relation actually exists" crates/batten/tests/it/ready.rs +// carried: "a blocker noted as closed still needs its relation" crates/batten/tests/it/ready.rs +// carried: "a blocker noted as closed passes when the relation is there, which it is" crates/batten/tests/it/ready.rs +// carried: "the body's cited keys are emitted before any verdict" crates/batten/tests/it/ready.rs +// carried: "THE §6 DECLARATION IS EMITTED, and none reaches the consumer as one token" crates/batten/tests/it/ready.rs +// carried: "a releasable type is emitted as what it declares, not as none" crates/batten/tests/it/ready.rs +// carried: "a body with no §6 clause emits no bump line at all — did not say is not none" crates/batten/tests/it/ready.rs +// carried: "an unrefined body still emits its cited keys" crates/batten/tests/it/ready.rs +// carried: "a body citing nothing emits the line and no keys" crates/batten/tests/it/ready.rs +// carried: "the §8 span's keys are emitted as their own set" crates/batten/tests/it/ready.rs +// carried: "§8 None is an explicit, valid answer" crates/batten/tests/it/ready.rs +// carried: "a relatedTo mention on the §8 line is not a claim" crates/batten/tests/it/ready.rs +// carried: "a house-style (§6) cross-reference is not the commit clause" crates/batten/tests/it/ready.rs +// carried: "§6 none is an explicit, valid no-commit declaration" crates/batten/tests/it/ready.rs +// carried: "a closed blocker in Linear's rendered-mention form is judged like any other" crates/batten/tests/it/ready.rs +// carried: "a rendered-mention blockedBy claim without a relation is still flagged" crates/batten/tests/it/ready.rs +// carried: "a cross-reference after the claim sentence is not a claim" crates/batten/tests/it/ready.rs +// carried: "feat to patch agrees below 0.1.0" crates/batten/tests/it/ready.rs +// carried: "a bump promising the retired arrow is reported below 0.1.0" crates/batten/tests/it/ready.rs +// carried: "a breaking change promising major is reported below 0.1.0" crates/batten/tests/it/ready.rs +// carried: "a breaking change declaring patch agrees below 0.1.0" crates/batten/tests/it/ready.rs +// carried: "a §6 clause denying a break is not read as declaring one" crates/batten/tests/it/ready.rs +// carried: "the marker on the type token still declares a break" crates/batten/tests/it/ready.rs +// carried: "a BREAKING CHANGE footer still declares a break" crates/batten/tests/it/ready.rs +// carried: "a §6 clause denying a break without naming a surface is refused" crates/batten/tests/it/ready.rs +// carried: "a denial qualified as consumer-facing passes" crates/batten/tests/it/ready.rs +// carried: "a denial qualified as the library API passes" crates/batten/tests/it/ready.rs +// carried: "a §6 clause making no breakage claim is untouched" crates/batten/tests/it/ready.rs +// carried: "CLOUD-832's §6 as written reproduces the refusal" crates/batten/tests/it/ready.rs +// carried: "the refusal carries no prose from the clause" crates/batten/tests/it/ready.rs +// carried: "a no-bump type does not collapse to patch below 0.1.0" crates/batten/tests/it/ready.rs +// carried: "an earlier code span whose prefix spells a type is not the declared type" crates/batten/tests/it/ready.rs +// carried: "the verdict follows the declared type, not the prefix that precedes it" crates/batten/tests/it/ready.rs +// carried: "a coincidental prefix no longer decides an honest no-bump line" crates/batten/tests/it/ready.rs +// carried: "a scoped commit type is still recognised" crates/batten/tests/it/ready.rs +// carried: "a disagreeing declaration beside a code span is still refused" crates/batten/tests/it/ready.rs +// carried: "the arrows fire again at 0.1.0 and above" crates/batten/tests/it/ready.rs +// carried: "patch under a released version is the disagreement" crates/batten/tests/it/ready.rs +// carried: "an unreadable workspace version exits 2, not a guessed verdict" crates/batten/tests/it/ready.rs +// carried: "an issue with no §6 clause needs no workspace version" crates/batten/tests/it/ready.rs +// carried: "a §6 clause naming no commit type is reported" crates/batten/tests/it/ready.rs +// carried: "an open-questions marker blocks Ready" crates/batten/tests/it/ready.rs +// carried: "the retired (clause N) dialect is reported, not silently accepted" crates/batten/tests/it/ready.rs +// carried: "an issue with no Ready block at all is reported" crates/batten/tests/it/ready.rs +// carried: "a parent's refinement-gate heading is a Ready block" crates/batten/tests/it/ready.rs +// carried: "a deeper refinement-gate heading is a Ready block too" crates/batten/tests/it/ready.rs +// carried: "clauses inside a parent block are still checked" crates/batten/tests/it/ready.rs +// carried: "a parent's §8 claim is held to the board like a leaf's" crates/batten/tests/it/ready.rs +// carried: "prose merely discussing refinement is not a Ready block" crates/batten/tests/it/ready.rs +// carried: "unparseable stdin exits 2, not 1" crates/batten/tests/it/ready.rs +// carried: "output is pointer-only — no issue prose echoed" crates/batten/tests/it/ready.rs +// carried: "a blocker claimed under a §8 HEADING with no relation is reported" crates/batten/tests/it/ready.rs +// carried: "the same claim with the relation present passes" crates/batten/tests/it/ready.rs +// carried: "a §8 heading claiming nothing is not a violation" crates/batten/tests/it/ready.rs +// carried: "the span stops at the next heading, so a later section is not §8 text" crates/batten/tests/it/ready.rs +// carried: "the span stops at the paragraph end, so a following paragraph is not the claim" crates/batten/tests/it/ready.rs +// carried: "a block that is only a refinement note carries no clause and is reported" crates/batten/tests/it/ready.rs +// carried: "a house-style cross-reference in prose does not satisfy the floor" crates/batten/tests/it/ready.rs +// carried: "a block carrying only §1 clears the floor — it is a floor, not a checklist" crates/batten/tests/it/ready.rs +// carried: "a heading-form label counts as a clause" crates/batten/tests/it/ready.rs +// carried: "a clause-free parent block is exempt from the floor" crates/batten/tests/it/ready.rs +// carried: "the non-canonical ready opener is reported, not treated as no block" crates/batten/tests/it/ready.rs +// carried: "a non-canonical opener still has its content judged" crates/batten/tests/it/ready.rs +// carried: "a payload with no description is exit 2 naming the field, never a verdict" crates/batten/tests/it/ready.rs +// carried: "a payload carrying only the declared field set is judged on its merits" crates/batten/tests/it/ready.rs +// carried: "(a) no relations key is a gap, never blocker-cited-without-relation" crates/batten/tests/it/ready.rs +// carried: "(b) relations present and empty is an answer, so the citation still reports" crates/batten/tests/it/ready.rs +// carried: "(c) relations present and carrying the cited id passes" crates/batten/tests/it/ready.rs +// carried: "(d) a judgeable violation outranks the gap: exit 1, not 2" crates/batten/tests/it/ready.rs +// carried: "the deferral rule has the same gap, and it reached further" crates/batten/tests/it/ready.rs +// carried: "a missing key costs nothing when the block cites nothing" crates/batten/tests/it/ready.rs +// carried: "a §7 introducing a deny gate with no replay is refused" crates/batten/tests/it/ready.rs +// carried: "a deny gate that reports its replay passes" crates/batten/tests/it/ready.rs +// carried: "a block declaring warn is not gated" crates/batten/tests/it/ready.rs +// carried: "a fenced [[rule]] at deny is a gate introduction too" crates/batten/tests/it/ready.rs +// carried: "a block introducing no gate is untouched by the replay clause" crates/batten/tests/it/ready.rs +// carried: "the deny-without-replay report carries no line of the block" crates/batten/tests/it/ready.rs // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; use std::process::Output; @@ -260,7 +260,7 @@ fn complete_claims() -> serde_json::Value { "commit_type": "feat", "blockers": [], "tests": [{ - "file": "crates/batten/tests/ready.rs", + "file": "crates/batten/tests/it/ready.rs", "mutation": "drop the required-key check", }], }) @@ -473,7 +473,7 @@ fn a_test_claim_missing_its_mutation_is_refused() { for key in ["file", "mutation"] { let mut object = complete_claims(); let mut entry = serde_json::json!({ - "file": "crates/batten/tests/ready.rs", + "file": "crates/batten/tests/it/ready.rs", "mutation": "drop the required-key check", }); entry.as_object_mut().expect("an object").remove(key); diff --git a/crates/batten/tests/reference_coverage.rs b/crates/batten/tests/it/reference_coverage.rs similarity index 89% rename from crates/batten/tests/reference_coverage.rs rename to crates/batten/tests/it/reference_coverage.rs index 16b26aad8..5641d1363 100644 --- a/crates/batten/tests/reference_coverage.rs +++ b/crates/batten/tests/it/reference_coverage.rs @@ -32,7 +32,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeSet; @@ -45,8 +45,8 @@ use common::{batten, scratch}; // QUOTED case name, a file arm's is a path. The suite's arm names its declared // `# subject:` too (CLOUD-1130), which this same delta retires. // -// carried: mise-tasks/reference-check.sh crates/batten/src/render.rs kind:mechanism crates/batten/tests/reference_coverage.rs -// carried: tests/reference-check.bats mise-tasks/reference-check.sh crates/batten/src/render.rs kind:mechanism crates/batten/tests/reference_coverage.rs +// carried: mise-tasks/reference-check.sh crates/batten/src/render.rs kind:mechanism crates/batten/tests/it/reference_coverage.rs +// carried: tests/reference-check.bats mise-tasks/reference-check.sh crates/batten/src/render.rs kind:mechanism crates/batten/tests/it/reference_coverage.rs // // CLOUD-908's case arms: every `@test` the retired suite declared, and where its // predicate lives now. Eight carried and two changed, and both changes are the @@ -54,17 +54,17 @@ use common::{batten, scratch}; // predicate dropped. Arms are suite-qualified because a case TITLE is not unique // across suites and this bundle retires four of them at once. // -// carried: "reference-check.bats::a reference naming every declared flag passes" crates/batten/tests/reference_coverage.rs -// carried: "reference-check.bats::a flag the reference omits is reported with its name" crates/batten/tests/reference_coverage.rs -// carried: "reference-check.bats::a flag the reference invents is reported with its name" crates/batten/tests/reference_coverage.rs -// carried: "reference-check.bats::both directions are reported in one run, not just the first" crates/batten/tests/reference_coverage.rs -// carried: "reference-check.bats::output is pointer-only — no line of the reference echoed" crates/batten/tests/reference_coverage.rs -// carried: "reference-check.bats::a reference naming no flags at all is could-not-look, never a pass" crates/batten/tests/reference_coverage.rs -// carried: "reference-check.bats::the gate leaves no reference behind in the tree it judges" crates/batten/tests/reference_coverage.rs -// carried: "reference-check.bats::this repo's reference covers its surface — the gate on the real tree" crates/batten/tests/reference_coverage.rs +// carried: "reference-check.bats::a reference naming every declared flag passes" crates/batten/tests/it/reference_coverage.rs +// carried: "reference-check.bats::a flag the reference omits is reported with its name" crates/batten/tests/it/reference_coverage.rs +// carried: "reference-check.bats::a flag the reference invents is reported with its name" crates/batten/tests/it/reference_coverage.rs +// carried: "reference-check.bats::both directions are reported in one run, not just the first" crates/batten/tests/it/reference_coverage.rs +// carried: "reference-check.bats::output is pointer-only — no line of the reference echoed" crates/batten/tests/it/reference_coverage.rs +// carried: "reference-check.bats::a reference naming no flags at all is could-not-look, never a pass" crates/batten/tests/it/reference_coverage.rs +// carried: "reference-check.bats::the gate leaves no reference behind in the tree it judges" crates/batten/tests/it/reference_coverage.rs +// carried: "reference-check.bats::this repo's reference covers its surface — the gate on the real tree" crates/batten/tests/it/reference_coverage.rs // -// changed: "reference-check.bats::a renderer that fails is could-not-look, never a pass" crates/batten/tests/reference_coverage.rs the suite stubbed a sibling program that exited 1, and there is no sibling to stub: the renderer is the binary under test. The property that survives is the one the stub stood in for — a render that did not produce a reference must not read as a covered one — asserted in `a_render_that_did_not_happen_is_never_read_as_coverage`, which drives the real binary to a non-zero exit and shows the empty reading is refused rather than passed -// changed: "reference-check.bats::an absent renderer is could-not-look, never a pass" crates/batten/tests/reference_coverage.rs same cause, one case further on: an ABSENT renderer is unreachable once the renderer is the binary, because a missing binary is a test harness that did not build rather than a verdict this tier can reach. The reading it protected — that an unusable render is not coverage — is the same one `a_render_that_did_not_happen_is_never_read_as_coverage` carries, so this arm records the collapse rather than claiming two cases survived +// changed: "reference-check.bats::a renderer that fails is could-not-look, never a pass" crates/batten/tests/it/reference_coverage.rs the suite stubbed a sibling program that exited 1, and there is no sibling to stub: the renderer is the binary under test. The property that survives is the one the stub stood in for — a render that did not produce a reference must not read as a covered one — asserted in `a_render_that_did_not_happen_is_never_read_as_coverage`, which drives the real binary to a non-zero exit and shows the empty reading is refused rather than passed +// changed: "reference-check.bats::an absent renderer is could-not-look, never a pass" crates/batten/tests/it/reference_coverage.rs same cause, one case further on: an ABSENT renderer is unreachable once the renderer is the binary, because a missing binary is a test harness that did not build rather than a verdict this tier can reach. The reading it protected — that an unusable render is not coverage — is the same one `a_render_that_did_not_happen_is_never_read_as_coverage` carries, so this arm records the collapse rather than claiming two cases survived /// Every flag id the surface declares, at every depth. /// diff --git a/crates/batten/tests/remedy_authorship.rs b/crates/batten/tests/it/remedy_authorship.rs similarity index 99% rename from crates/batten/tests/remedy_authorship.rs rename to crates/batten/tests/it/remedy_authorship.rs index 2339ed2de..958839742 100644 --- a/crates/batten/tests/remedy_authorship.rs +++ b/crates/batten/tests/it/remedy_authorship.rs @@ -26,7 +26,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/retirement_doctrine.rs b/crates/batten/tests/it/retirement_doctrine.rs similarity index 99% rename from crates/batten/tests/retirement_doctrine.rs rename to crates/batten/tests/it/retirement_doctrine.rs index ea62a8f03..77badc52f 100644 --- a/crates/batten/tests/retirement_doctrine.rs +++ b/crates/batten/tests/it/retirement_doctrine.rs @@ -43,7 +43,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/review_answered.rs b/crates/batten/tests/it/review_answered.rs similarity index 96% rename from crates/batten/tests/review_answered.rs rename to crates/batten/tests/it/review_answered.rs index 6470c5eff..143434ead 100644 --- a/crates/batten/tests/review_answered.rs +++ b/crates/batten/tests/it/review_answered.rs @@ -46,20 +46,20 @@ //! //! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads //! -// carried: tests/review-answered.bats policy/review-answered.rego crates/batten/tests/review_answered.rs +// carried: tests/review-answered.bats policy/review-answered.rego crates/batten/tests/it/review_answered.rs //! //! # RETIREMENT LEDGER — `tests/review-answered.bats`, 12 cases //! //! CARRIED — the property survives, proved here against the same two calls. //! -// carried: "a ready with no record at all is refused, and the receipt row names the command" crates/batten/tests/review_answered.rs -// carried: "a head whose threads are all answered is allowed" crates/batten/tests/review_answered.rs -// carried: "VACUITY: a buffer that is not the declared shape records nothing rather than one row" crates/batten/tests/review_answered.rs -// carried: "VACUITY: an empty buffer is not zero rows" crates/batten/tests/review_answered.rs -// carried: "a buffer from a command nobody asked for never becomes the record" crates/batten/tests/review_answered.rs -// carried: "a re-draft is not a ready, even on a head carrying findings" crates/batten/tests/review_answered.rs -// carried: "a commit message naming the command is prose, not a ready" crates/batten/tests/review_answered.rs -// carried: "reading the review is never refused, so the remedy is reachable" crates/batten/tests/review_answered.rs +// carried: "a ready with no record at all is refused, and the receipt row names the command" crates/batten/tests/it/review_answered.rs +// carried: "a head whose threads are all answered is allowed" crates/batten/tests/it/review_answered.rs +// carried: "VACUITY: a buffer that is not the declared shape records nothing rather than one row" crates/batten/tests/it/review_answered.rs +// carried: "VACUITY: an empty buffer is not zero rows" crates/batten/tests/it/review_answered.rs +// carried: "a buffer from a command nobody asked for never becomes the record" crates/batten/tests/it/review_answered.rs +// carried: "a re-draft is not a ready, even on a head carrying findings" crates/batten/tests/it/review_answered.rs +// carried: "a commit message naming the command is prose, not a ready" crates/batten/tests/it/review_answered.rs +// carried: "reading the review is never refused, so the remedy is reachable" crates/batten/tests/it/review_answered.rs //! //! CHANGED — the property survives and what it ASSERTS moved. Four of these //! asserted a count inside prose and now assert the same count as the @@ -67,10 +67,10 @@ //! four moved AGAIN under CLOUD-690, because what produces the count changed: //! each is noted below with what the number is now and why. //! -// changed: "review-answered.bats::THE MEASURED SHAPE: a head carrying unresolved threads is refused, naming the count" crates/batten/tests/review_answered.rs the count is identical and where it is read from is not: `4 blocking` was a substring of a free string, and it is now the `Subject::Count` the engine renders beside the token (CLOUD-1050) -// changed: "review-answered.bats::VACUITY: zero threads and no review reads as unreviewed, not as all-addressed" crates/batten/tests/review_answered.rs the count is 0 now and the rule is `review-absent`: the condition was one element of a `--jq` projection and is a second fact with its own inverted comparison since CLOUD-690, so the assertion moved from prose to a different predicate's subject rather than only to a subject -// changed: "review-answered.bats::VACUITY: a page the command could not read refuses rather than passing" crates/batten/tests/review_answered.rs same number, different producer: the projection emitted an extra element and the `blocking` column adds one, so the discriminating pair with the all-answered case is now two identical thread sets under different page flags -// changed: "review-answered.bats::THE BYPASS: a compound command is still a ready" crates/batten/tests/review_answered.rs same cause, same number; what the case proves — that the receipt row's selection and this module's narrowing agree about one command — is unchanged +// changed: "review-answered.bats::THE MEASURED SHAPE: a head carrying unresolved threads is refused, naming the count" crates/batten/tests/it/review_answered.rs the count is identical and where it is read from is not: `4 blocking` was a substring of a free string, and it is now the `Subject::Count` the engine renders beside the token (CLOUD-1050) +// changed: "review-answered.bats::VACUITY: zero threads and no review reads as unreviewed, not as all-addressed" crates/batten/tests/it/review_answered.rs the count is 0 now and the rule is `review-absent`: the condition was one element of a `--jq` projection and is a second fact with its own inverted comparison since CLOUD-690, so the assertion moved from prose to a different predicate's subject rather than only to a subject +// changed: "review-answered.bats::VACUITY: a page the command could not read refuses rather than passing" crates/batten/tests/it/review_answered.rs same number, different producer: the projection emitted an extra element and the `blocking` column adds one, so the discriminating pair with the all-answered case is now two identical thread sets under different page flags +// changed: "review-answered.bats::THE BYPASS: a compound command is still a ready" crates/batten/tests/it/review_answered.rs same cause, same number; what the case proves — that the receipt row's selection and this module's narrowing agree about one command — is unchanged //! //! # Two cases the retired suite could not have //! @@ -93,7 +93,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/rule_cost_census.rs b/crates/batten/tests/it/rule_cost_census.rs similarity index 100% rename from crates/batten/tests/rule_cost_census.rs rename to crates/batten/tests/it/rule_cost_census.rs diff --git a/crates/batten/tests/rules_builtin_claims.rs b/crates/batten/tests/it/rules_builtin_claims.rs similarity index 98% rename from crates/batten/tests/rules_builtin_claims.rs rename to crates/batten/tests/it/rules_builtin_claims.rs index bade7ffd8..4e9b6edcf 100644 --- a/crates/batten/tests/rules_builtin_claims.rs +++ b/crates/batten/tests/it/rules_builtin_claims.rs @@ -43,7 +43,7 @@ //! does not list slips through. That is a real bound: the honest object here is //! "a paragraph that denies an enabled feature in words we recognise", never //! "the file contains no false claim", and no §7 clause should be read as the -//! second. `crates/batten/tests/scanner_taxonomy.rs` sets the precedent of +//! second. `crates/batten/tests/it/scanner_taxonomy.rs` sets the precedent of //! saying plainly what a prose assertion holds and what it does not. //! //! The granularity is a **paragraph** rather than a sentence, deliberately: @@ -53,7 +53,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/rules_drift.rs b/crates/batten/tests/it/rules_drift.rs similarity index 99% rename from crates/batten/tests/rules_drift.rs rename to crates/batten/tests/it/rules_drift.rs index f530cf8d8..325fcfc15 100644 --- a/crates/batten/tests/rules_drift.rs +++ b/crates/batten/tests/it/rules_drift.rs @@ -81,7 +81,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::Path; diff --git a/crates/batten/tests/run_shape.rs b/crates/batten/tests/it/run_shape.rs similarity index 96% rename from crates/batten/tests/run_shape.rs rename to crates/batten/tests/it/run_shape.rs index d7c3a3ac1..758af578e 100644 --- a/crates/batten/tests/run_shape.rs +++ b/crates/batten/tests/it/run_shape.rs @@ -37,12 +37,12 @@ // THE FILE-GRANULARITY RETIREMENT ARM (CLOUD-1059). See the sibling note in // `privileged_lane.rs` for why one marker carries two disjoint ledgers. // -// carried: tests/run-shape.bats policy/run-shape.rego crates/batten/tests/run_shape.rs +// carried: tests/run-shape.bats policy/run-shape.rego crates/batten/tests/it/run_shape.rs // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; @@ -213,7 +213,7 @@ fn allowed_background(root: &Path, command: &str, background: bool) { // The predicate. // --------------------------------------------------------------------------- -// carried: "THE MEASURED SHAPE: a git commit naming no message source is denied" crates/batten/tests/run_shape.rs +// carried: "THE MEASURED SHAPE: a git commit naming no message source is denied" crates/batten/tests/it/run_shape.rs #[test] fn a_git_commit_naming_no_message_source_is_denied() { // `pre-commit` runs before git asks for a message, so this spends the whole @@ -223,7 +223,7 @@ fn a_git_commit_naming_no_message_source_is_denied() { denied(&root, "git commit -a"); } -// changed: "every form that CAN obtain a message stays allowed" crates/batten/tests/run_shape.rs a bare `git commit -F -` moved from this list to `a_commit_reading_unbound_stdin_is_refused`, because CLOUD-613 landed the predicate that tells the two apart — the retired case could not, so it asserted the weaker claim +// changed: "every form that CAN obtain a message stays allowed" crates/batten/tests/it/run_shape.rs a bare `git commit -F -` moved from this list to `a_commit_reading_unbound_stdin_is_refused`, because CLOUD-613 landed the predicate that tells the two apart — the retired case could not, so it asserted the weaker claim #[test] fn every_form_that_can_obtain_a_message_stays_allowed() { // The load-bearing half. A predicate that only ever denied would satisfy the @@ -429,7 +429,7 @@ fn a_mention_of_sleep_is_not_a_call() { allowed_background(&root, "git commit -m \"stop using sleep 90\"", false); } -// carried: "THE MEASURED SHAPE: a token carrying an m is not a flag cluster" crates/batten/tests/run_shape.rs +// carried: "THE MEASURED SHAPE: a token carrying an m is not a flag cluster" crates/batten/tests/it/run_shape.rs #[test] fn a_token_carrying_an_m_is_not_a_flag_cluster() { // CLOUD-885. The rule reads "one `-`, then LETTERS, at least one of which @@ -452,7 +452,7 @@ fn a_token_carrying_an_m_is_not_a_flag_cluster() { // The list, which is where a raw-string module goes silent. // --------------------------------------------------------------------------- -// carried: "a compound list is judged per element, not by its first word" crates/batten/tests/run_shape.rs +// carried: "a compound list is judged per element, not by its first word" crates/batten/tests/it/run_shape.rs #[test] fn a_compound_list_is_judged_per_element() { // THE SHAPE A RAW-STRING MODULE MISSES. The vendored `no-force-push` preset @@ -464,13 +464,13 @@ fn a_compound_list_is_judged_per_element() { allowed(&root, "git add -A && git commit -m x"); } -// carried: "a pipe stage is judged too" crates/batten/tests/run_shape.rs +// carried: "a pipe stage is judged too" crates/batten/tests/it/run_shape.rs #[test] fn a_pipe_stage_is_judged_too() { denied(&fixture("pipe-stage"), "echo hi | git commit"); } -// carried: "a wrapper is looked through to the program it runs" crates/batten/tests/run_shape.rs +// carried: "a wrapper is looked through to the program it runs" crates/batten/tests/it/run_shape.rs #[test] fn a_wrapper_is_looked_through_to_the_program_it_runs() { let root = fixture("wrapper"); @@ -482,7 +482,7 @@ fn a_wrapper_is_looked_through_to_the_program_it_runs() { // Scrubbing: prose is not a call. // --------------------------------------------------------------------------- -// carried: "a git commit inside a quoted span is prose, not a call" crates/batten/tests/run_shape.rs +// carried: "a git commit inside a quoted span is prose, not a call" crates/batten/tests/it/run_shape.rs #[test] fn a_git_commit_inside_a_quoted_span_is_prose() { // This repository writes the shape down constantly — in commit messages, in @@ -493,7 +493,7 @@ fn a_git_commit_inside_a_quoted_span_is_prose() { allowed(&root, "echo 'git commit'"); } -// carried: "a quoted span carrying a list separator is not a list" crates/batten/tests/run_shape.rs +// carried: "a quoted span carrying a list separator is not a list" crates/batten/tests/it/run_shape.rs #[test] fn a_quoted_span_carrying_a_list_separator_is_not_a_list() { // THE CASE THAT DISCRIMINATES the quote scrub. A quoted mention with no @@ -506,7 +506,7 @@ fn a_quoted_span_carrying_a_list_separator_is_not_a_list() { allowed(&root, "echo 'step one; git commit -x'"); } -// carried: "a git commit inside a heredoc body is prose, not a call" crates/batten/tests/run_shape.rs +// carried: "a git commit inside a heredoc body is prose, not a call" crates/batten/tests/it/run_shape.rs #[test] fn a_git_commit_inside_a_heredoc_body_is_prose() { allowed( @@ -515,14 +515,14 @@ fn a_git_commit_inside_a_heredoc_body_is_prose() { ); } -// carried: "an unquoted mention does not resolve to git" crates/batten/tests/run_shape.rs +// carried: "an unquoted mention does not resolve to git" crates/batten/tests/it/run_shape.rs #[test] fn an_unquoted_mention_does_not_resolve_to_git() { // The anchoring, without which `echo git commit` reads as a call. allowed(&fixture("unquoted-mention"), "echo git commit"); } -// carried: "a heredoc or redirect bound to this element is a message source" crates/batten/tests/run_shape.rs +// carried: "a heredoc or redirect bound to this element is a message source" crates/batten/tests/it/run_shape.rs #[test] fn a_heredoc_or_redirect_bound_to_this_element_is_a_message_source() { let root = fixture("bound-source"); @@ -534,7 +534,7 @@ fn a_heredoc_or_redirect_bound_to_this_element_is_a_message_source() { // The refusal itself. // --------------------------------------------------------------------------- -// changed: "the refusal names its predicate and the remedy that cannot rebind" crates/batten/tests/run_shape.rs the remedy moved from the module's prose into the declared class, so the assertion is over the token and the route rather than over three substrings of a sentence (CLOUD-1050) +// changed: "the refusal names its predicate and the remedy that cannot rebind" crates/batten/tests/it/run_shape.rs the remedy moved from the module's prose into the declared class, so the assertion is over the token and the route rather than over three substrings of a sentence (CLOUD-1050) #[test] fn the_refusal_names_its_predicate_its_class_and_the_route_out() { // A migrated gate keeps its remedy (CLOUD-437): a refusal that lost it in @@ -565,7 +565,7 @@ fn the_refusal_names_its_predicate_its_class_and_the_route_out() { ); } -// carried: "git -C commit is a deliberate false negative, carried over" crates/batten/tests/run_shape.rs +// carried: "git -C commit is a deliberate false negative, carried over" crates/batten/tests/it/run_shape.rs #[test] fn git_c_path_commit_is_a_deliberate_false_negative() { // The bash guard resolved `sub1` to the path and let it through, because a @@ -575,7 +575,7 @@ fn git_c_path_commit_is_a_deliberate_false_negative() { allowed(&fixture("dash-c"), "git -C /some/path commit"); } -// carried: "a command with no git commit in it at all is untouched" crates/batten/tests/run_shape.rs +// carried: "a command with no git commit in it at all is untouched" crates/batten/tests/it/run_shape.rs #[test] fn a_command_with_no_git_commit_in_it_is_untouched() { let root = fixture("untouched"); diff --git a/crates/batten/tests/run_shape_guard_door.rs b/crates/batten/tests/it/run_shape_guard_door.rs similarity index 99% rename from crates/batten/tests/run_shape_guard_door.rs rename to crates/batten/tests/it/run_shape_guard_door.rs index 755e6c45a..b88080fa1 100644 --- a/crates/batten/tests/run_shape_guard_door.rs +++ b/crates/batten/tests/it/run_shape_guard_door.rs @@ -45,7 +45,7 @@ #![cfg(unix)] #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/runner_verdict.rs b/crates/batten/tests/it/runner_verdict.rs similarity index 100% rename from crates/batten/tests/runner_verdict.rs rename to crates/batten/tests/it/runner_verdict.rs diff --git a/crates/batten/tests/scanner_taxonomy.rs b/crates/batten/tests/it/scanner_taxonomy.rs similarity index 99% rename from crates/batten/tests/scanner_taxonomy.rs rename to crates/batten/tests/it/scanner_taxonomy.rs index 44b98f8fe..221fece71 100644 --- a/crates/batten/tests/scanner_taxonomy.rs +++ b/crates/batten/tests/it/scanner_taxonomy.rs @@ -34,7 +34,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/secrets_kind.rs b/crates/batten/tests/it/secrets_kind.rs similarity index 99% rename from crates/batten/tests/secrets_kind.rs rename to crates/batten/tests/it/secrets_kind.rs index 0a45128c5..094abd52e 100644 --- a/crates/batten/tests/secrets_kind.rs +++ b/crates/batten/tests/it/secrets_kind.rs @@ -29,7 +29,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fmt::Write as _; use std::fs; diff --git a/crates/batten/tests/semver_gate.rs b/crates/batten/tests/it/semver_gate.rs similarity index 94% rename from crates/batten/tests/semver_gate.rs rename to crates/batten/tests/it/semver_gate.rs index b347d118d..50c933d2e 100644 --- a/crates/batten/tests/semver_gate.rs +++ b/crates/batten/tests/it/semver_gate.rs @@ -32,21 +32,21 @@ //! //! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads //! -// carried: mise-tasks/semver.sh crates/batten/src/semver.rs kind:verb crates/batten/tests/semver_gate.rs -// carried: tests/semver.bats crates/batten/src/semver.rs kind:verb crates/batten/tests/semver_gate.rs +// carried: mise-tasks/semver.sh crates/batten/src/semver.rs kind:verb crates/batten/tests/it/semver_gate.rs +// carried: tests/semver.bats crates/batten/src/semver.rs kind:verb crates/batten/tests/it/semver_gate.rs //! //! # RETIREMENT LEDGER — `tests/semver.bats`, 12 cases //! //! CARRIED — the property survives, proved here against the binary. //! -// carried: "a patch-compatible delta passes, and names the claim it verified" crates/batten/tests/semver_gate.rs -// carried: "THE VACUOUS RUN: a report that graded 0 checks is exit 2, never a pass" crates/batten/tests/semver_gate.rs -// carried: "an undeclared break fails, and names the lint rather than the payload" crates/batten/tests/semver_gate.rs -// carried: "a break declared with a bang passes, and names the declaring commit" crates/batten/tests/semver_gate.rs -// carried: "a break declared with a BREAKING CHANGE footer passes too" crates/batten/tests/semver_gate.rs -// carried: "A DECLARATION ON THE BASELINE DOES NOT COUNT — only this branch's commits" crates/batten/tests/semver_gate.rs -// carried: "an exit code that is neither verdict is exit 2 — a broken run is not a pass" crates/batten/tests/semver_gate.rs -// carried: "output is a pointer — lint ids and a short sha, never the rustdoc it read" crates/batten/tests/semver_gate.rs +// carried: "a patch-compatible delta passes, and names the claim it verified" crates/batten/tests/it/semver_gate.rs +// carried: "THE VACUOUS RUN: a report that graded 0 checks is exit 2, never a pass" crates/batten/tests/it/semver_gate.rs +// carried: "an undeclared break fails, and names the lint rather than the payload" crates/batten/tests/it/semver_gate.rs +// carried: "a break declared with a bang passes, and names the declaring commit" crates/batten/tests/it/semver_gate.rs +// carried: "a break declared with a BREAKING CHANGE footer passes too" crates/batten/tests/it/semver_gate.rs +// carried: "A DECLARATION ON THE BASELINE DOES NOT COUNT — only this branch's commits" crates/batten/tests/it/semver_gate.rs +// carried: "an exit code that is neither verdict is exit 2 — a broken run is not a pass" crates/batten/tests/it/semver_gate.rs +// carried: "output is a pointer — lint ids and a short sha, never the rustdoc it read" crates/batten/tests/it/semver_gate.rs //! //! CHANGED — behaviour that diverges deliberately, each with its reason. //! @@ -63,7 +63,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use batten::exit::ExitCode; use batten::semver::{Commit, Compared, Route, Verdict, declared_break, reconcile}; diff --git a/crates/batten/tests/shell_retirement.rs b/crates/batten/tests/it/shell_retirement.rs similarity index 99% rename from crates/batten/tests/shell_retirement.rs rename to crates/batten/tests/it/shell_retirement.rs index af73bf1b8..33f107e8d 100644 --- a/crates/batten/tests/shell_retirement.rs +++ b/crates/batten/tests/it/shell_retirement.rs @@ -22,7 +22,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/shell_write_advisory.rs b/crates/batten/tests/it/shell_write_advisory.rs similarity index 99% rename from crates/batten/tests/shell_write_advisory.rs rename to crates/batten/tests/it/shell_write_advisory.rs index d3eb27f53..7318cbbcc 100644 --- a/crates/batten/tests/shell_write_advisory.rs +++ b/crates/batten/tests/it/shell_write_advisory.rs @@ -27,7 +27,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/sinks.rs b/crates/batten/tests/it/sinks.rs similarity index 99% rename from crates/batten/tests/sinks.rs rename to crates/batten/tests/it/sinks.rs index 73c3f115a..325f28a85 100644 --- a/crates/batten/tests/sinks.rs +++ b/crates/batten/tests/it/sinks.rs @@ -23,7 +23,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/skill_contract.rs b/crates/batten/tests/it/skill_contract.rs similarity index 93% rename from crates/batten/tests/skill_contract.rs rename to crates/batten/tests/it/skill_contract.rs index 5c71532e3..041892db8 100644 --- a/crates/batten/tests/skill_contract.rs +++ b/crates/batten/tests/it/skill_contract.rs @@ -33,7 +33,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeSet; use std::fmt::Write as _; @@ -47,36 +47,36 @@ use common::{at_root, batten, scratch}; // claim a conservation nobody checked. The suite's arm names its declared // `# subject:` too (CLOUD-1130), which this same delta retires. // -// carried: mise-tasks/skill-check.sh crates/batten/src/surface.rs kind:mechanism crates/batten/tests/skill_contract.rs -// carried: tests/skill-check.bats mise-tasks/skill-check.sh crates/batten/src/surface.rs kind:mechanism crates/batten/tests/skill_contract.rs +// carried: mise-tasks/skill-check.sh crates/batten/src/surface.rs kind:mechanism crates/batten/tests/it/skill_contract.rs +// carried: tests/skill-check.bats mise-tasks/skill-check.sh crates/batten/src/surface.rs kind:mechanism crates/batten/tests/it/skill_contract.rs // // CLOUD-908's case arms: every `@test` the retired suite declared. Nineteen // carried and two changed, and each change is a SEAM the port moved rather than a // predicate it dropped. Arms are suite-qualified because a case TITLE is not // unique across suites and this bundle retires four of them at once. // -// carried: "skill-check.bats::a skill inside budget, naming only declared verbs, exits 0" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a verb the binary does not declare is reported with a file:line pointer" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a subcommand the binary does not declare is caught, not just a bare verb" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a positional argument does not read as an undeclared subcommand" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::flags do not read as subcommands" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a console block is judged as well as an inline span" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::prose naming the product is not read as a verb" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a skill over the line budget is refused, and the count is named" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::the budget is a boundary, not a suggestion" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::an exit meaning that drifts from the binary's rendering is caught" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a code the skill never names is caught even when its meaning is present" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a vendor path that is a copy rather than a symlink is refused" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a symlink pointing at some other file is refused" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a missing skill is exit 2 — could not look, not a clean tree" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::the repo as it stands passes" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a second skill with no vendor symlink is a violation" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a second skill is discovered while UNTRACKED — presence is the predicate" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a well-formed second skill leaves the run clean" crates/batten/tests/skill_contract.rs -// carried: "skill-check.bats::a second skill over budget is reported against its own path" crates/batten/tests/skill_contract.rs +// carried: "skill-check.bats::a skill inside budget, naming only declared verbs, exits 0" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a verb the binary does not declare is reported with a file:line pointer" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a subcommand the binary does not declare is caught, not just a bare verb" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a positional argument does not read as an undeclared subcommand" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::flags do not read as subcommands" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a console block is judged as well as an inline span" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::prose naming the product is not read as a verb" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a skill over the line budget is refused, and the count is named" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::the budget is a boundary, not a suggestion" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::an exit meaning that drifts from the binary's rendering is caught" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a code the skill never names is caught even when its meaning is present" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a vendor path that is a copy rather than a symlink is refused" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a symlink pointing at some other file is refused" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a missing skill is exit 2 — could not look, not a clean tree" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::the repo as it stands passes" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a second skill with no vendor symlink is a violation" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a second skill is discovered while UNTRACKED — presence is the predicate" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a well-formed second skill leaves the run clean" crates/batten/tests/it/skill_contract.rs +// carried: "skill-check.bats::a second skill over budget is reported against its own path" crates/batten/tests/it/skill_contract.rs // -// changed: "skill-check.bats::an unreadable spec is exit 2, never a pass over an unjudged vocabulary" crates/batten/tests/skill_contract.rs the suite pointed `BATTEN_BIN` at a nonexistent path, and this tier has no such indirection: `common::batten()` resolves `CARGO_BIN_EXE_batten`, so a binary that is not there is a harness that did not build rather than a verdict. What that case protected — that an unjudged vocabulary never reads as a clean one — survives as `an_empty_vocabulary_is_never_read_as_a_clean_skill`, which asserts the refusal from the reading's side instead of from the launcher's -// changed: "skill-check.bats::the gate is wired: hk.pkl declares a step that runs this task" crates/batten/tests/skill_contract.rs the step now runs the successor task rather than `mise run skill-check`, so the literal it asserted is gone. The property is the same one and is still gated: `the_skill_contract_is_wired_into_the_hk_gate` asserts hk.pkl declares a `skill-check` step whose check names the task that runs THIS file +// changed: "skill-check.bats::an unreadable spec is exit 2, never a pass over an unjudged vocabulary" crates/batten/tests/it/skill_contract.rs the suite pointed `BATTEN_BIN` at a nonexistent path, and this tier has no such indirection: `common::batten()` resolves `CARGO_BIN_EXE_batten`, so a binary that is not there is a harness that did not build rather than a verdict. What that case protected — that an unjudged vocabulary never reads as a clean one — survives as `an_empty_vocabulary_is_never_read_as_a_clean_skill`, which asserts the refusal from the reading's side instead of from the launcher's +// changed: "skill-check.bats::the gate is wired: hk.pkl declares a step that runs this task" crates/batten/tests/it/skill_contract.rs the step now runs the successor task rather than `mise run skill-check`, so the literal it asserted is gone. The property is the same one and is still gated: `the_skill_contract_is_wired_into_the_hk_gate` asserts hk.pkl declares a `skill-check` step whose check names the task that runs THIS file /// The ceiling, stated once. Raising it is a visible diff in this file. const MAX_LINES: usize = 300; @@ -856,6 +856,6 @@ fn the_skill_contract_is_wired_into_the_hk_gate() { assert!( check.contains("test:skill-contract"), "the skill-check step must run the task that drives \ - crates/batten/tests/skill_contract.rs, and runs `{check}`" + crates/batten/tests/it/skill_contract.rs, and runs `{check}`" ); } diff --git a/crates/batten/tests/sleep_ban.rs b/crates/batten/tests/it/sleep_ban.rs similarity index 99% rename from crates/batten/tests/sleep_ban.rs rename to crates/batten/tests/it/sleep_ban.rs index 85dec1c2b..dc46fd6c3 100644 --- a/crates/batten/tests/sleep_ban.rs +++ b/crates/batten/tests/it/sleep_ban.rs @@ -36,7 +36,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/snapshots.rs b/crates/batten/tests/it/snapshots.rs similarity index 99% rename from crates/batten/tests/snapshots.rs rename to crates/batten/tests/it/snapshots.rs index f008e0e4c..3e7b2885c 100644 --- a/crates/batten/tests/snapshots.rs +++ b/crates/batten/tests/it/snapshots.rs @@ -37,7 +37,7 @@ use std::process::Output; -mod common; +use crate::common; use common::{Fixture, batten, scratch}; diff --git a/crates/batten/tests/snapshots/snapshots__golden_exit_code_table.snap b/crates/batten/tests/it/snapshots/it__snapshots__golden_exit_code_table.snap similarity index 86% rename from crates/batten/tests/snapshots/snapshots__golden_exit_code_table.snap rename to crates/batten/tests/it/snapshots/it__snapshots__golden_exit_code_table.snap index 544e7b82b..1b532ebc9 100644 --- a/crates/batten/tests/snapshots/snapshots__golden_exit_code_table.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__golden_exit_code_table.snap @@ -1,5 +1,5 @@ --- -source: crates/batten/tests/snapshots.rs +source: crates/batten/tests/it/snapshots.rs expression: "batten::exit::table()" --- 0 clean — nothing to report; a mediated call is allowed diff --git a/crates/batten/tests/snapshots/snapshots__golden_json_schema.snap b/crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap similarity index 99% rename from crates/batten/tests/snapshots/snapshots__golden_json_schema.snap rename to crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap index 6b1eff8d7..910b19507 100644 --- a/crates/batten/tests/snapshots/snapshots__golden_json_schema.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap @@ -1,5 +1,5 @@ --- -source: crates/batten/tests/snapshots.rs +source: crates/batten/tests/it/snapshots.rs expression: stdout_of(&output) --- { diff --git a/crates/batten/tests/snapshots/snapshots__json_output_is_frozen.snap b/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap similarity index 93% rename from crates/batten/tests/snapshots/snapshots__json_output_is_frozen.snap rename to crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap index aa2882cdf..b82e88dd2 100644 --- a/crates/batten/tests/snapshots/snapshots__json_output_is_frozen.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap @@ -1,5 +1,5 @@ --- -source: crates/batten/tests/snapshots.rs +source: crates/batten/tests/it/snapshots.rs expression: stdout_of(&output) --- { diff --git a/crates/batten/tests/snapshots/snapshots__pointer_output_is_frozen.snap b/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap similarity index 61% rename from crates/batten/tests/snapshots/snapshots__pointer_output_is_frozen.snap rename to crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap index 2543a4a14..2276ee2bf 100644 --- a/crates/batten/tests/snapshots/snapshots__pointer_output_is_frozen.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap @@ -1,5 +1,5 @@ --- -source: crates/batten/tests/snapshots.rs +source: crates/batten/tests/it/snapshots.rs expression: stdout_of(&output) --- a.rs:1 no-todo diff --git a/crates/batten/tests/spawn_ceilings.rs b/crates/batten/tests/it/spawn_ceilings.rs similarity index 92% rename from crates/batten/tests/spawn_ceilings.rs rename to crates/batten/tests/it/spawn_ceilings.rs index 5ea89ff5e..cdb8e135a 100644 --- a/crates/batten/tests/spawn_ceilings.rs +++ b/crates/batten/tests/it/spawn_ceilings.rs @@ -15,26 +15,26 @@ //! `tests/fanout-guard.bats`, twelve cases, every one placed and every arm //! suite-qualified. //! -// carried: "fanout-guard.bats::an ordinary single-target spawn is allowed" crates/batten/tests/spawn_ceilings.rs -// carried: "fanout-guard.bats::a manifest over the cap is refused, naming the cap and the count" crates/batten/tests/spawn_ceilings.rs -// carried: "fanout-guard.bats::a mem: reference counts as an artifact, resolved against the tree" crates/batten/tests/spawn_ceilings.rs -// carried: "fanout-guard.bats::a path-shaped token naming nothing tracked does not count" crates/batten/tests/spawn_ceilings.rs -// carried: "fanout-guard.bats::an oversize prompt is refused against the token budget" crates/batten/tests/spawn_ceilings.rs -// carried: "fanout-guard.bats::a tool that is not a spawn is never judged" crates/batten/tests/spawn_ceilings.rs -// carried: "fanout-guard.bats::an absent prompt fails open" crates/batten/tests/spawn_ceilings.rs -// carried: "fanout-guard.bats::the refusal is a pointer — it carries no prompt bytes" crates/batten/tests/spawn_ceilings.rs +// carried: "fanout-guard.bats::an ordinary single-target spawn is allowed" crates/batten/tests/it/spawn_ceilings.rs +// carried: "fanout-guard.bats::a manifest over the cap is refused, naming the cap and the count" crates/batten/tests/it/spawn_ceilings.rs +// carried: "fanout-guard.bats::a mem: reference counts as an artifact, resolved against the tree" crates/batten/tests/it/spawn_ceilings.rs +// carried: "fanout-guard.bats::a path-shaped token naming nothing tracked does not count" crates/batten/tests/it/spawn_ceilings.rs +// carried: "fanout-guard.bats::an oversize prompt is refused against the token budget" crates/batten/tests/it/spawn_ceilings.rs +// carried: "fanout-guard.bats::a tool that is not a spawn is never judged" crates/batten/tests/it/spawn_ceilings.rs +// carried: "fanout-guard.bats::an absent prompt fails open" crates/batten/tests/it/spawn_ceilings.rs +// carried: "fanout-guard.bats::the refusal is a pointer — it carries no prompt bytes" crates/batten/tests/it/spawn_ceilings.rs //! //! SUBSUMED — the plumbing became the engine's, which is what a migration should //! produce. //! -// subsumed: "fanout-guard.bats::unparseable stdin neither refuses nor errors" crates/batten/tests/cli.rs +// subsumed: "fanout-guard.bats::unparseable stdin neither refuses nor errors" crates/batten/tests/it/cli.rs // subsumed: "fanout-guard.bats::the Task hook is registered in settings, by shape" mise-tasks/hooks-wiring-check.sh //! //! CHANGED — two, and each is a capability moving rather than a property being //! dropped. //! -// changed: "fanout-guard.bats::the caps are configurable in both directions" crates/batten/tests/spawn_ceilings.rs BATTEN_FANOUT_READING_CAP and BATTEN_FANOUT_PROMPT_BUDGET are gone: each cap is `max` on its own row, configured where every other property of the row is. Per-call override is deliberately not carried — an agent that can raise the ceiling at the call being gated is not gated — and the two directions are carried as the at-cap and over-cap cases below -// changed: "fanout-guard.bats::the bypass is honoured" crates/batten/tests/guardrail_bypass.rs BATTEN_FANOUT_GUARD_BYPASS is gone; a mediated deny takes the engine's own hatch, the consolidation rows 1-3 record +// changed: "fanout-guard.bats::the caps are configurable in both directions" crates/batten/tests/it/spawn_ceilings.rs BATTEN_FANOUT_READING_CAP and BATTEN_FANOUT_PROMPT_BUDGET are gone: each cap is `max` on its own row, configured where every other property of the row is. Per-call override is deliberately not carried — an agent that can raise the ceiling at the call being gated is not gated — and the two directions are carried as the at-cap and over-cap cases below +// changed: "fanout-guard.bats::the bypass is honoured" crates/batten/tests/it/guardrail_bypass.rs BATTEN_FANOUT_GUARD_BYPASS is gone; a mediated deny takes the engine's own hatch, the consolidation rows 1-3 record //! //! ─── CLOUD-909's REPLAY, row 6 ─────────────────────────────────────────────── //! @@ -43,7 +43,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; @@ -55,7 +55,7 @@ use common::{Fixture, run_with_stdin, stderr}; /// reason: a suite asserting a hand-written copy of the row would pass over a /// config that says something else. fn repo(name: &str) -> PathBuf { - let staged = Fixture::new(name).config(include_str!("../../../batten.toml")); + let staged = Fixture::new(name).config(include_str!("../../../../batten.toml")); let modules = staged.path().join("policy"); std::fs::create_dir_all(&modules).expect("the fixture's policy directory is creatable"); let committed = Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/batten/tests/spawn_census.rs b/crates/batten/tests/it/spawn_census.rs similarity index 99% rename from crates/batten/tests/spawn_census.rs rename to crates/batten/tests/it/spawn_census.rs index 79e488fce..232dfb1fa 100644 --- a/crates/batten/tests/spawn_census.rs +++ b/crates/batten/tests/it/spawn_census.rs @@ -24,7 +24,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/staged_facts.rs b/crates/batten/tests/it/staged_facts.rs similarity index 99% rename from crates/batten/tests/staged_facts.rs rename to crates/batten/tests/it/staged_facts.rs index 93fc6aaea..291d18811 100644 --- a/crates/batten/tests/staged_facts.rs +++ b/crates/batten/tests/it/staged_facts.rs @@ -20,7 +20,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/stop_posture.rs b/crates/batten/tests/it/stop_posture.rs similarity index 95% rename from crates/batten/tests/stop_posture.rs rename to crates/batten/tests/it/stop_posture.rs index 7fabd124e..1f348f963 100644 --- a/crates/batten/tests/stop_posture.rs +++ b/crates/batten/tests/it/stop_posture.rs @@ -22,39 +22,39 @@ //! The suite's successor is the module, which is where the one rule that COULD //! be a predicate went. //! -// carried: mise-tasks/stop-guard.sh crates/batten/src/lib.rs kind:mechanism crates/batten/tests/stop_posture.rs -// carried: tests/stop-guard.bats policy/stop-posture.rego crates/batten/tests/stop_posture.rs +// carried: mise-tasks/stop-guard.sh crates/batten/src/lib.rs kind:mechanism crates/batten/tests/it/stop_posture.rs +// carried: tests/stop-guard.bats policy/stop-posture.rego crates/batten/tests/it/stop_posture.rs //! //! # RETIREMENT LEDGER — `tests/stop-guard.bats`, 33 cases //! //! CARRIED — the property survives, proved here or in the module's own suite. //! -// carried: "a turn whose final message carries the tell is kicked" crates/batten/tests/stop_posture.rs -// carried: "the kick names the rule and the durable destination" crates/batten/tests/stop_posture.rs -// carried: "the kick declares the Stop event, so the harness routes it as feedback" crates/batten/tests/stop_posture.rs -// carried: "the kick is valid JSON on stdout" crates/batten/tests/stop_posture.rs -// carried: "the re-entry caused by a previous kick is not kicked again" crates/batten/tests/stop_posture.rs -// carried: "A CLEAN FINAL MESSAGE IS ANSWERED WITH SILENCE" crates/batten/tests/stop_posture.rs +// carried: "a turn whose final message carries the tell is kicked" crates/batten/tests/it/stop_posture.rs +// carried: "the kick names the rule and the durable destination" crates/batten/tests/it/stop_posture.rs +// carried: "the kick declares the Stop event, so the harness routes it as feedback" crates/batten/tests/it/stop_posture.rs +// carried: "the kick is valid JSON on stdout" crates/batten/tests/it/stop_posture.rs +// carried: "the re-entry caused by a previous kick is not kicked again" crates/batten/tests/it/stop_posture.rs +// carried: "A CLEAN FINAL MESSAGE IS ANSWERED WITH SILENCE" crates/batten/tests/it/stop_posture.rs // carried: "a turn that says it is stopping is not re-prompted" policy/stop-posture.rego // carried: "an ordinary answer to a question is not re-prompted" policy/stop-posture.rego -// carried: "an absent last_assistant_message costs the first rule and nothing else" crates/batten/tests/stop_posture.rs -// carried: "stop-guard::the bypass is honoured" crates/batten/tests/stop_posture.rs -// carried: "the guard never exits non-zero, so it cannot surface as a hook error" crates/batten/tests/stop_posture.rs -// carried: "a turn that strands a finding is pointed at, and the turn still ends" crates/batten/tests/stop_posture.rs -// carried: "POINTER, NEVER PAYLOAD: the advisory carries no byte of the turn's prose" crates/batten/tests/stop_posture.rs -// carried: "the advisory says what to do, since a coordinate alone is not an instruction" crates/batten/tests/stop_posture.rs -// carried: "the shipped rule keeps precedence when both would fire" crates/batten/tests/stop_posture.rs -// carried: "a turn that strands nothing is silent" crates/batten/tests/stop_posture.rs -// carried: "an unreadable transcript manufactures no advisory" crates/batten/tests/stop_posture.rs -// carried: "the recursion bound still holds for the second rule" crates/batten/tests/stop_posture.rs -// carried: "A FILED ROW NAMING THIS BRANCH'S OWN DIFF IS POINTED AT, BEFORE ANY CI" crates/batten/tests/filed_here.rs -// carried: "the punt pointer carries no prose from the row" crates/batten/tests/filed_here.rs -// carried: "the punt rule yields to the measured posture rule" crates/batten/tests/stop_posture.rs -// carried: "a branch with no filed row names none" crates/batten/tests/filed_here.rs -// carried: "UNLANDED WORK AT A DECLARED STOPPING POINT IS POINTED AT" crates/batten/tests/stop_posture.rs -// carried: "the unlanded pointer carries no transcript text and no store key" crates/batten/tests/stop_posture.rs -// carried: "the unlanded rule yields to the measured posture rule" crates/batten/tests/stop_posture.rs -// carried: "landed work is silent" crates/batten/tests/stop_posture.rs +// carried: "an absent last_assistant_message costs the first rule and nothing else" crates/batten/tests/it/stop_posture.rs +// carried: "stop-guard::the bypass is honoured" crates/batten/tests/it/stop_posture.rs +// carried: "the guard never exits non-zero, so it cannot surface as a hook error" crates/batten/tests/it/stop_posture.rs +// carried: "a turn that strands a finding is pointed at, and the turn still ends" crates/batten/tests/it/stop_posture.rs +// carried: "POINTER, NEVER PAYLOAD: the advisory carries no byte of the turn's prose" crates/batten/tests/it/stop_posture.rs +// carried: "the advisory says what to do, since a coordinate alone is not an instruction" crates/batten/tests/it/stop_posture.rs +// carried: "the shipped rule keeps precedence when both would fire" crates/batten/tests/it/stop_posture.rs +// carried: "a turn that strands nothing is silent" crates/batten/tests/it/stop_posture.rs +// carried: "an unreadable transcript manufactures no advisory" crates/batten/tests/it/stop_posture.rs +// carried: "the recursion bound still holds for the second rule" crates/batten/tests/it/stop_posture.rs +// carried: "A FILED ROW NAMING THIS BRANCH'S OWN DIFF IS POINTED AT, BEFORE ANY CI" crates/batten/tests/it/filed_here.rs +// carried: "the punt pointer carries no prose from the row" crates/batten/tests/it/filed_here.rs +// carried: "the punt rule yields to the measured posture rule" crates/batten/tests/it/stop_posture.rs +// carried: "a branch with no filed row names none" crates/batten/tests/it/filed_here.rs +// carried: "UNLANDED WORK AT A DECLARED STOPPING POINT IS POINTED AT" crates/batten/tests/it/stop_posture.rs +// carried: "the unlanded pointer carries no transcript text and no store key" crates/batten/tests/it/stop_posture.rs +// carried: "the unlanded rule yields to the measured posture rule" crates/batten/tests/it/stop_posture.rs +// carried: "landed work is silent" crates/batten/tests/it/stop_posture.rs //! //! SUBSUMED — the plumbing became the engine\'s, which is what a migration should //! produce. Each names the general property that now covers it. @@ -81,7 +81,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::io::Write as _; diff --git a/crates/batten/tests/submodule.rs b/crates/batten/tests/it/submodule.rs similarity index 99% rename from crates/batten/tests/submodule.rs rename to crates/batten/tests/it/submodule.rs index cc6ab2a33..efe1d0b5e 100644 --- a/crates/batten/tests/submodule.rs +++ b/crates/batten/tests/it/submodule.rs @@ -24,7 +24,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeSet; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/suite_subjects.rs b/crates/batten/tests/it/suite_subjects.rs similarity index 99% rename from crates/batten/tests/suite_subjects.rs rename to crates/batten/tests/it/suite_subjects.rs index 627181cbb..44baa30d7 100644 --- a/crates/batten/tests/suite_subjects.rs +++ b/crates/batten/tests/it/suite_subjects.rs @@ -21,7 +21,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/surface.rs b/crates/batten/tests/it/surface.rs similarity index 95% rename from crates/batten/tests/surface.rs rename to crates/batten/tests/it/surface.rs index 6595de0ed..9cb4bcb2f 100644 --- a/crates/batten/tests/surface.rs +++ b/crates/batten/tests/it/surface.rs @@ -18,36 +18,36 @@ //! genuinely missing is the SET half, which arrives here as //! `the_committed_artifacts_are_exactly_the_ones_the_surface_declares`. -// subsumed: mise-tasks/derived-check.sh crates/batten/src/surface.rs kind:mechanism crates/batten/tests/surface.rs -// subsumed: mise-tasks/man-pages.sh crates/batten/src/spec.rs kind:mechanism crates/batten/tests/surface.rs -// carried: tests/derived-check.bats crates/batten/src/surface.rs kind:mechanism crates/batten/tests/surface.rs +// subsumed: mise-tasks/derived-check.sh crates/batten/src/surface.rs kind:mechanism crates/batten/tests/it/surface.rs +// subsumed: mise-tasks/man-pages.sh crates/batten/src/spec.rs kind:mechanism crates/batten/tests/it/surface.rs +// carried: tests/derived-check.bats crates/batten/src/surface.rs kind:mechanism crates/batten/tests/it/surface.rs //! # RETIREMENT LEDGER — `tests/derived-check.bats`, 10 cases //! //! SUBSUMED — the assertion already stood here before the gate died. -// subsumed: "committed artifacts matching the surface exit 0" crates/batten/tests/surface.rs -// subsumed: "a drifted completion is reported with a pointer" crates/batten/tests/surface.rs -// subsumed: "a drifted man page is reported with a pointer" crates/batten/tests/surface.rs -// subsumed: "the gate leaves the tree it judges unmodified" crates/batten/tests/surface.rs -// subsumed: "every committed page's filename matches the .TH title inside it" crates/batten/tests/surface.rs -// subsumed: "this repo's committed artifacts match its surface — the gate on the real tree" crates/batten/tests/surface.rs +// subsumed: "committed artifacts matching the surface exit 0" crates/batten/tests/it/surface.rs +// subsumed: "a drifted completion is reported with a pointer" crates/batten/tests/it/surface.rs +// subsumed: "a drifted man page is reported with a pointer" crates/batten/tests/it/surface.rs +// subsumed: "the gate leaves the tree it judges unmodified" crates/batten/tests/it/surface.rs +// subsumed: "every committed page's filename matches the .TH title inside it" crates/batten/tests/it/surface.rs +// subsumed: "this repo's committed artifacts match its surface — the gate on the real tree" crates/batten/tests/it/surface.rs //! CARRIED — the three cells the existing tier was blind to, closed by the one //! new assertion this row writes. -// carried: "a missing artifact is reported rather than silently skipped" crates/batten/tests/surface.rs -// carried: "a page the surface no longer derives is reported as an orphan" crates/batten/tests/surface.rs -// carried: "the derived page list names the root page with an empty command path" crates/batten/tests/surface.rs +// carried: "a missing artifact is reported rather than silently skipped" crates/batten/tests/it/surface.rs +// carried: "a page the surface no longer derives is reported as an orphan" crates/batten/tests/it/surface.rs +// carried: "the derived page list names the root page with an empty command path" crates/batten/tests/it/surface.rs //! CHANGED — behaviour that diverges deliberately, with its reason. -// changed: "output is pointer-only — no artifact body echoed" crates/batten/tests/surface.rs the gate wrote findings to stderr, where non-negotiable rule 4 binds and a page body would have been the payload; a failing assertion here is a developer diagnostic on a local run rather than a finding a gate emits, and `assert_eq!` over the bytes is what makes a drift readable at all. The SET assertion below is pointer-only in the rule's own sense — it names paths and never opens a file +// changed: "output is pointer-only — no artifact body echoed" crates/batten/tests/it/surface.rs the gate wrote findings to stderr, where non-negotiable rule 4 binds and a page body would have been the payload; a failing assertion here is a developer diagnostic on a local run rather than a finding a gate emits, and `assert_eq!` over the bytes is what makes a drift readable at all. The SET assertion below is pointer-only in the rule's own sense — it names paths and never opens a file // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::collections::BTreeSet; use std::fs; diff --git a/crates/batten/tests/symbols.rs b/crates/batten/tests/it/symbols.rs similarity index 100% rename from crates/batten/tests/symbols.rs rename to crates/batten/tests/it/symbols.rs diff --git a/crates/batten/tests/it/target_consolidation.rs b/crates/batten/tests/it/target_consolidation.rs new file mode 100644 index 000000000..1e4eac934 --- /dev/null +++ b/crates/batten/tests/it/target_consolidation.rs @@ -0,0 +1,75 @@ +//! Grouping 144 test targets into one changed nothing a case can observe +//! (CLOUD-1210). +//! +//! # Why this is asserted rather than cited +//! +//! nextest's design doc states the property plainly — "a key factor +//! distinguishing nextest from `cargo test` is that nextest runs **each test in a +//! separate process**", giving memory isolation, state isolation and independent +//! termination. So consolidation changes the LINK COUNT and not what a test can +//! see. +//! +//! That claim is load-bearing: if it were false, CLOUD-1210 would be trading +//! isolation for build speed, which is a trade nobody agreed to. A citation is +//! not a mechanism, and the runner is a pinned tool that could change — so the +//! property ships as a case rather than as a sentence in a commit message. +//! +//! # What each case actually discriminates +//! +//! The three below are chosen so that a runner sharing a process between tests +//! would red at least one of them, and so that none of them can pass vacuously: +//! each first ESTABLISHES the state it is about, then asserts nobody else sees +//! it. + +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use std::sync::atomic::{AtomicU32, Ordering}; + +/// Process-global, and deliberately so. Under one process per test each case +/// sees its own zero; sharing a process would let whichever ran second see the +/// first's increment. +static TOUCHED: AtomicU32 = AtomicU32::new(0); + +#[test] +fn a_grouped_test_starts_from_a_fresh_process_state() { + assert_eq!( + TOUCHED.fetch_add(1, Ordering::SeqCst), + 0, + "this case sees a zeroed static, so it is not sharing a process with its \ + siblings — the isolation nextest documents and CLOUD-1210 relies on" + ); +} + +#[test] +fn a_sibling_in_the_same_target_does_not_see_that_state() { + // The same assertion from the other side. Whichever of the two runs second + // would see 1 rather than 0 if the target boundary were what provided + // isolation, because they are now in ONE target where they used to be in two. + assert_eq!( + TOUCHED.fetch_add(1, Ordering::SeqCst), + 0, + "grouping put these two cases in one binary; they must still each get a \ + process, or consolidation would have traded isolation for link time" + ); +} + +/// The other thing grouping could plausibly have broken, and the one every other +/// case in this tree depends on. +/// +/// `CARGO_BIN_EXE_` is set by cargo PER TEST TARGET, and `common::batten()` +/// reads it to find the binary under test. Consolidating 144 targets into one +/// changes which target that variable is set for, so a migration that got this +/// wrong would leave the whole end-to-end tier running some other `batten` off +/// `PATH` — or nothing — which is CLOUD-592's silent-stale-artifact failure +/// arriving through a different door. Asserted rather than assumed, because it is +/// the assumption the other 145 modules are built on. +#[test] +fn the_binary_under_test_is_still_addressable_from_the_grouped_target() { + let path = std::path::Path::new(env!("CARGO_BIN_EXE_batten")); + assert!( + path.is_file(), + "CARGO_BIN_EXE_batten must resolve to the built binary from inside the \ + grouped target: {}", + path.display() + ); +} diff --git a/crates/batten/tests/target_prune.rs b/crates/batten/tests/it/target_prune.rs similarity index 96% rename from crates/batten/tests/target_prune.rs rename to crates/batten/tests/it/target_prune.rs index ac5d508a4..871bee15d 100644 --- a/crates/batten/tests/target_prune.rs +++ b/crates/batten/tests/it/target_prune.rs @@ -43,25 +43,25 @@ // The file granularity: each deleted path, and the two successors that hold what // it held. // -// changed: mise-tasks/target-prune.sh crates/batten/src/prune.rs kind:verb crates/batten/tests/target_prune.rs -// changed: tests/target-prune.bats crates/batten/src/prune.rs kind:verb crates/batten/tests/target_prune.rs +// changed: mise-tasks/target-prune.sh crates/batten/src/prune.rs kind:verb crates/batten/tests/it/target_prune.rs +// changed: tests/target-prune.bats crates/batten/src/prune.rs kind:verb crates/batten/tests/it/target_prune.rs // // The retention rule. Seven cases, and every one of them is a property of the // reclaim rather than of the program that ran it, so they port straight across // into the module's own tier where they need no fixture tree at all. // -// carried: "the newest K copies survive and the rest are removed" crates/batten/tests/target_prune.rs -// carried: "THE SPARE IS KEPT, so a reverted lap is not a full rebuild" crates/batten/tests/target_prune.rs -// carried: "a stem with fewer than K copies is untouched" crates/batten/tests/target_prune.rs +// carried: "the newest K copies survive and the rest are removed" crates/batten/tests/it/target_prune.rs +// carried: "THE SPARE IS KEPT, so a reverted lap is not a full rebuild" crates/batten/tests/it/target_prune.rs +// carried: "a stem with fewer than K copies is untouched" crates/batten/tests/it/target_prune.rs // carried: "stems are grouped separately — one binary's copies never count as another's" crates/batten/src/prune.rs kind:verb -// carried: "NOTHING OUTSIDE deps IS CONSIDERED — a cache is not a superseded artifact" crates/batten/tests/target_prune.rs -// carried: "a cross-target deps directory is pruned too, on the same rule" crates/batten/tests/target_prune.rs -// changed: "a non-executable file beside the artifacts is left alone" crates/batten/tests/target_prune.rs the scope is a KIND rather than the executable bit (CLOUD-1157). The bit was never the property worth pinning — it made `.rlib`, `.rmeta` and `.so` unreachable however many copies accumulated, while reading as a safety check — so the case is `a_file_of_a_kind_this_pass_does_not_reclaim_is_left_alone` and asserts the same thing about `.d` and about anything unrecognised, with three copies of one `.d` stem so it cannot pass by never reaching `keep`. +// carried: "NOTHING OUTSIDE deps IS CONSIDERED — a cache is not a superseded artifact" crates/batten/tests/it/target_prune.rs +// carried: "a cross-target deps directory is pruned too, on the same rule" crates/batten/tests/it/target_prune.rs +// changed: "a non-executable file beside the artifacts is left alone" crates/batten/tests/it/target_prune.rs the scope is a KIND rather than the executable bit (CLOUD-1157). The bit was never the property worth pinning — it made `.rlib`, `.rmeta` and `.so` unreachable however many copies accumulated, while reading as a safety check — so the case is `a_file_of_a_kind_this_pass_does_not_reclaim_is_left_alone` and asserts the same thing about `.d` and about anything unrecognised, with three copies of one `.d` stem so it cannot pass by never reaching `keep`. // // The output contract. // -// carried: "the report is a count and bytes, never a path listing" crates/batten/tests/target_prune.rs -// changed: "the report names the floor beside the free space, so both numbers travel" crates/batten/tests/target_prune.rs the report now names the floor's BASIS beside it, because there are two floors and a number alone cannot say which one is in force or why. A `carried` arm would claim the assertion is unchanged when it is strictly stronger. +// carried: "the report is a count and bytes, never a path listing" crates/batten/tests/it/target_prune.rs +// changed: "the report names the floor beside the free space, so both numbers travel" crates/batten/tests/it/target_prune.rs the report now names the floor's BASIS beside it, because there are two floors and a number alone cannot say which one is in force or why. A `carried` arm would claim the assertion is unchanged when it is strictly stronger. // // The argument surface. All three were assertions about a hand-rolled `while` // loop over `$@`, and clap owns that now: an unknown flag, a missing value and @@ -72,7 +72,7 @@ // // subsumed: "--root with no value is refused, and does not hang" crates/batten/src/surface.rs kind:mechanism // subsumed: "an unknown flag is a usage error" crates/batten/src/surface.rs kind:mechanism -// changed: "an absent build directory is exit 2, never a silent pass" crates/batten/tests/target_prune.rs could-not-look is exit 3 rather than exit 2 under the engine's contract, which reserves 2 for a violation with no per-verb exception (house-style §6-§7). The predecessor was a standalone program with its own two-code table; what must NOT change is that it is non-zero and names what was not examined, and that is what the ported case asserts. +// changed: "an absent build directory is exit 2, never a silent pass" crates/batten/tests/it/target_prune.rs could-not-look is exit 3 rather than exit 2 under the engine's contract, which reserves 2 for a violation with no per-verb exception (house-style §6-§7). The predecessor was a standalone program with its own two-code table; what must NOT change is that it is non-zero and names what was not examined, and that is what the ported case asserts. // // The budget's self-check. Five cases, and all five were a program parsing its // own source with a regex to prove a comment matched a variable. The floors are @@ -89,16 +89,16 @@ // // The order, and the refusal it protects. // -// changed: "THE ORDER IS LOAD-BEARING: a prunable tree is never refused for being over budget" crates/batten/tests/target_prune.rs the predecessor asserted the order by comparing two LINE NUMBERS in its own source, which is the only instrument a shell suite had, and this arm claimed the port ran the thing instead. It does not, and #734's review is what caught the overclaim: the free-space seam is DECLARED, so a reading cannot rise because the reclaim freed something, and no fixture can put the judgement on the far side of a reclaim that moved it. What the ported case does assert is the weaker half that is still worth having — a tree full of superseded copies reclaims them and comes back exit 0 rather than being refused. The order itself is asserted where it IS expressible: `escalating_judges_against_the_cold_floor_it_just_created` only reaches a second reading because the reclaim ran first. -// carried: "a tree still below the floor after pruning is refused" crates/batten/tests/target_prune.rs -// carried: "the refusal explains how exhaustion would otherwise present" crates/batten/tests/target_prune.rs +// changed: "THE ORDER IS LOAD-BEARING: a prunable tree is never refused for being over budget" crates/batten/tests/it/target_prune.rs the predecessor asserted the order by comparing two LINE NUMBERS in its own source, which is the only instrument a shell suite had, and this arm claimed the port ran the thing instead. It does not, and #734's review is what caught the overclaim: the free-space seam is DECLARED, so a reading cannot rise because the reclaim freed something, and no fixture can put the judgement on the far side of a reclaim that moved it. What the ported case does assert is the weaker half that is still worth having — a tree full of superseded copies reclaims them and comes back exit 0 rather than being refused. The order itself is asserted where it IS expressible: `escalating_judges_against_the_cold_floor_it_just_created` only reaches a second reading because the reclaim ran first. +// carried: "a tree still below the floor after pruning is refused" crates/batten/tests/it/target_prune.rs +// carried: "the refusal explains how exhaustion would otherwise present" crates/batten/tests/it/target_prune.rs // // CLOUD-861's escalation, and the two rows CLOUD-1030 is about. The first two // carry: the escalation still runs only when the floor is breached, and a tree // above it still keeps its cache. The third is retired outright. // -// carried: "CLOUD-861: a tree below the floor escalates and drops the incremental cache" crates/batten/tests/target_prune.rs -// carried: "CLOUD-861: a tree ABOVE the floor keeps its incremental cache" crates/batten/tests/target_prune.rs +// carried: "CLOUD-861: a tree below the floor escalates and drops the incremental cache" crates/batten/tests/it/target_prune.rs +// carried: "CLOUD-861: a tree ABOVE the floor keeps its incremental cache" crates/batten/tests/it/target_prune.rs // withdrawn: "CLOUD-861: the floor's basis names a lap no observed lap exceeds" it grepped the task's own source for `worst-lap=6242mb`, pinning ONE literal against ONE stale predecessor. The floors are config now and `Floor::validate` decides the arithmetic for whatever they say, so the case has no source line to read and the property it guarded is checked at load for every value rather than for one. // // CLOUD-778's hermeticity row. The seam widened — a comma-separated SEQUENCE @@ -106,18 +106,18 @@ // second reading to differ from the first, and a single value makes the two equal // by construction. // -// changed: "CLOUD-778: the prune verdict is identical above and below the floor" crates/batten/tests/target_prune.rs the seam is now the readings a run takes IN ORDER, with the last repeating, so every single-valued caller means what it meant and the escalation's second reading becomes expressible. +// changed: "CLOUD-778: the prune verdict is identical above and below the floor" crates/batten/tests/it/target_prune.rs the seam is now the readings a run takes IN ORDER, with the last repeating, so every single-valued caller means what it meant and the escalation's second reading becomes expressible. // // The fresh-clone path. // -// carried: "an unbuilt tree beside a Cargo.toml prunes nothing and still judges the floor" crates/batten/tests/target_prune.rs -// carried: "an unbuilt tree below the floor still refuses, so the fresh-clone path is not an escape" crates/batten/tests/target_prune.rs -// changed: "no target and no Cargo.toml is still could-not-look" crates/batten/tests/target_prune.rs exit 3 rather than exit 2, for the reason the absent-build-directory row above states: the engine's contract reserves 2 for a violation and this is not one. +// carried: "an unbuilt tree beside a Cargo.toml prunes nothing and still judges the floor" crates/batten/tests/it/target_prune.rs +// carried: "an unbuilt tree below the floor still refuses, so the fresh-clone path is not an escape" crates/batten/tests/it/target_prune.rs +// changed: "no target and no Cargo.toml is still could-not-look" crates/batten/tests/it/target_prune.rs exit 3 rather than exit 2, for the reason the absent-build-directory row above states: the engine's contract reserves 2 for a violation and this is not one. // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; @@ -719,9 +719,9 @@ fn based(name: &str, count: usize, files: &[(&str, &str)]) -> PathBuf { /// Four tracked files under the basis glob. const BASIS_FILES: &[(&str, &str)] = &[ - ("crates/batten/tests/one.rs", "// one\n"), - ("crates/batten/tests/two.rs", "// two\n"), - ("crates/batten/tests/three.rs", "// three\n"), + ("crates/batten/tests/it/one.rs", "// one\n"), + ("crates/batten/tests/it/two.rs", "// two\n"), + ("crates/batten/tests/it/three.rs", "// three\n"), ("crates/batten/src/lib.rs", "// lib\n"), ]; diff --git a/crates/batten/tests/task_prose.rs b/crates/batten/tests/it/task_prose.rs similarity index 99% rename from crates/batten/tests/task_prose.rs rename to crates/batten/tests/it/task_prose.rs index 6559ee2c9..25e39bbf3 100644 --- a/crates/batten/tests/task_prose.rs +++ b/crates/batten/tests/it/task_prose.rs @@ -28,7 +28,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; diff --git a/crates/batten/tests/task_receipt.rs b/crates/batten/tests/it/task_receipt.rs similarity index 99% rename from crates/batten/tests/task_receipt.rs rename to crates/batten/tests/it/task_receipt.rs index 156a457e2..334b4c501 100644 --- a/crates/batten/tests/task_receipt.rs +++ b/crates/batten/tests/it/task_receipt.rs @@ -21,7 +21,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/it/test_targets.rs b/crates/batten/tests/it/test_targets.rs new file mode 100644 index 000000000..b2573bf09 --- /dev/null +++ b/crates/batten/tests/it/test_targets.rs @@ -0,0 +1,227 @@ +//! `policy/test-targets.rego` over the COMPILED engine (CLOUD-1210). +//! +//! # Why this file exists when the module already has `test_` rules +//! +//! Those are the load-time tier and they pin the PREDICATE. They cannot pin that +//! the engine BUILDS the input the predicate reads: `with input as` fabricates +//! the very shape the engine may be unable to produce, so a module reading a key +//! nothing fills passes its own suite green and enforces nothing. +//! +//! `.claude/rules/policy-modules.md` records that class twice over — a module +//! copied from `policy.rs`'s own doc iterated a tree key the engine never built, +//! and OpenTelemetry's `weaver` printed "No policy violation", exit 0, over a +//! knowingly-broken registry because its module read a key the v1 schema does not +//! build. Both live instances in this tree were found by adding this tier, not by +//! reading. So the cases below drive `run_static` over a real fixture repository +//! with a real base ref, and the fact under test — `input.tree["base-delta"]` — is +//! one the engine has to resolve from git rather than one a harness hands over. +//! +//! # What the rule is for +//! +//! Cargo autodiscovers one test target per top-level `crates/batten/tests/*.rs` +//! and rustc relinks the whole dependency closure into each. This repository had +//! 144 of them; CLOUD-1210 grouped them into one. The ratchet is what stops the +//! count regrowing, and it has to be survivable by CLOUD-843's campaign, which +//! adds a `crates/batten/tests/*.rs` tier per retired gate BY MANDATE — hence a +//! rule about TOP-LEVEL paths rather than about test files. + +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use crate::common; + +use std::fs; +use std::path::{Path, PathBuf}; + +use batten::rules::{self, Rule}; + +/// The predicate id the module declares. +const TARGET_ADDED: &str = "test-target-added"; + +/// A fixture repository whose base is one commit back and whose working tree +/// ADDS `changed`, so the engine's own `base-delta` resolution is what produces +/// the fact under test. +/// +/// `origin/main` is a local ref pointed at the base commit: `base_delta` resolves +/// a rev, and configuring a remote would make every case here depend on the +/// network for an entirely local question. Same shape as `filed_here.rs`. +fn repo(name: &str, added: &[&str]) -> PathBuf { + let root = common::scratch(name); + common::git_in(&root, &["init", "--quiet"]); + common::git_in(&root, &["config", "user.email", "t@example.com"]); + common::git_in(&root, &["config", "user.name", "t"]); + fs::write(root.join("seed.txt"), "seed\n").expect("seed"); + common::git_in(&root, &["add", "-A"]); + common::git_in(&root, &["commit", "--quiet", "-m", "base"]); + let base = common::git_in(&root, &["rev-parse", "HEAD"]); + common::git_in(&root, &["update-ref", "refs/remotes/origin/main", &base]); + + for path in added { + let full = root.join(path); + if let Some(parent) = full.parent() { + fs::create_dir_all(parent).expect("scratch parent"); + } + fs::write(full, "// added\n").expect("write added file"); + } + + install_module(&root); + root +} + +/// The COMMITTED module, copied rather than re-typed. A fixture carrying its own +/// copy of the predicate would pass while the shipped one was broken, which is +/// the fidelity failure this tier exists to catch. +fn install_module(root: &Path) { + let source = common::at_root("policy/test-targets.rego") + .canonicalize() + .expect("the committed module is where the row says it is"); + fs::create_dir_all(root.join("policy")).expect("scratch policy dir"); + fs::copy(source, root.join("policy/test-targets.rego")).expect("install committed module"); +} + +/// The committed row's shape, so a registration the loader would reject cannot +/// pass here. +fn row() -> Rule { + serde_json::from_value(serde_json::json!({ + "id": "test-targets", + "kind": "policy", + "scope": "tree", + "base": "origin/main", + "delta_sources": ["**"], + "module": "policy/test-targets.rego", + "severity": "deny", + })) + .expect("the loader accepts the committed row's shape") +} + +fn scan(root: &Path) -> rules::Scan { + let verdicts = common::verdicts_in(root); + rules::run_static( + &[row()], + &[], + batten::policy::Vocabulary { + patterns: &[], + verdicts: &verdicts, + recorders: &[], + }, + root, + ) + .expect("the read surface runs a policy row") +} + +fn verdicts(root: &Path) -> Vec { + scan(root) + .findings + .into_iter() + .map(|finding| finding.rule) + .collect() +} + +fn pointers(root: &Path) -> Vec { + scan(root) + .findings + .into_iter() + .map(|finding| finding.path) + .collect() +} + +// --------------------------------------------------------------------------- +// The pass side first: without it every refusal below is satisfied by a module +// that refuses everything. +// --------------------------------------------------------------------------- + +#[test] +fn a_branch_adding_no_test_file_passes_untouched() { + let root = repo("test-targets-clean", &["src/lib.rs"]); + assert!( + verdicts(&root).is_empty(), + "a diff that mints no cargo test target is not this rule's business" + ); +} + +#[test] +fn a_new_top_level_test_file_is_refused_over_the_compiled_engine() { + let root = repo("test-targets-added", &["crates/batten/tests/new_gate.rs"]); + assert_eq!( + verdicts(&root), + vec![TARGET_ADDED.to_owned()], + "a top-level crates/batten/tests/*.rs is a cargo test target, and the \ + engine's own base-delta is what has to surface it" + ); + assert_eq!( + pointers(&root), + vec!["crates/batten/tests/new_gate.rs".to_owned()], + "the finding points at the file that mints the target, and nothing else" + ); +} + +/// THE CASE THAT MAKES THE RULE SURVIVABLE, and the one an implementer would +/// skip. `.claude/rules/toolchain.md` requires every retirement to land a +/// `crates/batten/tests/*.rs` tier, so a rule refusing every added test file +/// would fire on the next correctly-executed retirement and the campaign would +/// have to switch it off — the shape a gate does not survive. A tier landing as a +/// `mod` inside the group must be invisible to it. +#[test] +fn a_module_inside_the_group_is_not_a_target() { + let root = repo( + "test-targets-grouped", + &["crates/batten/tests/it/new_gate.rs"], + ); + assert!( + verdicts(&root).is_empty(), + "a file one segment deeper is a module in an existing target, not a new \ + one — this is what keeps CLOUD-843's campaign able to land its tiers" + ); +} + +/// ANTI-VACUITY on the depth test. `crates/other/tests/x.rs` has the same shape +/// and the same segment count, so a rule anchored only on depth would refuse a +/// sibling crate's targets — and one anchored wrongly would refuse nothing at all +/// while still passing every case above. +#[test] +fn another_crates_test_file_is_not_this_rules_business() { + let root = repo( + "test-targets-other-crate", + &["crates/other/tests/new_gate.rs"], + ); + assert!( + verdicts(&root).is_empty(), + "the rule is about THIS crate's autodiscovered targets" + ); +} + +/// Fixture data under `tests/` is not a target, however deep it sits. +#[test] +fn a_fixture_file_is_not_a_target() { + let root = repo( + "test-targets-fixture", + &["crates/batten/tests/fixtures/hooks/new.json"], + ); + assert!( + verdicts(&root).is_empty(), + "only a .rs file directly under tests/ mints a target" + ); +} + +/// Every added target is named, not just the first — a gate reporting one of +/// three reads as satisfied once that one is moved. +#[test] +fn every_added_target_is_named() { + let root = repo( + "test-targets-several", + &[ + "crates/batten/tests/a_gate.rs", + "crates/batten/tests/b_gate.rs", + "crates/batten/tests/it/c_gate.rs", + ], + ); + let mut named = pointers(&root); + named.sort(); + assert_eq!( + named, + vec![ + "crates/batten/tests/a_gate.rs".to_owned(), + "crates/batten/tests/b_gate.rs".to_owned(), + ], + "both top-level additions are reported and the grouped one is not" + ); +} diff --git a/crates/batten/tests/todo_promotion.rs b/crates/batten/tests/it/todo_promotion.rs similarity index 99% rename from crates/batten/tests/todo_promotion.rs rename to crates/batten/tests/it/todo_promotion.rs index 3e8b3a16d..5c5f6b43c 100644 --- a/crates/batten/tests/todo_promotion.rs +++ b/crates/batten/tests/it/todo_promotion.rs @@ -37,7 +37,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; @@ -51,7 +51,7 @@ use common::{Fixture, run_with_stdin, stderr}; /// compile time, so a row edited in `batten.toml` is exercised by the next run /// rather than by whoever remembers to update a duplicate. fn repo(name: &str) -> PathBuf { - let staged = Fixture::new(name).config(include_str!("../../../batten.toml")); + let staged = Fixture::new(name).config(include_str!("../../../../batten.toml")); // Copied by ENUMERATION rather than by name, and staged before the commit so // they are tracked like the config is: naming a consumer's policy filenames // in `crates/**` is non-negotiable rule 1's violation, and `no-consumer-repo- diff --git a/crates/batten/tests/tool_selector.rs b/crates/batten/tests/it/tool_selector.rs similarity index 99% rename from crates/batten/tests/tool_selector.rs rename to crates/batten/tests/it/tool_selector.rs index 4cde3ae2a..dba1320ce 100644 --- a/crates/batten/tests/tool_selector.rs +++ b/crates/batten/tests/it/tool_selector.rs @@ -30,7 +30,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/tool_verdict_facts.rs b/crates/batten/tests/it/tool_verdict_facts.rs similarity index 99% rename from crates/batten/tests/tool_verdict_facts.rs rename to crates/batten/tests/it/tool_verdict_facts.rs index 31eed1307..297f95e1d 100644 --- a/crates/batten/tests/tool_verdict_facts.rs +++ b/crates/batten/tests/it/tool_verdict_facts.rs @@ -93,7 +93,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/use_graph.rs b/crates/batten/tests/it/use_graph.rs similarity index 100% rename from crates/batten/tests/use_graph.rs rename to crates/batten/tests/it/use_graph.rs diff --git a/crates/batten/tests/verdict_registry.rs b/crates/batten/tests/it/verdict_registry.rs similarity index 99% rename from crates/batten/tests/verdict_registry.rs rename to crates/batten/tests/it/verdict_registry.rs index bae839820..fd64eaf70 100644 --- a/crates/batten/tests/verdict_registry.rs +++ b/crates/batten/tests/it/verdict_registry.rs @@ -24,7 +24,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/waivers.rs b/crates/batten/tests/it/waivers.rs similarity index 93% rename from crates/batten/tests/waivers.rs rename to crates/batten/tests/it/waivers.rs index ccc6a687e..a47abd206 100644 --- a/crates/batten/tests/waivers.rs +++ b/crates/batten/tests/it/waivers.rs @@ -19,7 +19,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; @@ -118,7 +118,7 @@ fn without_a_waiver_the_rule_denies() { assert!(stdout.contains("lib.rs:2 no-todo"), "got: {stdout}"); } -// subsumed: "an exempted entry passes only through a waiver carrying a reason" crates/batten/tests/waivers.rs that case was about the waiver SURFACE rather than about `no-source-built-tool` — a live waiver clears the verdict and leaves a pointer-only audit line on stderr — and this drives the compiled binary over a `forbid` row to assert exactly that (CLOUD-1137) +// subsumed: "an exempted entry passes only through a waiver carrying a reason" crates/batten/tests/it/waivers.rs that case was about the waiver SURFACE rather than about `no-source-built-tool` — a live waiver clears the verdict and leaves a pointer-only audit line on stderr — and this drives the compiled binary over a `forbid` row to assert exactly that (CLOUD-1137) #[test] fn a_live_waiver_clears_the_verdict_and_audits_on_stderr() { let (repo, home) = repo("waiver-live", &format!("{RULE}{}", waiver(LIVE))); @@ -144,7 +144,7 @@ fn a_live_waiver_clears_the_verdict_and_audits_on_stderr() { ); } -// subsumed: "an exemption that has lapsed stops exempting, with nobody acting" crates/batten/tests/waivers.rs the property that makes a waiver an exemption rather than a deletion is generic over which row it names, and this asserts it on the same shape (CLOUD-1137) +// subsumed: "an exemption that has lapsed stops exempting, with nobody acting" crates/batten/tests/it/waivers.rs the property that makes a waiver an exemption rather than a deletion is generic over which row it names, and this asserts it on the same shape (CLOUD-1137) #[test] fn a_lapsed_waiver_leaves_the_finding_and_the_verdict_alone() { // The property the whole design rests on, at the surface a caller sees: @@ -218,7 +218,7 @@ fn two_runs_over_one_config_and_one_date_are_byte_identical() { assert_eq!(first, second); } -// subsumed: "the exemption is scoped to what it names: a second violation still blocks" crates/batten/tests/waivers.rs the scoping half is here and the channel half — the waived finding absent from the ANSWER channel the exit code was computed from — is `the_data_channel_never_mentions_the_waived_finding_or_the_waiver` just above (CLOUD-1137) +// subsumed: "the exemption is scoped to what it names: a second violation still blocks" crates/batten/tests/it/waivers.rs the scoping half is here and the channel half — the waived finding absent from the ANSWER channel the exit code was computed from — is `the_data_channel_never_mentions_the_waived_finding_or_the_waiver` just above (CLOUD-1137) #[test] fn a_narrowed_waiver_leaves_the_rest_of_the_rule_gating() { let narrowed = format!( @@ -241,7 +241,7 @@ fn a_narrowed_waiver_leaves_the_rest_of_the_rule_gating() { assert!(stderr.contains("waived vendor/dep.rs:1"), "got: {stderr}"); } -// subsumed: "an exemption with no reason is refused as bad input, not applied" crates/batten/tests/waivers.rs exit 1 is a statement about the invocation and never a policy verdict, which is what this asserts, generic over the row waived (CLOUD-1137) +// subsumed: "an exemption with no reason is refused as bad input, not applied" crates/batten/tests/it/waivers.rs exit 1 is a statement about the invocation and never a policy verdict, which is what this asserts, generic over the row waived (CLOUD-1137) #[test] fn a_waiver_with_no_reason_is_a_usage_error_not_a_verdict() { // Refused at load, and as bad *input* — exit 1. Reporting it as 2 would tell diff --git a/crates/batten/tests/walker.rs b/crates/batten/tests/it/walker.rs similarity index 99% rename from crates/batten/tests/walker.rs rename to crates/batten/tests/it/walker.rs index 2f68a0f53..3d87ad417 100644 --- a/crates/batten/tests/walker.rs +++ b/crates/batten/tests/it/walker.rs @@ -24,7 +24,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/wiring_reclaim.rs b/crates/batten/tests/it/wiring_reclaim.rs similarity index 99% rename from crates/batten/tests/wiring_reclaim.rs rename to crates/batten/tests/it/wiring_reclaim.rs index 2fb143a96..f1ac73eb3 100644 --- a/crates/batten/tests/wiring_reclaim.rs +++ b/crates/batten/tests/it/wiring_reclaim.rs @@ -39,7 +39,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::{Path, PathBuf}; diff --git a/crates/batten/tests/zero_config.rs b/crates/batten/tests/it/zero_config.rs similarity index 99% rename from crates/batten/tests/zero_config.rs rename to crates/batten/tests/it/zero_config.rs index eb0715c56..da1c633b3 100644 --- a/crates/batten/tests/zero_config.rs +++ b/crates/batten/tests/it/zero_config.rs @@ -28,7 +28,7 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] -mod common; +use crate::common; use std::path::PathBuf; diff --git a/crates/batten/tests/policy_modules.rs b/crates/batten/tests/policy_modules.rs index 967d6bb50..a43ce1906 100644 --- a/crates/batten/tests/policy_modules.rs +++ b/crates/batten/tests/policy_modules.rs @@ -19,6 +19,16 @@ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] +// THE ONE TARGET THAT STAYS SEPARATE (CLOUD-1210). `evaluator-io-check` +// probes this file with `cargo test --test policy_modules`, and that task is +// a governed `mise-tasks/*.sh`: `shell-retirement` gives it exactly two +// shapes — retire it whole, or leave it alone — so repointing the probe at +// the group is not an edit this change may make. Keeping the target is the +// cheaper half of that trade: one extra link against a gate that stays live. +// +// `#[path]` because `common/` moved into the group with everything else, and +// there is deliberately ONE copy of the fixture materializer (CLOUD-63). +#[path = "it/common/mod.rs"] mod common; use std::fs; diff --git a/hk.pkl b/hk.pkl index 94ace567d..977dc58e2 100644 --- a/hk.pkl +++ b/hk.pkl @@ -676,7 +676,7 @@ local gate = new Mapping { List( "skills/**", ".claude/skills/**", - "crates/batten/tests/skill_contract.rs", + "crates/batten/tests/it/skill_contract.rs", "crates/batten/src/surface.rs", "crates/batten/src/exit.rs", ) diff --git a/mise.toml b/mise.toml index aa394f7d1..468d4fdc2 100644 --- a/mise.toml +++ b/mise.toml @@ -1032,17 +1032,17 @@ if ! cargo nextest run --workspace; then exit 1; fi # inputs would be minted by a run of one target and then answer for the rest. [tasks."test:reference-coverage"] description = "Gate: the rendered CLI reference and the command spec name exactly the same flags, in both directions (CLOUD-171)" -run = "cargo nextest run -p batten --no-tests=fail -E 'binary(reference_coverage)'" +run = "cargo nextest run -p batten --no-tests=fail -E 'binary(it) & test(/^reference_coverage::/)'" env = { BATTEN_TEST_SCRATCH_LANE = "narrow" } [tasks."test:skill-contract"] description = "Gate: every shipped skill stays inside its line budget and describes only the surface the binary ships (CLOUD-213)" -run = "cargo nextest run -p batten --no-tests=fail -E 'binary(skill_contract)'" +run = "cargo nextest run -p batten --no-tests=fail -E 'binary(it) & test(/^skill_contract::/)'" env = { BATTEN_TEST_SCRATCH_LANE = "narrow" } [tasks."test:config-schema"] description = "Gate: the committed JSON Schemas match the ones the binary derives from the config types (CLOUD-33)" -run = "cargo nextest run -p batten --no-tests=fail -E 'binary(config_schema)'" +run = "cargo nextest run -p batten --no-tests=fail -E 'binary(it) & test(/^config_schema::/)'" env = { BATTEN_TEST_SCRATCH_LANE = "narrow" } [tasks."test:hook-profile"] @@ -1050,7 +1050,7 @@ run = "cargo nextest run -p batten --no-tests=fail -E 'binary(hook_profile)'" [tasks."test:config-deprecations"] description = "Gate: no config key left the published schema without a deprecation window (CLOUD-360)" -run = "cargo nextest run -p batten --no-tests=fail -E 'binary(config_deprecations)'" +run = "cargo nextest run -p batten --no-tests=fail -E 'binary(it) & test(/^config_deprecations::/)'" env = { BATTEN_TEST_SCRATCH_LANE = "narrow" } [tasks.prose-only-check] diff --git a/policy/test-targets.rego b/policy/test-targets.rego new file mode 100644 index 000000000..6066e52aa --- /dev/null +++ b/policy/test-targets.rego @@ -0,0 +1,152 @@ +# CLOUD-1210's ratchet: the cargo test-target count does not grow. +# +# WHY A MODULE AND NOT `kind = "ratchet"`. That kind's own doc defines it as "the +# total occurrences of `pattern` across files matching `glob`" — a TOKEN COUNT +# INSIDE FILES. `tests-not-deleted` counts `#[test]`; `bash-surface-not-growing` +# counts `#MISE description=`. A cargo test-target count is a property of the +# DIRECTORY STRUCTURE plus Cargo's autodiscovery, and no `glob`+`pattern` pair +# computes it. So the ratchet is spelled as a predicate over paths. +# +# WHY THE PATH SPELLING IS SOUND HERE WHEN A FILE COUNT WAS NOT. Cargo +# autodiscovers one test target per TOP-LEVEL `crates/batten/tests/*.rs`, and a +# file one segment deeper — inside a group directory carrying `main.rs` — is not a +# target at all. So "no new top-level `crates/batten/tests/*.rs`" IS "the target +# count does not grow", exactly, rather than approximately. +# +# THAT DISTINCTION IS WHAT KEEPS CLOUD-843'S CAMPAIGN RUNNING, and it is the whole +# reason this is not a `[[ratchet]]` over files. `.claude/rules/toolchain.md` +# requires every retirement to land its predicate as a `policy/*.rego` module PLUS +# a `crates/batten/tests/*.rs` tier, so a retirement adds a test file BY MANDATE — +# which is why `prune.rs` recorded the count moving 110 -> 114 -> 118 across three +# readings in ten days, and why 142 -> 144 happened in eight commits. A gate +# refusing every added test FILE would fire on the next correctly-executed +# retirement and the campaign would have to switch it off: the shape a gate does +# not survive. A retirement landing its tier as a `mod` inside the group is +# invisible to this rule, which is the property that makes it survivable. +# +# NO SPAWN, AND NO BASE-REV POSITION TO TAKE. `input.tree["base-delta"]` is the +# comparison already — the engine resolved it — so the hard half of a ratchet +# spelling disappears rather than being solved. +# THE MUTATIONS, chosen to DISCRIMINATE rather than to be plausible. A mutation +# over a conjunct some other conjunct already excludes survives, and surviving is +# the only way you find out — so each names the case that must turn red. +# +# The first is not hypothetical: the depth test shipped as `== 5` in this file's +# first revision, which is exactly inverted, and the compiled tier caught it. +#MUTANT depth-may-invert|s@count(segments) == 4@count(segments) == 5@|a module inside the group is not a target, and a new top-level file is +#MUTANT extension-may-widen|s@endswith(path, ".rs")@true@|a fixture file under tests/ is not a target +# +#MUTANT-EXEMPT CLOUD-1210|no `tests/test-targets.bats` exists and none may: `.claude/rules/toolchain.md`'s two-shapes rule and `V-SHELL-RULE-ADDED` refuse adding an authored bats suite, and `mutant` resolves a gate's suite as `tests/$gate.bats`, so there is no named case a mutation could turn red. The second tier is `crates/batten/tests/it/test_targets.rs`, which drives the compiled engine over a real fixture repository with a real base ref — and is what caught the inverted depth test the first `#MUTANT` row above records +package batten + +import rego.v1 + +rules contains "test-target-added" + +# The branch's own diff. `base-delta` is NULL when the base rev does not resolve, +# so `added` does not hold and this rule goes silent — could-not-look, never a +# fabricated empty delta that would pass the gate on ignorance. That is +# `filed-here.rego`'s reading of the same fact and it is deliberate here too. +delta := input.tree["base-delta"] + +# A path is a NEW TEST TARGET when it is added, sits directly under +# `crates/batten/tests/`, and ends in `.rs`. +# +# `count(segments) == 4` is what makes it TOP-LEVEL: `crates/batten/tests/x.rs` +# splits to FOUR segments, while `crates/batten/tests/it/x.rs` splits to five and +# is a module rather than a target. Spelled as a segment count rather than a glob +# because the engine's globs use `literal_separator(true)`, so `*` already stops +# at a `/` — and stating the depth explicitly is what a reader can check against +# Cargo's autodiscovery rule without knowing that. +# +# THE COUNT WAS WRITTEN AS 5 AND THAT WAS EXACTLY INVERTED: it refused the grouped +# module and allowed the new target, which is the one direction that fails +# silently. The load-time cases below agreed with the mistake, because a +# `with input as` case can only be as right as its author. The compiled tier in +# `crates/batten/tests/it/test_targets.rs` is what caught it — which is the whole +# reason `.claude/rules/policy-modules.md` calls that second tier not optional. +added_target contains path if { + some path in delta.added + segments := split(path, "/") + count(segments) == 4 + segments[0] == "crates" + segments[1] == "batten" + segments[2] == "tests" + endswith(path, ".rs") +} + +violation contains { + "rule": "test-target-added", + "verdict": "V-TEST-TARGET-ADDED", + "subjects": [{"path": path}], +} if { + some path in added_target +} + +deny contains finding if { + some finding in violation +} + +# --- the module's own tier --------------------------------------------------- +# +# These pin the PREDICATE. What they cannot pin is that the engine builds the +# input the predicate reads — `with input as` fabricates the very shape the engine +# may be unable to produce — so `crates/batten/tests/it/test_targets.rs` runs the +# same questions over the compiled binary. Both tiers, per +# `.claude/rules/policy-modules.md`, and the second is not optional. + +test_a_new_top_level_test_file_is_refused if { + count(violation) == 1 with input as {"tree": {"base-delta": { + "added": ["crates/batten/tests/new_gate.rs"], + "edited": [], + "deleted": [], + }}} +} + +# THE CASE THAT MAKES THE RULE SURVIVABLE. A retirement's tier lands inside the +# group and must not be refused; without this the campaign switches the gate off. +test_a_module_inside_the_group_is_not_a_target if { + count(violation) == 0 with input as {"tree": {"base-delta": { + "added": ["crates/batten/tests/it/new_gate.rs"], + "edited": [], + "deleted": [], + }}} +} + +# An EDITED top-level file is not a new target. Only `added` mints one, and +# reading `edited` would refuse every change to a file that already exists. +test_an_edited_file_is_not_a_new_target if { + count(violation) == 0 with input as {"tree": {"base-delta": { + "added": [], + "edited": ["crates/batten/tests/it/walker.rs"], + "deleted": [], + }}} +} + +# A non-Rust file under `tests/` is fixture data, not a target. +test_a_fixture_file_is_not_a_target if { + count(violation) == 0 with input as {"tree": {"base-delta": { + "added": ["crates/batten/tests/fixtures/hooks/new.json"], + "edited": [], + "deleted": [], + }}} +} + +# ANTI-VACUITY on the depth check. A path under ANOTHER crate's `tests/` has the +# same shape and the same segment count, so without the `crates/batten` anchor +# this rule would refuse a sibling crate's targets — and with a wrong anchor it +# would refuse nothing at all and still pass every case above. +test_another_crates_test_file_is_not_this_rules_business if { + count(violation) == 0 with input as {"tree": {"base-delta": { + "added": ["crates/other/tests/new_gate.rs"], + "edited": [], + "deleted": [], + }}} +} + +# COULD NOT LOOK. A null `base-delta` must go silent rather than read as an empty +# diff — the distinction `filed-here.rego` records and the one a migration gate +# has to keep. +test_an_unresolvable_base_refuses_nothing if { + count(violation) == 0 with input as {"tree": {"base-delta": null}} +}