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));
}