Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-build-thing/tests/buildgate_red_probe.rs

//! **The RED PROBE for the build gate — the part that makes it worth trusting.**
//!
//! A gate is a claim about defects it would catch. The only evidence for that claim is
//! having *seen it catch one*. Three agents on 2026-08-04 built hollow guards **while
//! hunting hollow guards**, and the red probe itself was blind four times over on
//! gunnar — so the standard here is: for every assertion, build a repo that really has
//! the defect, run the real gate, and assert it goes RED **with the right numbers and
//! the right names**; then remove the defect and assert the SAME gate goes GREEN.
//!
//! Both directions, every time. A test that only ever sees green cannot tell a working
//! gate from a dead one — and neither can one that only ever sees red.
//!
//! These fixtures are real cargo workspaces with a real `.nornir/build-variants.toml`,
//! compiled by real cargo. Nothing is mocked: `buildgate::run` is the same function
//! `cargo xtask buildgate` and the dwarves ⚒ Build Thing call. They are tiny (two
//! trivial bins, three test files), so the whole file is seconds, not the hours a
//! probe against korp's release atoms would cost.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use nornir_build_thing::buildgate::{self, Arms, GateSpec};

// ───────────────────────────── the fixture ─────────────────────────────

/// What the fixture should contain — each knob turns exactly one defect on or off, so
/// a red can be attributed to one cause rather than to "something in there".
#[derive(Clone, Copy)]
struct Fixture {
    /// Declare an atom that builds `app-extra` (the bin behind `required-features`).
    /// `false` ⇒ that bin is declared and never built. **Defect 1.**
    atom_for_extra: bool,
    /// Add a `tests/dark.rs` gated on a feature no atom carries. **Defect 2.**
    dark_test_file: bool,
    /// Write a reason for the unbuilt bin into `[build.allow_unbuilt_bins]`.
    excuse_extra: bool,
    /// Write a reason for the dark file into `[build.allow_dark_test_files]`.
    excuse_dark: bool,
    /// Write an excuse with no words in it.
    unreasoned_excuse: bool,
}

impl Fixture {
    /// No defects: both bins have an atom, every test file is reached.
    fn healthy() -> Self {
        Self {
            atom_for_extra: true,
            dark_test_file: false,
            excuse_extra: false,
            excuse_dark: false,
            unreasoned_excuse: false,
        }
    }
}

fn w(root: &Path, rel: &str, body: &str) {
    let p = root.join(rel);
    std::fs::create_dir_all(p.parent().unwrap()).unwrap();
    std::fs::write(&p, body).unwrap();
}

/// Materialise a real cargo workspace on disk.
///
/// `app` declares two bins over one `src/`: `app-main` (no `required-features`) and
/// `app-extra` (`required-features = ["extra"]`, and `extra` is NOT in `default`) —
/// the exact shape that makes `cargo build --bins` build one of two and exit 0.
fn build_fixture(f: Fixture) -> tempfile::TempDir {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();

    w(root, "Cargo.toml", "[workspace]\nmembers = [\"app\"]\nresolver = \"2\"\n");

    w(
        root,
        "app/Cargo.toml",
        r#"[package]
name = "app"
version = "0.0.0"
edition = "2021"
publish = false

[features]
default = []
extra = []

[[bin]]
name = "app-main"
path = "src/main.rs"

[[bin]]
name = "app-extra"
path = "src/extra.rs"
required-features = ["extra"]
"#,
    );

    // Both bins carry a `mod tests`, so the `--bin` arm has something to find.
    w(root, "app/src/main.rs", "fn main() {}\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn main_unit() {}\n}\n");
    w(root, "app/src/extra.rs", "fn main() {}\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn extra_unit() {}\n}\n");

    // Always compiled, always non-empty — the control. If this ever shows up as dark,
    // the gate is miscounting, not the fixture.
    w(root, "app/tests/always.rs", "#[test]\nfn always_here() {}\n");
    // Gated on `extra`: empty under `app-main`, populated under `app-extra`. This is
    // the file that proves the gate aggregates ACROSS atoms instead of firing on every
    // correctly-gated file.
    w(root, "app/tests/gated.rs", "#![cfg(feature = \"extra\")]\n#[test]\nfn gated_here() {}\n");
    if f.dark_test_file {
        // Gated on a feature NO atom carries: empty under every declared resolution.
        // Nothing compiles it, nothing runs it, and libtest says `ok`.
        w(root, "app/tests/dark.rs", "#![cfg(feature = \"nobody-has-this\")]\n#[test]\nfn never_compiled() {}\n");
        // The feature must exist or cargo errors on the cfg; it is simply never
        // selected by any atom.
        let m = root.join("app/Cargo.toml");
        let t = std::fs::read_to_string(&m).unwrap();
        std::fs::write(&m, t.replace("extra = []", "extra = []\nnobody-has-this = []")).unwrap();
    }

    let mut toml = String::from("[build]\nbase_suite = \"ORIGINAL\"\n\n");
    toml.push_str(
        "[[build.variant]]\nname = \"app-main\"\npackage = \"app\"\nbin = \"app-main\"\n\
         features = []\nprofile = \"dev\"\nrole = \"tool\"\nlinkage = \"not-applicable\"\n\
         kind = \"exe\"\nmember = \"app\"\nsuites = [\"ORIGINAL\"]\ndefault = true\n\n",
    );
    if f.atom_for_extra {
        toml.push_str(
            "[[build.variant]]\nname = \"app-extra\"\npackage = \"app\"\nbin = \"app-extra\"\n\
             features = [\"extra\"]\nprofile = \"dev\"\nrole = \"tool\"\n\
             linkage = \"not-applicable\"\nkind = \"exe\"\nmember = \"app\"\n\
             suites = [\"ORIGINAL\"]\n\n",
        );
    }
    if f.excuse_extra {
        toml.push_str(
            "[build.allow_unbuilt_bins]\napp-extra = \"deliberately not shipped in this \
             delivery; kept for the 2026 migration\"\n\n",
        );
    }
    if f.excuse_dark {
        toml.push_str(
            // The key MUST be quoted: `dark.rs = "…"` is a TOML dotted key and
            // declares a table `dark`, silently excusing nothing. The red probe
            // found exactly that, and the gate now reports it — see the
            // `an_unquoted_allowlist_key_is_red` probe below.
            "[build.allow_dark_test_files]\n\"dark.rs\" = \"waits on the vendor SDK that \
             nobody-has-this gates; tracked in the worklist\"\n\n",
        );
    }
    if f.unreasoned_excuse {
        toml.push_str("[build.allow_unbuilt_bins]\napp-extra = \"\"\n\n");
    }
    w(root, ".nornir/build-variants.toml", &toml);

    dir
}

/// Run the real gate over a fixture. One shared target dir across the whole file keeps
/// it to a single cold compile of `app`.
fn gate(root: &Path, arms: Arms) -> buildgate::GateOutcome {
    let target = PathBuf::from(
        std::env::var("CARGO_TARGET_DIR")
            .unwrap_or_else(|_| {
                format!("{}/scratch/cargo/edda-buildgate", std::env::var("HOME").unwrap_or_default())
            }),
    )
    .join("buildgate-probe");
    buildgate::run(&GateSpec::new(root).arms(arms).target_dir(target)).expect("the gate ran")
}

fn joined(o: &buildgate::GateOutcome) -> String {
    format!("{}\n{}", o.render(), o.failures.join("\n"))
}

// ───────────────────────────── assertion 1: declared bins vs built bins ─────────────────────────────

/// **RED PROBE — a declared bin that no atom builds.**
///
/// The defect is introduced by deleting the `app-extra` atom from the declaration,
/// which is exactly korp's shape: `korp-server`, `korp-cli`, `korp-solo`,
/// `korp-spark41` and `start-korp-demo` all carry `required-features` outside
/// `default`, so `cargo build --bins` skips them **silently and exits 0**.
///
/// Asserted BOTH ways in one test, because a red on its own proves only that something
/// is unhappy — it does not prove the gate is measuring the thing that changed.
#[test]
fn a_declared_bin_that_no_atom_builds_is_red_and_the_numbers_say_so() {
    // ── RED ───────────────────────────────────────────────────────────────────
    let broken = Fixture { atom_for_extra: false, ..Fixture::healthy() };
    let d = build_fixture(broken);
    let o = gate(d.path(), Arms { bins: true, tests: false });
    let text = joined(&o);

    assert!(!o.is_green(), "a bin no atom builds MUST be red:\n{text}");
    assert_eq!(o.bins.declared.len(), 2, "both bins are declared: {:?}", o.bins.declared);
    assert_eq!(o.bins.built.len(), 1, "only one was built: {:?}", o.bins.built);
    assert!(o.bins.built.contains("app-main"), "{:?}", o.bins.built);
    assert_eq!(o.bins.unbuilt, vec!["app-extra".to_string()], "the gate must NAME it");
    // The ratio, not the raw count — a count with no denominator hides the thing.
    assert!((o.bins.ratio() - 0.5).abs() < 1e-9, "1 of 2 = 50%, got {}", o.bins.ratio());
    // EVERY term the percentage is made of, not just the numerator's first half:
    // a sentence where `1 of 15` sits beside `40%` reads as broken arithmetic and
    // teaches the reader to stop reading the gate.
    assert!(text.contains("2 declared"), "the denominator must be in the text:\n{text}");
    assert!(text.contains("1 built"), "the built count must be in the text:\n{text}");
    assert!(text.contains("1/2 = 50% accounted for"), "the ratio must show its own numerator:\n{text}");
    assert!(text.contains("app-extra"), "the failure must be a work list:\n{text}");

    // ── GREEN, same gate, one line of declaration changed ─────────────────────
    let d = build_fixture(Fixture::healthy());
    let o = gate(d.path(), Arms { bins: true, tests: false });
    assert!(o.is_green(), "declaring the atom must make it pass:\n{}", joined(&o));
    assert_eq!(o.bins.built.len(), 2, "both bins built now: {:?}", o.bins.built);
    assert!(o.bins.unbuilt.is_empty());
    assert!((o.bins.ratio() - 1.0).abs() < 1e-9);
}

/// **The excuse path, both ways.** An unbuilt bin with a WRITTEN reason is green; the
/// same bin with an empty reason is red. Otherwise `[build.allow_unbuilt_bins]` becomes
/// a place to make a red go away by typing a key.
#[test]
fn an_unbuilt_bin_is_excusable_only_with_words() {
    let d = build_fixture(Fixture { atom_for_extra: false, excuse_extra: true, ..Fixture::healthy() });
    let o = gate(d.path(), Arms { bins: true, tests: false });
    assert!(o.is_green(), "a written reason excuses it:\n{}", joined(&o));
    assert_eq!(o.bins.excused.len(), 1, "and it is COUNTED as excused, not forgotten");
    assert!(o.bins.unbuilt.is_empty());
    // The denominator did not shrink: 1 built + 1 excused over 2 declared.
    assert_eq!(o.bins.declared.len(), 2);

    let d = build_fixture(Fixture {
        atom_for_extra: false,
        unreasoned_excuse: true,
        ..Fixture::healthy()
    });
    let o = gate(d.path(), Arms { bins: true, tests: false });
    assert!(!o.is_green(), "an empty reason is NOT a reason:\n{}", joined(&o));
    assert!(joined(&o).contains("no reason"), "{}", joined(&o));
}

/// **A stale excuse is red.** The bin IS built, and the allowlist still names it — left
/// alone, the list rots into a blanket that hides the next real hole.
#[test]
fn an_excuse_for_a_bin_that_is_built_again_is_red() {
    let d = build_fixture(Fixture { excuse_extra: true, ..Fixture::healthy() });
    let o = gate(d.path(), Arms { bins: true, tests: false });
    assert!(!o.is_green(), "a stale excuse must be red:\n{}", joined(&o));
    assert_eq!(o.bins.stale_excuses, vec!["app-extra".to_string()]);
    assert!(joined(&o).contains("rots into a blanket"), "{}", joined(&o));
}

// ───────────────────────────── assertion 2: a test file that compiles to nothing ─────────────────────────────

/// **RED PROBE — a `tests/*.rs` that compiles to a binary with zero tests.**
///
/// `tests/dark.rs` is gated on a feature no declared atom carries, so under every
/// resolution it compiles to an empty binary and libtest prints `running 0 tests … ok`
/// — shaped exactly like a pass. That is korp's measured shape: of 122 `tests/*.rs`,
/// 42 carry a file-level `#![cfg(…)]`.
///
/// The control in the same fixture is `tests/gated.rs`, gated on `extra`, which IS
/// carried by an atom: it is empty under `app-main` and populated under `app-extra`,
/// and the gate must **not** flag it. A gate that fired on every correctly-gated file
/// would be switched off within a day, which is how the repo came to have no gate.
#[test]
fn a_test_file_that_compiles_to_zero_tests_is_red_and_the_gate_names_the_file() {
    // ── RED ───────────────────────────────────────────────────────────────────
    let d = build_fixture(Fixture { dark_test_file: true, ..Fixture::healthy() });
    let o = gate(d.path(), Arms { bins: false, tests: true });
    let text = joined(&o);

    assert!(!o.is_green(), "a file that compiles to zero tests MUST be red:\n{text}");
    assert_eq!(o.tests.empty.len(), 1, "exactly one dark file, got {:?}", o.tests.empty);
    let dark = &o.tests.empty[0];
    assert_eq!(
        dark.src.file_name().unwrap().to_string_lossy(),
        "dark.rs",
        "the gate must NAME the source file, not merely count"
    );
    assert!(text.contains("dark.rs"), "the file name must reach the report:\n{text}");

    // The control was NOT flagged — this is what separates a working gate from one
    // that simply fails on everything.
    let flagged: BTreeSet<String> = o
        .tests
        .empty
        .iter()
        .map(|e| e.src.file_name().unwrap().to_string_lossy().to_string())
        .collect();
    assert!(
        !flagged.contains("gated.rs"),
        "gated.rs IS reached by the app-extra atom and must not be flagged: {flagged:?}"
    );
    assert!(!flagged.contains("always.rs"), "always.rs is never empty: {flagged:?}");

    // The denominator is real: several test binaries were interrogated and most hold
    // tests. A gate reporting `0 of 0` would be vacuous.
    assert!(o.tests.produced >= 4, "too few binaries interrogated: {}", o.tests.produced);
    assert!(o.tests.files >= 3, "three distinct test files seen: {}", o.tests.files);
    assert_eq!(o.tests.files_with_tests, o.tests.files - 1, "exactly one file is dark");
    assert!(o.tests.tests_found >= 4, "tests were actually found: {}", o.tests.tests_found);
    assert!(text.contains("EMPTY"), "{text}");

    // ── GREEN, same gate, the dark file removed ───────────────────────────────
    let d = build_fixture(Fixture::healthy());
    let o = gate(d.path(), Arms { bins: false, tests: true });
    assert!(o.is_green(), "without the dark file the SAME gate passes:\n{}", joined(&o));
    assert!(o.tests.empty.is_empty());
    assert!(o.tests.produced > 0, "and it did not pass by interrogating nothing");
    assert!(o.tests.files >= 2, "over a real set of files: {}", o.tests.files);
    // Every FILE is reached by some atom — including `gated.rs`, which is empty under
    // `app-main` and populated under `app-extra`. The per-BINARY ratio is legitimately
    // below 1 here, and asserting on it instead is the mistake this line records.
    assert!((o.tests.ratio() - 1.0).abs() < 1e-9, "per-file ratio {}", o.tests.ratio());
    assert!(o.tests.with_tests < o.tests.produced, "…while the per-binary ratio is NOT 1");
}

/// **A dark file is excusable only with words, and a stale excuse is red.** Same
/// contract as the bin allowlist — one rule to argue about, not two.
#[test]
fn a_dark_test_file_is_excusable_only_with_words_and_the_excuse_can_go_stale() {
    let d = build_fixture(Fixture { dark_test_file: true, excuse_dark: true, ..Fixture::healthy() });
    let o = gate(d.path(), Arms { bins: false, tests: true });
    assert!(o.is_green(), "a written reason excuses it:\n{}", joined(&o));
    // The excused file stays in the denominator — an excuse must not shrink what is
    // being measured, or the ratio improves every time somebody writes one.
    assert!(o.tests.produced >= 4, "denominator intact: {}", o.tests.produced);

    // STALE: `gated.rs` IS reached (by `app-extra`) and the allowlist still excuses
    // it. Left alone the list rots into a blanket that hides the next real hole.
    let d = build_fixture(Fixture::healthy());
    let toml = d.path().join(".nornir/build-variants.toml");
    let mut t = std::fs::read_to_string(&toml).unwrap();
    t.push_str("[build.allow_dark_test_files]\n\"gated.rs\" = \"stale: this file is \
                reached by the app-extra atom and needs no excuse\"\n");
    std::fs::write(&toml, t).unwrap();
    let o = gate(d.path(), Arms { bins: false, tests: true });
    assert!(!o.is_green(), "a stale dark-file excuse must be red:\n{}", joined(&o));
    assert!(joined(&o).contains("rots into a blanket"), "{}", joined(&o));
    assert!(joined(&o).contains("gated.rs"), "and it must name the file:\n{}", joined(&o));
}

/// **RED PROBE — the TOML quoting trap, which the red probe itself found.**
///
/// `dark.rs = "…"` is a dotted key: TOML reads it as a table `dark` with a member `rs`.
/// The allowlist therefore contained a key `dark` whose value was a table, excusing
/// nothing at all while looking on the page exactly like an excuse that works. Before
/// the fix the gate coerced it to an empty reason and complained about the wrong thing;
/// now it says what is actually wrong and how to spell it.
#[test]
fn an_unquoted_allowlist_key_is_red_and_says_how_to_spell_it() {
    let d = build_fixture(Fixture { dark_test_file: true, ..Fixture::healthy() });
    let toml = d.path().join(".nornir/build-variants.toml");
    let mut t = std::fs::read_to_string(&toml).unwrap();
    t.push_str("[build.allow_dark_test_files]\ndark.rs = \"a perfectly well written \
                reason that TOML will never attach to a file name\"\n");
    std::fs::write(&toml, t).unwrap();

    let bad = buildgate::malformed(d.path(), "allow_dark_test_files");
    assert_eq!(bad.len(), 1, "the dotted key must be caught: {bad:?}");
    assert!(bad[0].contains("dotted key"), "{bad:?}");
    assert!(bad[0].contains("\"dark.rs\""), "it must show the correct spelling: {bad:?}");

    let o = gate(d.path(), Arms { bins: false, tests: true });
    assert!(!o.is_green(), "and the gate must be RED over it:\n{}", joined(&o));

    // GREEN once quoted — the same reason, one pair of quotes.
    let mut t = std::fs::read_to_string(&toml).unwrap();
    t = t.replace("\ndark.rs = ", "\n\"dark.rs\" = ");
    std::fs::write(&toml, t).unwrap();
    assert!(buildgate::malformed(d.path(), "allow_dark_test_files").is_empty());
    let o = gate(d.path(), Arms { bins: false, tests: true });
    assert!(o.is_green(), "quoted, it really does excuse the file:\n{}", joined(&o));
}

// ───────────────────────────── the gate's own blind spots ─────────────────────────────

/// **A gate that walks nothing must not report success.** This is the `ARMS=` typo the
/// shell script guarded against, and the same hole exists for an atom filter.
#[test]
fn an_atom_filter_that_matches_nothing_is_red() {
    let d = build_fixture(Fixture::healthy());
    let spec = GateSpec::new(d.path()).arms(Arms { bins: true, tests: false }).only("no-such-atom");
    let o = buildgate::run(&spec).expect("the gate ran");
    assert!(!o.is_green(), "walking zero atoms is not a pass:\n{}", joined(&o));
    assert!(joined(&o).contains("matched NO declared atom"), "{}", joined(&o));
    assert!(o.runs.is_empty(), "and it must not have compiled anything");

    // The same filter, spelled correctly, is green — so the red above is about the
    // filter matching nothing, not about filtering at all.
    let spec = GateSpec::new(d.path()).arms(Arms { bins: true, tests: false }).only("app-main");
    let o = buildgate::run(&spec).expect("the gate ran");
    assert_eq!(o.atoms_walked, vec!["app-main".to_string()]);
    assert_eq!(o.runs.len(), 1);
}

/// **The free diagnosis is measured on a real workspace**, and it is the number the
/// whole exercise turns on: `cargo build --bins` reaches one of two here, exactly as it
/// reaches a minority of korp's.
#[test]
fn the_naive_reach_diagnosis_is_measured_not_asserted() {
    let d = build_fixture(Fixture::healthy());
    let n = buildgate::naive_reach(d.path()).expect("cargo metadata");
    assert_eq!(n.bins, 2, "two bin targets");
    assert_eq!(n.reachable, 1, "`cargo build --bins` reaches only app-main");
    assert_eq!(n.skipped.len(), 1);
    assert_eq!(n.skipped[0].0, "app-extra");
    assert_eq!(n.skipped[0].1, vec!["extra".to_string()], "and it says WHICH feature is short");
    assert!((n.ratio() - 0.5).abs() < 1e-9);
    assert!(n.headline().contains("1 of 2"), "{}", n.headline());
}

/// **`list_tests` must be able to return zero AND non-zero**, or the whole second
/// assertion rests on a function that always says the same thing. Probed against real
/// binaries the fixture just built.
#[test]
fn list_tests_distinguishes_an_empty_binary_from_a_full_one() {
    let d = build_fixture(Fixture { dark_test_file: true, ..Fixture::healthy() });
    let o = gate(d.path(), Arms { bins: false, tests: true });
    let dark = o.tests.empty.first().expect("the dark binary was found");
    assert_eq!(buildgate::list_tests(&dark.exe).unwrap(), 0, "the empty binary really lists 0");
    // …and some binary in the same run really lists more than 0, from the same
    // function. Without this half, `list_tests` returning 0 unconditionally would pass
    // every assertion above.
    assert!(
        o.tests.tests_found > 0,
        "the same function found real tests elsewhere: {}",
        o.tests.headline()
    );
}

/// **RED PROBE for the escape hatch itself.**
///
/// A partial run (`--atom X`) cannot assert coverage: every bin belonging to a skipped
/// atom would show as unbuilt. MEASURED on korp `b388500` — `--atom korp-cli` reported
/// "8 of 10 UNBUILT", all eight belonging to the five excluded atoms. A gate that is
/// red no matter what is a gate people learn to ignore, so the assertion is suppressed.
///
/// But an escape hatch is exactly where a gate goes hollow, so this probes BOTH sides:
/// the partial run must still say the numbers out loud and must still fail on a real
/// compile error, and the FULL run over the same repo must still be red.
#[test]
fn a_partial_run_reports_the_numbers_but_does_not_assert_coverage() {
    let d = build_fixture(Fixture { atom_for_extra: false, ..Fixture::healthy() });

    // FULL run over the same repo: RED. This is the control — without it, "partial is
    // green" would be indistinguishable from "the gate never fires".
    let full = gate(d.path(), Arms { bins: true, tests: false });
    assert!(!full.is_green(), "the full run over this repo is red:\n{}", joined(&full));
    assert!(!full.bins.partial);

    // PARTIAL run: green on coverage, because coverage is unknowable here…
    let spec = GateSpec::new(d.path()).arms(Arms { bins: true, tests: false }).only("app-main");
    let o = buildgate::run(&spec).expect("the gate ran");
    assert!(o.bins.partial, "the tally must KNOW it is partial");
    assert!(o.is_green(), "a partial run must not fail on unknowable coverage:\n{}", joined(&o));

    // …but the numbers are still printed, and the report SAYS why it cannot assert.
    // A silent suppression is the hole; an announced one is a limitation.
    let text = o.render();
    assert!(text.contains("PARTIAL RUN"), "it must say so out loud:\n{text}");
    assert!(text.contains("2 declared"), "the numbers still come out:\n{text}");
    assert!(text.contains("1 built"), "the numbers still come out:\n{text}");
    assert!(text.contains("accounted for"), "and the ratio still shows its numerator:\n{text}");
    assert!(!o.bins.unbuilt.is_empty(), "and the tally is still populated: {:?}", o.bins.unbuilt);
}

/// **A partial run must still be RED when the atom it DID walk does not compile.**
///
/// This is the half that keeps the escape hatch from swallowing everything: coverage is
/// suppressed, compilation never is.
#[test]
fn a_partial_run_is_still_red_when_its_own_atom_will_not_compile() {
    let d = build_fixture(Fixture::healthy());
    // Break `app-main` and nothing else.
    std::fs::write(d.path().join("app/src/main.rs"), "fn main() { this_is_not_a_function() }\n")
        .unwrap();

    let spec = GateSpec::new(d.path()).arms(Arms { bins: true, tests: false }).only("app-main");
    let o = buildgate::run(&spec).expect("the gate ran");
    assert!(!o.is_green(), "a broken atom is red even in a partial run:\n{}", joined(&o));
    let text = joined(&o);
    assert!(text.contains("DID NOT COMPILE"), "{text}");
    // The compiler's OWN words must reach the report — "cargo exited non-zero" sends
    // somebody back to reproduce what the gate already knew.
    assert!(
        text.contains("this_is_not_a_function") || text.contains("cannot find function"),
        "the diagnostic must be attached, not summarised:\n{text}"
    );

    // GREEN again once it compiles — same gate, same filter.
    std::fs::write(d.path().join("app/src/main.rs"), "fn main() {}\n").unwrap();
    let o = buildgate::run(&spec).expect("the gate ran");
    assert!(o.is_green(), "and green once fixed:\n{}", joined(&o));
}