xtask/tests/guard.rs
//! The dependency guard, seen to go red.
//!
//! *Never accept a green you have not seen red.* A guard nobody has
//! watched fail is worth nothing, and this one is named specifically: *"the
//! dependency guard must go red on a deliberately added rayon dep"*.
//!
//! So the guard is asserted in **both** directions, permanently:
//!
//! * **red**, against three deliberately-bad workspaces under
//! `tests/fixtures/`, each breaking exactly one rule, each resolved by the
//! real Cargo resolver through the same [`xtask::guard::run`] the binary
//! calls. Not a hand-built string: a real `cargo tree`.
//! * **green**, against gunnar's own graph. A guard that rejects everything is
//! the identity-value trap; it must accept the thing it is meant to accept.
//!
//! Each red test also asserts **which** rule fired and on **which** item. A
//! guard that returned "violation" for every input would pass a bare
//! `assert!(!report.is_clean())` and prove nothing.
use std::path::{Path, PathBuf};
use xtask::guard::{self, Profile, Rule, Scope, APPLIANCE};
fn fixture(name: &str) -> PathBuf {
let p = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
.join("Cargo.toml");
assert!(p.is_file(), "fixture manifest is missing: {}", p.display());
p
}
/// Run the guard over a fixture with the `S`-crate rule set.
///
/// Two details, both learned the hard way when this file was first run:
///
/// * the profile is named `fixture:<name>`, **not** [`APPLIANCE`]. Deviations
/// are scoped by profile name, so a fixture calling itself `appliance` would
/// inherit gunnar's own `fs` exceptions and have the very violations it
/// exists to demonstrate quietly excused: a fixture that could not go red.
/// * `check_stale` is off: a fixture exercises one rule, so every unrelated
/// deviation would be reported stale and drown the finding under test.
fn check_fixture(name: &str, package: &str, expect_present: &[&str]) -> guard::Report {
let profile = Profile::new(
&format!("fixture:{name}"),
&["-p", package],
Scope::ServerHalf,
expect_present,
);
let report = guard::run(&fixture(name), &[profile], false)
.unwrap_or_else(|e| panic!("guard could not resolve fixture `{name}`: {e}"));
assert!(
report.tracked.is_empty(),
"a tracked deviation applied to fixture `{name}`. Fixtures must be judged by the \
bare rules, or they cannot prove the rules fire:\n{}",
report.render()
);
report
}
fn items(report: &guard::Report, rule: Rule) -> Vec<String> {
report
.violations
.iter()
.filter(|v| v.rule == rule)
.map(|v| v.item.clone())
.collect()
}
// ── RED ─────────────────────────────────────────────────────────────────────
/// In as many words: *"the dependency guard must go red on a deliberately
/// added rayon dep."* This is that test, against the real rayon from
/// crates.io.
#[test]
fn red_rayon_is_rejected() {
let report = check_fixture("rayon_creeps_in", "gunnar-core", &["gunnar-core", "rayon"]);
assert!(
!report.is_clean(),
"the guard ACCEPTED a graph containing rayon:\n{}",
report.render()
);
let hits = items(&report, Rule::ForbiddenAnywhere);
assert!(
hits.contains(&"rayon".to_string()),
"the guard went red, but not on rayon, so it is red for the wrong reason and \
says nothing about the multicore rule. Forbidden-anywhere hits: {hits:?}\n{}",
report.render()
);
// `rayon-core` is rayon's own dependency and is on the list too. Catching
// both is the difference between matching a name and matching the thing.
assert!(
hits.contains(&"rayon-core".to_string()),
"rayon-core, which arrives under rayon, was not caught: {hits:?}"
);
// And no tracked deviation may ever excuse it.
assert!(
!report.tracked.iter().any(|v| v.item == "rayon"),
"a tracked deviation excused rayon. The multicore rule has no exceptions:\n{}",
report.render()
);
}
/// *"`gunnar-client` must not be a dependency of any `S` crate. That edge
/// is what would quietly drag `gix`'s porcelain (and with it the whole
/// filesystem assumption) into the appliance build."*
#[test]
fn red_an_s_crate_depending_on_gunnar_client_is_rejected() {
let report = check_fixture(
"s_depends_on_client",
"gunnar-store",
&["gunnar-store", "gunnar-client"],
);
assert!(
!report.is_clean(),
"the guard ACCEPTED an S → gunnar-client edge:\n{}",
report.render()
);
let hits = items(&report, Rule::ForbiddenInServerGraph);
assert!(
hits.contains(&"gunnar-client".to_string()),
"the guard went red, but not on the gunnar-client edge. S-graph hits: {hits:?}\n{}",
report.render()
);
}
/// The resolver-2 point, made concrete. The fixture's manifest asks for `gix`
/// with `default-features = false` and one feature, `status`. The *declaration*
/// looks minimal; the *resolved* graph carries dirwalk, index, blob-diff and
/// the worktree crates. A declaration-reading guard passes here. This one must
/// not.
#[test]
fn red_gix_porcelain_features_in_a_server_graph_are_rejected() {
let report = check_fixture(
"gix_porcelain_in_server",
"gunnar-store",
&["gunnar-store", "gix"],
);
assert!(
!report.is_clean(),
"the guard ACCEPTED gix with `status` in an S graph:\n{}",
report.render()
);
let feature_hits = items(&report, Rule::GixPorcelainFeature);
for expected in ["gix:status", "gix:dirwalk", "gix:index"] {
assert!(
feature_hits.contains(&expected.to_string()),
"`{expected}` was not reported. The whole point of resolving the graph is that \
one declared feature turns several on. Feature hits: {feature_hits:?}\n{}",
report.render()
);
}
// And the crates a worktree-free server needs none of.
let crate_hits = items(&report, Rule::ForbiddenInServerGraph);
for expected in ["gix-worktree", "gix-index", "gix-status"] {
assert!(
crate_hits.contains(&expected.to_string()),
"`{expected}` was not reported: {crate_hits:?}\n{}",
report.render()
);
}
}
/// The empty-graph trap, seen to go red.
///
/// A `cargo tree` that resolves nothing produces no output, and no output is
/// shaped exactly like "no forbidden crate found". This asks the guard for a
/// package that does not exist in the fixture and requires it to call that a
/// failure rather than a clean bill of health.
#[test]
fn red_a_graph_missing_its_sentinel_is_not_a_pass() {
let profile = Profile::new(
"vacuum",
&["-p", "gunnar-core"],
Scope::ServerHalf,
// `gunnar-core` is really there; `russh` is not. The guard must notice.
&["gunnar-core", "russh"],
);
let report =
guard::run(&fixture("rayon_creeps_in"), &[profile], false).expect("the fixture resolves");
assert!(
!report.is_clean(),
"a graph missing a package the profile demands was reported CLEAN, so every \
absence this guard asserts could be vacuous:\n{}",
report.render()
);
assert_eq!(
items(&report, Rule::VacuousGraph),
vec!["russh".to_string()],
"the vacuous-graph rule fired on the wrong item:\n{}",
report.render()
);
}
/// A stale exception is a failure, not a courtesy.
///
/// When the `fs` deviation is fixed and gix leaves the appliance graph, the
/// entries in `TRACKED_DEVIATIONS` stop excusing anything, and if that were
/// silent, the ledger would outlive its cause and quietly pre-authorise the
/// violation coming back. Here the guard is run over a clean fixture with
/// stale-checking ON, and must report every unmatched deviation.
#[test]
fn red_a_deviation_that_excuses_nothing_fails_the_guard() {
let profile = Profile::new(
"clean-room",
&["-p", "gunnar-client"],
Scope::EitherHalf,
&["gunnar-client"],
);
let report = guard::run(&fixture("s_depends_on_client"), &[profile], true)
.expect("the fixture resolves");
assert!(
report.violations.is_empty(),
"this fixture crate is a bare stub; nothing should be a real violation:\n{}",
report.render()
);
assert!(
!report.stale_deviations.is_empty(),
"no deviation was reported stale even though none of them could match, so the \
ledger would never be forced to shrink:\n{}",
report.render()
);
assert!(
!report.is_clean(),
"stale deviations were reported but the guard still called itself clean:\n{}",
report.render()
);
// This run asked for a self-check but supplied no `default` profile, which
// means the anti-phantom rule had nothing to run against. That must be
// reported rather than read as "no phantoms found": the same silent hole,
// one level up.
assert!(
report.phantom_rules.iter().any(|v| v.item == "default"),
"a self-check without a `default` profile skipped the anti-phantom rule silently:\n{}",
report.render()
);
}
// ── GREEN ───────────────────────────────────────────────────────────────────
/// The other direction, and the one that keeps the tests above honest: the
/// guard must **accept** gunnar's real graph. A guard that rejects everything
/// passes every red test and is worth nothing.
#[test]
fn green_the_real_workspace_passes() {
let report = guard::run_gunnar().expect("the guard resolves gunnar's graphs");
assert!(
report.violations.is_empty(),
"the dependency guard found untracked violations in gunnar's own graph:\n{}",
report.render()
);
assert!(
report.stale_deviations.is_empty(),
"some tracked deviations no longer excuse anything. Delete them:\n{}",
report.render()
);
assert!(
report.phantom_rules.is_empty(),
"some rules name crates that can never appear, so they never fire:\n{}",
report.render()
);
assert!(report.is_clean());
}
/// The tracked deviations are load-bearing, not decorative.
///
/// This test used to pin the `fs` deviation: `gunnar-store`'s `fs` backend went
/// through `gix` porcelain, so the appliance build linked nine `gix-*` crates
/// and two gix features, all recorded in `TRACKED_DEVIATIONS`. That deviation
/// was **fixed** on 2026-08-03. `fs` now sits on `gix-odb`/`gix-ref` plumbing
/// directly, the entries stopped excusing anything, the staleness rule failed
/// the guard on all eleven, and they were deleted along with the assertion that
/// named them.
///
/// `axum` is the one deviation left, so it is what this now pins: the guard
/// really is finding it and really is excusing it by name on every profile that
/// links tonic, rather than the `axum` rule having been quietly deleted from
/// [`guard::FORBIDDEN_ANYWHERE`].
#[test]
fn green_the_axum_deviation_is_visible_and_named() {
let report = guard::run_gunnar().expect("the guard resolves gunnar's graphs");
// tonic is in the appliance image, in the gRPC crate on its own, and in the default
// build: the product `gunnar-cli` (GUN-PRODUCTS, 2026-10-30) carries the control plane,
// whose gRPC client dials the one server named at `--at` (AIRGAP: nothing else).
for profile in [APPLIANCE, "S:gunnar-grpc", "default"] {
let tracked: Vec<&str> = report
.tracked
.iter()
.filter(|v| v.profile == profile)
.map(|v| v.item.as_str())
.collect();
assert!(
tracked.contains(&"axum"),
"`axum` is no longer reported against the `{profile}` profile. Either tonic's \
`router` feature stopped pulling it — in which case delete the entry from \
TRACKED_DEVIATIONS, and this assertion with it — or the rule that found it was \
weakened. Tracked: {tracked:?}\n{}",
report.render()
);
}
// And the ledger is exactly what it is meant to be. Pinned as a list so
// that growing it is a decision somebody had to write down here, not a side
// effect of adding a dependency.
//
// `gunnar-delta` joined on 2026-08-04 — `gunnar gc` recomputed deltas then,
// so the encoder was in the two whole-binary graphs that carried the verb —
// and **left again at `d69d334`**, when the repack engine was amputated.
// gunnar computes no delta anywhere now: the serve path copies the pushing
// client's entries byte for byte (`upload_pack.rs`: *"Nothing is
// re-compressed and no delta is computed"*) and `gc` belongs to the storage
// engine. An excuse whose cause is gone is not a smaller excuse, it is a
// false one, so the entry went rather than being reworded.
//
// ⚠ This assertion said `["gunnar-delta", "axum"]` until 2026-08-10 and had
// been RED since `d69d334`, whose own message reads "xtask guard: PASS" — the
// guard VERB passed, and `cargo test -p xtask` was not what it ran. The
// ledger is pinned here precisely so a change to it is a decision somebody
// wrote down; that only works if the pin is read.
//
// ⚠ It said `["axum"]` until 2026-08-30 and was RED from `804d96a8`, which
// added the two entries below and did not come here to say so. That is the
// pin doing its job: the ledger grew by a DECISION (the project's — the
// appliance replicates, measured end to end) and the decision is now
// written down in both places. `green_no_deviation_pre_authorises_the_
// contamination_rules` is where the shape of those two entries is held.
let items: Vec<&str> = guard::TRACKED_DEVIATIONS.iter().map(|d| d.item).collect();
assert_eq!(
items,
vec!["gunnar-client", "nornir-git", "axum"],
"the deviation ledger changed. Every entry must be justified where it is written."
);
// And the two new ones are LIVE on the appliance rather than merely written
// down: `run_gunnar` reported them against the profile that decides what
// ships. Read off the report and not off the table, because the table is
// what the previous assertion already read — a deviation for an edge that
// is not in the graph is what `stale_deviations` exists to fail, and this
// is the same fact from the other side.
let on_appliance: Vec<&str> = report
.tracked
.iter()
.filter(|v| v.profile == APPLIANCE)
.map(|v| v.item.as_str())
.collect();
for item in ["gunnar-client", "nornir-git"] {
assert!(
on_appliance.contains(&item),
"`{item}` is excused on `{APPLIANCE}` but the guard did not find it there, so the \
entry excuses nothing and the appliance cannot be replicating. Tracked on \
{APPLIANCE}: {on_appliance:?}\n{}",
report.render()
);
}
// The clause the list above used to carry implicitly, kept as a rule of its
// own so it survives the list changing: a `gix-*` deviation coming back
// would mean the appliance build re-acquired gix porcelain, and no argument
// written in a `why` field makes that acceptable.
for item in &items {
assert!(
!item.starts_with("gix"),
"`{item}` is excused in the deviation ledger. A `gix-*` exception means the \
appliance build links porcelain again, which the no-OS rule does not permit at any price."
);
}
}
/// The encoder is in **no** gunnar graph, appliance included.
///
/// # This was the opposite assertion until 2026-08-10
///
/// It read *"excused where `gunnar gc` runs, and nowhere else"*, and its first
/// half asserted `gunnar-delta` was PRESENT in the appliance — because on
/// 2026-08-04 `gunnar gc` recomputed deltas, so the encoder had to be in the
/// graphs carrying that verb, and the point of the test was to keep that a
/// narrowing rather than a hole.
///
/// The repack engine was amputated and the premise went with it. gunnar
/// computes no delta anywhere now: the serve path copies the pushing client's
/// entries byte for byte (`gunnar-wire/src/upload_pack.rs`: *"Nothing is
/// re-compressed and no delta is computed"*), and `gc` is
/// [`GitOps::gc`](https://docs.rs/) — the storage engine's, over its own
/// archive. `d69d334` deleted the deviation, correctly, and left this test
/// asserting a presence that had stopped being true. It had been RED since.
///
/// So the excuse becomes a **ban**, which is a strictly stronger statement and
/// the one that is now true. `gunnar-delta` is a delta *encoder*; the serve
/// path reuses the client's deltas and computes none; nothing gunnar ships needs
/// one. If the encoder comes back, that is a design change and it should have to
/// come back through here.
#[test]
fn green_the_encoder_is_in_no_graph_gunnar_ships() {
let report = guard::run_gunnar().expect("the guard resolves gunnar's graphs");
// Not excused anywhere, because there is nothing left to excuse. An entry
// reappearing means somebody re-acquired the encoder and wrote a `why` for
// it instead of asking whether the serve path should have one.
let excused: Vec<&str> = report.tracked.iter().map(|v| v.item.as_str()).collect();
assert!(
!excused.contains(&"gunnar-delta"),
"the delta ENCODER is excused again somewhere. gunnar computes no delta: the serve \
path copies entries verbatim and `gc` belongs to the storage engine. An excuse here \
means the amputation was partly undone. Tracked: {excused:?}\n{}",
report.render()
);
// ── the SERVER-shaped whole-binary graphs, which is the half that used to
// be inverted ────────────────────────────────────────────────────────
//
// `default` is deliberately not in this list, and the exclusion is the
// finding rather than a convenience: `gunnar-pack/delta` is still turned on
// by **`gunnar-client`**, whose send-pack computes deltas for a push. A
// developer's `cargo build` therefore links the encoder, and should. What
// changed on 2026-08-04→`d69d334` is only the *server* side of it.
//
// `default` is asserted the other way below, so this test cannot pass by the
// encoder having vanished from the workspace entirely.
let manifest_root = guard::workspace_root().join("Cargo.toml");
let mut server_shapes = 0usize;
for profile in guard::gunnar_profiles() {
if profile.name.starts_with("S:") || profile.name == guard::DEFAULT_BUILD {
continue;
}
let graph = guard::resolve(&manifest_root, &profile)
.unwrap_or_else(|err| panic!("resolving `{}`: {err}", profile.name));
assert!(
!graph.contains("gunnar-delta"),
"`{}` links the delta ENCODER. Nothing gunnar SERVES computes a delta any more \
(the serve path copies entries verbatim, `gc` is the storage \
engine's); if this shape needs one, say why here rather than letting it in.",
profile.name
);
server_shapes += 1;
}
assert!(
server_shapes >= 3,
"only {server_shapes} server-shaped whole-binary graph(s) were resolved; the \
appliance, appliance+http and service shapes must all be asked"
);
// The counterweight: the encoder is still SOMEWHERE, so "absent from the
// server" is a statement about the server and not about the workspace. This
// is the half whose absence let the old test pass vacuously in the other
// direction.
let client = guard::gunnar_profiles()
.into_iter()
.find(|p| p.name == guard::DEFAULT_BUILD)
.expect("the default profile exists");
let graph = guard::resolve(&manifest_root, &client).expect("resolving the default build");
assert!(
graph.contains("gunnar-delta"),
"no graph in the workspace links the delta encoder, so the assertions above are \
about a crate that is not there. `gunnar-client`'s send-pack computes deltas for \
a push; if it has stopped, this test is measuring nothing"
);
// And absent from every serve-path crate resolved on its own. Resolved here
// rather than read off the report, because the report only carries what
// *fired*: an encoder edge added to `gunnar-wire` would show up there as a
// violation, but so would a dozen other things, and "no violation mentioning
// gunnar-delta" is an absence of evidence. This asks the graph.
let manifest = guard::workspace_root().join("Cargo.toml");
let mut asked = 0usize;
for profile in guard::gunnar_profiles() {
if !profile.name.starts_with("S:") {
continue;
}
let graph = guard::resolve(&manifest, &profile)
.unwrap_or_else(|err| panic!("resolving `{}`: {err}", profile.name));
assert!(
!graph.contains("gunnar-delta"),
"`{}` links the delta ENCODER. The serve path reuses the client's deltas \
and must compute none; only the maintenance verb may have one.",
profile.name
);
asked += 1;
}
// A loop that asked nothing proves nothing.
assert!(
asked >= 6,
"only {asked} per-crate graphs were resolved, so this assertion covered \
almost none of the server half"
);
}
// ── the anti-phantom rule ───────────────────────────────────────────────────
/// A rule that names a crate nothing can ever resolve is decoration.
///
/// This is the check `crates/gunnar-cli/tests/zero_shell_guard.rs` carried
/// before its rule table was merged into `xtask::guard` (one implementation, not two), and it is the
/// mirror of the staleness rule: staleness catches an *exception* that excuses
/// nothing, this catches a *rule* that forbids nothing. Seen red here against a
/// real resolved graph: the appliance build, which contains none of the banned
/// gix crates, so the check is known to be able to report a name as
/// unprovable rather than assumed to.
///
/// `skidbladnir` is in this list from 2026-08-04 and is the strongest case: it
/// is absent from the appliance graph BY DESIGN — that absence is the systemd
/// ban's whole content — so a rule table proven only against the appliance
/// could never show that ban capable of firing. Its witness is the `service`
/// profile, which is why [`guard::WITNESS_PROFILES`] is a set and not one name.
#[test]
fn red_a_rule_name_absent_from_the_graph_is_reported_phantom() {
let profile = guard::gunnar_profiles()
.into_iter()
.find(|p| p.name == APPLIANCE)
.expect("the appliance profile exists");
let graph = guard::resolve(&guard::workspace_root().join("Cargo.toml"), &profile)
.expect("the appliance graph resolves");
let phantom: Vec<String> = guard::check_rule_names_can_fire(&[&graph])
.into_iter()
.map(|v| v.item)
.collect();
// `gunnar-client` stood here until 2026-08-30 and had to go: `804d96a8`
// put it IN the appliance graph on purpose (replication, the wire
// half with `porcelain` off), so it is no longer a name this graph cannot
// witness and the assertion had been red ever since — correctly, because
// the check reports what is absent and `gunnar-client` stopped being.
//
// `gix` takes its place rather than the list shrinking to three, and it is
// the stronger member: the facade ban is the rule that REPLACED the
// `gunnar-client` and `nornir-git` proxies, so it is the one whose ability
// to be reported as unprovable now matters most. A typo in it would leave
// the appliance with no porcelain ban at all.
for expected in ["gix", "gix-command", "gix-worktree", "skidbladnir"] {
assert!(
phantom.contains(&expected.to_string()),
"`{expected}` is absent from the appliance graph, so a rule table proven only \
against THAT graph could name it with a typo and never notice. The check did \
not report it: {phantom:?}"
);
}
}
/// The green direction: against the DEFAULT build, every S-graph rule name is a
/// crate that really exists and really can appear, so none of them is
/// decoration, and `run_gunnar` says so.
#[test]
fn green_every_forbidden_name_is_a_crate_that_can_actually_appear() {
let report = guard::run_gunnar().expect("the guard resolves gunnar's graphs");
assert!(
report.phantom_rules.is_empty(),
"some S-graph rules name crates that appear in NO build, so they can never fire:\n{}",
report.render()
);
// And the check was not silently skipped: `run_gunnar` must resolve EVERY
// witness profile, or `phantom_rules` would be empty for the wrong reason.
// One name was enough while `default` was the only witness; a set that
// quietly lost a member takes with it every ban only that member could
// prove, and an empty `phantom_rules` looks identical either way.
let declared = guard::gunnar_profiles();
for want in guard::WITNESS_PROFILES {
assert!(
declared.iter().any(|p| p.name == *want),
"no `{want}` profile, so the anti-phantom check has one fewer graph to run \
against and every rule only IT could witness goes unproven"
);
}
}
/// The rules that guard the appliance against resolver-2 contamination are NOT
/// excused by any deviation.
///
/// This is the difference between "recorded a deviation" and "weakened the
/// guard". The `fs` backend brings `index` and `attributes`; it does not bring
/// `status`, `dirwalk`, `worktree-mutation` or `blocking-network-client`, and
/// those are precisely what a `cargo build --workspace` unification would leak
/// from `gunnar-client` into the appliance. None of them may ever be
/// pre-authorised.
///
/// # `gunnar-client` and `nornir-git` left the never-list on 2026-08-30
///
/// They were on it, carrying this test's own sentence — *"that is not recording
/// a deviation, it is switching the rule off"* — and `804d96a8` gave them a
/// deviation anyway. That commit is **right and this test was the stale half**,
/// but only because of what else it did, and that difference is the whole
/// content of this comment:
///
/// * both names were on the never-list as **proxies**. What is forbidden is gix
/// porcelain on an image with no filesystem, and until 2026-08-30 there was
/// no way to have the send-pack wire without the `gix::Repository` behind it,
/// so banning the carrier was the only way to ban the cargo.
/// * `nornir-git 0.1.9` and `gunnar-client 1.1.4` put their gix half behind a
/// default-on `porcelain` feature, and `gunnar-server`'s `replication` takes
/// the wire half with `default-features = false`: **+4 packages** on the
/// appliance graph (365 → 369, MEASURED) against +128 for the old
/// all-or-nothing `client` feature.
/// * so the ban moved **onto its real subject**: the `gix` FACADE crate is
/// banned by name in [`guard::FORBIDDEN_IN_SERVER_GRAPH`] and excused by
/// nothing on any profile — a rule neither proxy could ever be walked around,
/// and the one this test now pins first.
///
/// A rule that got stricter is not a rule that was switched off. What must
/// still be impossible is those two entries **widening**, so that is what is
/// asserted in place of their absence: they may excuse the two whole-binary
/// appliance profiles and nothing else, and in particular no `S:` graph — a
/// store, a wire codec or a ref reader dialling ssh is what the layering ban was always
/// about, and that half of the ban did not move.
///
/// Whether the appliance really does link `gunnar-client` while linking no gix
/// is asserted on the resolved graphs, both directions, by
/// `crates/gunnar-cli/tests/feature_split.rs`'s
/// `the_appliance_build_links_no_gix_porcelain` — which is where a claim about
/// a graph belongs. This file holds the LEDGER to its shape.
#[test]
fn green_no_deviation_pre_authorises_the_contamination_rules() {
for never in [
// THE rule that replaced the two proxies. First in the list, because
// everything under it is a symptom of this one crate reaching an S
// graph.
"gix",
"gix:status",
"gix:dirwalk",
"gix:worktree",
"gix:blocking-network-client",
"gix-worktree",
"gix-command",
"rayon",
"rayon-core",
"git2",
"openssl",
"openssl-sys",
] {
assert!(
!guard::TRACKED_DEVIATIONS.iter().any(|d| d.item == never),
"`{never}` has a tracked deviation. That is not recording a deviation, it is \
switching the rule off."
);
}
// An excuse-free ban is worth nothing if the ban itself was deleted, and
// deleting it is exactly how the two entries below could be made to look
// harmless. `gix` carries the whole argument now, so its presence in the
// rule table is asserted rather than assumed.
assert!(
guard::FORBIDDEN_IN_SERVER_GRAPH
.iter()
.any(|(item, _)| *item == "gix"),
"the `gix` facade is no longer banned in an S graph. That ban is what makes it \
acceptable to excuse `gunnar-client` on the appliance at all; without it nothing \
keeps porcelain out of an image with no filesystem."
);
// The two narrowed entries: excused where the decision was taken, nowhere
// else. `"*"` or an `S:` profile here is "switching the rule off" wearing a
// `why` field.
for narrowed in ["gunnar-client", "nornir-git"] {
let dev = guard::TRACKED_DEVIATIONS
.iter()
.find(|d| d.item == narrowed)
.unwrap_or_else(|| {
panic!(
"`{narrowed}` has no deviation. If the appliance stopped replicating, \
delete the entry and put the name back on the never-list above — but \
`green_the_axum_deviation_is_visible_and_named` pins the ledger too, \
so one of the two is then wrong."
)
});
assert_eq!(
dev.profiles,
&[APPLIANCE, guard::APPLIANCE_HTTP],
"`{narrowed}` is excused on {:?}. It may name the two whole-binary appliance \
profiles and nothing else: that is the difference between the appliance BINARY \
carrying a replication edge on purpose and the layering ban being lifted.",
dev.profiles
);
for krate in guard::SERVER_CRATES {
let per_crate = format!("S:{krate}");
assert!(
!dev.profiles.contains(&per_crate.as_str()) && !dev.profiles.contains(&"*"),
"`{narrowed}` is excused on `{per_crate}`. The rule is about layering: a store, a \
wire codec or a ref reader has no business dialling ssh, and that half of \
the ban did not move on 2026-08-30."
);
}
}
}
/// Every `S` crate has a profile of its own, so an edge added to a crate
/// the appliance build does not yet reach is still caught, and caught in the
/// crate that introduced it.
#[test]
fn green_every_server_crate_is_actually_checked() {
let profiles = guard::gunnar_profiles();
for krate in guard::SERVER_CRATES {
let name = format!("S:{krate}");
let p = profiles
.iter()
.find(|p| p.name == name)
.unwrap_or_else(|| panic!("no guard profile for the S crate `{krate}`"));
assert_eq!(p.scope, Scope::ServerHalf);
assert!(
p.expect_present.iter().any(|e| e == krate),
"profile `{name}` has no sentinel naming its own crate"
);
}
// And the list itself: the S set is exactly the crates named there, so
// adding a server crate without adding it here fails.
//
// Eight when smart HTTP's `gunnar-http` landed — it is a listener, it can end up
// on an appliance, and the no-OS rule applies to it like the rest. **Seven
// since `d69d334`**, which removed `gunnar-pack`: that crate left the
// workspace for edda and is consumed by path, where feature unification and
// dev-dep resolution differ from a member's and produced an `S:` graph no
// build compiles. The whole-binary profiles still ban the same crates from
// every graph that actually links it, so nothing stopped being checked — the
// per-crate probe stopped being resolvable. `guard.rs`'s own comment above
// `SERVER_CRATES` records that; this count did not follow it and had been RED
// ever since.
assert_eq!(
guard::SERVER_CRATES.len(),
7,
"the S set changed; the server-crate list plus gunnar-http for the smart-HTTP \
transport, less `gunnar-pack` which moved to edda. A new one needs a line in \
the table and a profile here, not just a bump"
);
}