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

nordisk/gunnar

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