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

nordisk/monetize

xtask/tests/score.rs

//! **`cargo xtask score`: each scoring rule, on small fixture trees.**
//!
//! Every tree here is written to a fresh temporary directory and scored with
//! [`xtask::score::score`]. The scorer reads files only, so a fixture is just files.
//! `good` meets every bar (30 of 30); `bad` misses every bar (6 of 30); the other
//! tests change one thing and name the check that must move.

use std::fs;
use std::path::{Path, PathBuf};

use xtask::score::{self, Area, Card};

/// A fixture: `<tmp>/xtask-score-<name>-<pid>/monetize` is the repository root, and
/// `<tmp>/…/sib` is a sibling checkout beside it.
struct Tree {
    top: PathBuf,
}

impl Tree {
    fn new(name: &str) -> Tree {
        let top = std::env::temp_dir().join(format!("xtask-score-{name}-{}", std::process::id()));
        let _ = fs::remove_dir_all(&top);
        fs::create_dir_all(top.join("monetize")).unwrap();
        Tree { top }
    }
    fn root(&self) -> PathBuf {
        self.top.join("monetize")
    }
    /// Write `text` at `rel` under the root (`../` reaches the siblings).
    fn file(&self, rel: &str, text: &str) -> &Tree {
        let p = self.root().join(rel);
        fs::create_dir_all(p.parent().unwrap()).unwrap();
        fs::write(&p, text).unwrap();
        self
    }
    fn remove(&self, rel: &str) -> &Tree {
        let p = self.root().join(rel);
        if p.is_dir() {
            fs::remove_dir_all(&p).unwrap();
        } else {
            fs::remove_file(&p).unwrap();
        }
        self
    }
    fn score(&self) -> Card {
        score::score(&self.root())
    }
}

impl Drop for Tree {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.top);
    }
}

fn krate(name: &str) -> String {
    format!("[package]\nname = \"{name}\"\nversion.workspace = true\nedition = \"2021\"\n")
}

fn tests_text(n: usize) -> String {
    let mut s = String::new();
    for i in 0..n {
        s.push_str(&format!("#[test]\nfn t{i}() {{ assert_eq!(1, 1); }}\n"));
    }
    s
}

fn guide(op_names: &[&str], alert_table: bool, checklist: bool) -> String {
    let mut g = String::from("# The guide\n\n### 5.1 The guest op table\n\n| Op | Name | Asked | Answer |\n|---|---|---|---|\n");
    for (i, n) in op_names.iter().enumerate() {
        g.push_str(&format!("| {} | `{n}` | at load | x |\n", i + 1));
    }
    g.push_str("\n### 5.2 The lifecycle\n\nText.\n");
    if alert_table {
        g.push_str("\n## Alerts\n\n| Code | Severity | Who |\n|---|---|---|\n| stall | high | operator |\n");
    }
    if checklist {
        g.push_str("\n## 11. The release checklist\n\n1. Freeze.\n");
    }
    for i in 0..300 {
        g.push_str(&format!("Line {i} of the guide.\n"));
    }
    g
}

/// A tree that meets every bar.
fn good(name: &str) -> Tree {
    let t = Tree::new(name);
    t.file(
        "Cargo.toml",
        "[workspace]\nmembers = [\"crates/*\"]\n\n[workspace.package]\nversion = \"0.2.0\"\n",
    )
    .file(
        "Cargo.lock",
        "version = 4\n\n[[package]]\nname = \"monetize-core\"\nversion = \"0.2.0\"\ndependencies = [\n \"sib-crate\",\n]\n\n[[package]]\nname = \"sib-crate\"\nversion = \"1.4.0\"\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
    )
    .file("README.md", "# monetize\n")
    .file("docs/integration-guide.md", &guide(&["id", "meters"], true, true))
    .file(
        "xtask/src/main.rs",
        "fn task_ci() { let s = vec![nornir_xtask_support::lockcheck::step(&root), Step::cargo(\"test\", &[\"test\", \"--workspace\", \"--locked\"])]; }\n",
    )
    // The sibling checkout and the in-tree crate that path-depends on it.
    .file("../sib/crates/sib-crate/Cargo.toml", "[package]\nname = \"sib-crate\"\nversion = \"1.4.0\"\n")
    .file(
        "crates/core/Cargo.toml",
        &format!("{}\n[dependencies]\nsib-crate = {{ path = \"../../../sib/crates/sib-crate\" }}\n", krate("monetize-core")),
    )
    .file("crates/core/README.md", "# core\n");
    // Runtime code: no unwrap, no spawn, named refusals, alerts, counters, the sandbox.
    let mut lib = String::from("//! The core.\n\npub struct OpCounters { pub calls: u64 }\n\n");
    for i in 0..60 {
        lib.push_str(&format!(
            "pub fn r{i}() -> String {{ format!(\"REFUSED [code-{i}] no\") }}\n"
        ));
        lib.push_str(&format!(
            "pub fn a{i}() {{ eprintln!(\"ALERT estate {i} is stuck\"); }}\n"
        ));
    }
    lib.push_str(
        "#[cfg(test)]\nmod tests {\n    #[test]\n    fn inner() { Some(1).unwrap(); }\n}\n",
    );
    t.file("crates/core/src/lib.rs", &lib)
        .file("crates/core/tests/many.rs", &tests_text(1000))
        // The three trait suites, each run by a test.
        .file("crates/monetize-product-guest/Cargo.toml", &krate("monetize-product-guest"))
        .file(
            "crates/monetize-product-guest/src/lib.rs",
            "//! The guest SDK.\npub mod conformance;\npub mod ops {\n    pub const ID: u32 = 1;\n    pub const METERS: u32 = 2;\n    pub const ALL: &[(u32, &str)] = &[\n        (ID, \"id\"),\n        (METERS, \"meters\"),\n    ];\n}\npub mod grants {\n    pub const HTTP: &str = \"http\";\n    pub const HMAC: &str = \"hmac\";\n    pub const VOCABULARY: &[&str] = &[HTTP, HMAC];\n    pub const PRODUCT: &[&str] = &[];\n    pub const VENDOR: &[&str] = &[HTTP, HMAC];\n}\n",
        )
        .file("crates/monetize-product-guest/src/conformance.rs", "//! The product suite.\npub fn six_ops() {}\n")
        .file("crates/monetize-product-guest/tests/conformance.rs", "#[test]\nfn lake() { monetize_product_guest::conformance::six_ops(); }\n")
        .file("crates/monetize-cloud/Cargo.toml", &krate("monetize-cloud"))
        .file("crates/monetize-cloud/src/lib.rs", "//! Clouds.\npub mod conformance;\n")
        .file("crates/monetize-cloud/src/conformance.rs", "//! The cloud suite.\npub fn leaves_nothing() {}\n")
        .file("crates/monetize-cloud/tests/sandbox.rs", "use monetize_cloud::conformance;\n#[test]\nfn sandbox() { conformance::leaves_nothing(); }\n")
        .file("crates/monetize-pay/Cargo.toml", &krate("monetize-pay"))
        .file("crates/monetize-pay/src/lib.rs", "//! Payments.\npub mod conformance;\n")
        .file("crates/monetize-pay/src/conformance.rs", "//! The payment suite.\npub fn refund() {}\n")
        .file("crates/monetize-pay/tests/sandbox.rs", "#[test]\nfn sandbox() { monetize_pay::conformance::refund(); }\n")
        .file("crates/monetize-product-wasm/Cargo.toml", &krate("monetize-product-wasm"))
        .file(
            "crates/monetize-product-wasm/src/lib.rs",
            "//! The guest host.\npub struct Limits { pub fuel: u64, pub deadline: u64, pub max_memory_bytes: u64 }\npub const BODY_CAP: usize = 16;\npub fn hosts(http_hosts: &[String]) -> usize { http_hosts.len() }\npub fn roots(guest_trust_roots: &str) -> String { format!(\"{guest_trust_roots}.p7s\") }\n",
        )
        .file("crates/monetize-product-wasm/tests/security.rs", "#[test]\nfn a_changed_digest_is_refused() {}\n");
    t
}

/// A tree that misses every bar.
fn bad(name: &str) -> Tree {
    let t = Tree::new(name);
    t.file("Cargo.toml", "[workspace]\nmembers = [\"crates/*\"]\n\n[workspace.package]\nversion = \"0.2.0\"\n")
        // The lock names the sibling at a version its manifest no longer carries.
        .file(
            "Cargo.lock",
            "version = 4\n\n[[package]]\nname = \"monetize-core\"\nversion = \"0.2.0\"\n\n[[package]]\nname = \"sib-crate\"\nversion = \"1.3.0\"\n",
        )
        .file("../sib/crates/sib-crate/Cargo.toml", "[package]\nname = \"sib-crate\"\nversion = \"1.4.0\"\n")
        .file(
            "crates/core/Cargo.toml",
            &format!("{}\n[dependencies]\nsib-crate = {{ path = \"../../../sib/crates/sib-crate\" }}\nmonetize-product-gunnar = {{ path = \"../../products/gunnar\" }}\n", krate("monetize-core")),
        )
        .file("docs/integration-guide.md", "# Short\n\nYour guest must still answer them for a ONE box that dials.\n")
        .file(
            "crates/core/src/lib.rs",
            "pub fn f() { let v: Option<u8> = None; v.unwrap(); v.expect(\"x\"); }\npub fn g() { std::process::Command::new(\"sh\"); }\npub fn h(s: &S) { let _ = s.save_order(); }\npub fn k() { std::thread::spawn(|| {}); std::thread::spawn(|| {}); std::thread::spawn(|| {}); std::thread::spawn(|| {}); std::thread::spawn(|| {}); std::thread::spawn(|| {}); }\npub fn r() -> &'static str { \"REFUSED [one]\" }\n",
        )
        .file("crates/core/tests/plugin.rs", "#[test]\n#[ignore]\nfn native() { monetize_product_gunnar::plugin(); }\n")
        .file("crates/monetize-product-guest/Cargo.toml", &krate("monetize-product-guest"))
        .file(
            "crates/monetize-product-guest/src/lib.rs",
            "pub mod ops {\n    pub const ALL: &[(u32, &str)] = &[\n        (ID, \"id\"),\n    ];\n}\npub mod grants {\n    pub const ALL: &[&str] = &[HTTP, GRPC, SIGN, ENV, STATE, HMAC];\n}\n",
        )
        .file("crates/monetize-product-wasm/Cargo.toml", &krate("monetize-product-wasm"))
        .file("crates/monetize-product-wasm/src/lib.rs", "pub fn load() {}\n")
        .file("crates/a/Cargo.lock", "version = 4\n")
        .file("crates/a/Cargo.toml", &krate("a"))
        .file("crates/b/Cargo.lock", "version = 4\n")
        .file("crates/b/Cargo.toml", &krate("b"))
        .file("crates/c/Cargo.lock", "version = 4\n")
        .file("crates/c/Cargo.toml", &krate("c"))
        // A28 M1 (T14): the bar is five locks outside xtask; this tree has six.
        .file("crates/d/Cargo.lock", "version = 4\n")
        .file("crates/d/Cargo.toml", &krate("d"))
        .file("crates/e/Cargo.lock", "version = 4\n")
        .file("crates/e/Cargo.toml", &krate("e"));
    t
}

fn check<'a>(card: &'a Card, id: &str) -> &'a score::Check {
    card.areas
        .iter()
        .flat_map(|a| a.checks.iter())
        .find(|c| c.id == id)
        .unwrap_or_else(|| panic!("no check `{id}` in the card:\n{}", card.render()))
}

fn area(card: &Card, a: Area) -> u8 {
    card.areas
        .iter()
        .find(|s| s.area == a)
        .map(|s| s.score)
        .unwrap()
}

#[test]
fn every_area_has_four_bars_and_a_baseline_from_a25() {
    let t = good("shape");
    let card = t.score();
    assert_eq!(card.areas.len(), 6);
    let names: Vec<&str> = card.areas.iter().map(|a| a.area.name()).collect();
    assert_eq!(
        names,
        [
            "tests",
            "robustness",
            "security",
            "observability",
            "docs",
            "release"
        ]
    );
    for a in &card.areas {
        assert_eq!(
            a.checks.len(),
            4,
            "{} has {} bars",
            a.area.name(),
            a.checks.len()
        );
        assert_eq!(
            a.score as usize,
            1 + a.checks.iter().filter(|c| c.pass).count()
        );
    }
    assert_eq!(
        score::BASELINE.iter().map(|(_, b)| *b as u32).sum::<u32>(),
        16,
        "A25 §4: 16 of 30"
    );
    assert_eq!(card.max(), 30);
}

#[test]
fn a_tree_that_meets_every_bar_scores_30() {
    let t = good("good");
    let card = t.score();
    let missed: Vec<String> = card
        .areas
        .iter()
        .flat_map(|a| a.checks.iter())
        .filter(|c| !c.pass)
        .map(|c| format!("{}: {}", c.id, c.evidence.join("; ")))
        .collect();
    assert!(missed.is_empty(), "missed:\n{}", missed.join("\n"));
    assert_eq!(card.total(), 30);
}

#[test]
fn a_tree_that_misses_every_bar_scores_6() {
    let t = bad("bad");
    let card = t.score();
    let met: Vec<&str> = card
        .areas
        .iter()
        .flat_map(|a| a.checks.iter())
        .filter(|c| c.pass)
        .map(|c| c.id)
        .collect();
    assert!(met.is_empty(), "met: {met:?}\n{}", card.render());
    assert_eq!(card.total(), 6);
    for a in &card.areas {
        assert_eq!(a.score, 1);
    }
}

#[test]
fn every_check_carries_evidence() {
    for t in [good("ev-good"), bad("ev-bad")] {
        let card = t.score();
        for c in card.areas.iter().flat_map(|a| a.checks.iter()) {
            assert!(!c.evidence.is_empty(), "{} has no evidence", c.id);
            assert!(!c.bar.is_empty(), "{} has no bar", c.id);
        }
    }
}

#[test]
fn test_code_does_not_count_as_runtime_code() {
    let t = good("cfgtest");
    // An in-file test module, a test-gated module file, a test-gated item, a tests/
    // directory and an examples/ directory: none of these is runtime code.
    t.file(
        "crates/core/src/extra.rs",
        "//! Extra.\npub fn ok() -> u8 { 1 }\n#[cfg(test)]\nmod tests;\n#[cfg(any(test, feature = \"testkit\"))]\npub fn helper() { Some(1).unwrap(); let _ = save(); }\n#[cfg(test)]\nmod inline {\n    fn x() { let s = \"}\"; Some(s).unwrap(); std::thread::spawn(|| {}); }\n    fn y() { Some(2).expect(\"y\"); }\n}\npub fn after() -> u8 { 2 }\n",
    )
    .file("crates/core/src/extra/tests.rs", "fn z() { Some(1).unwrap(); std::process::Command::new(\"sh\"); }\n")
    .file("crates/core/examples/demo.rs", "fn main() { Some(1).unwrap(); std::process::Command::new(\"sh\"); }\n")
    .file("crates/core/tests/more.rs", "#[test]\nfn m() { Some(1).unwrap(); let _ = store.save(1); }\n");
    let card = t.score();
    assert!(check(&card, "robustness.unwrap").pass, "{}", card.render());
    assert!(
        check(&card, "robustness.no-spawn").pass,
        "{}",
        card.render()
    );
    assert!(
        check(&card, "robustness.no-swallowed-save").pass,
        "{}",
        card.render()
    );
    assert!(check(&card, "robustness.threads").pass, "{}", card.render());
    // …and the code after the test module is runtime code again.
    t.file("crates/core/src/extra.rs", "//! Extra.\n#[cfg(test)]\nmod inline {\n    fn x() {}\n}\npub fn after() { Some(2).unwrap(); }\n");
    let card = t.score();
    let c = check(&card, "robustness.unwrap");
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("crates/core/src/extra.rs")),
        "{c:?}"
    );
}

/// R5a: a raw string in a test module (`r#" {name}=""#`) holds quotes and
/// braces that are text, not code. The mask read them as code: a test module ended in
/// the middle and its unwraps counted as runtime (estate-svg's and monetize-cli's, 32).
#[test]
fn a_raw_string_does_not_end_a_test_module() {
    let t = good("rawstr");
    t.file(
        "crates/core/src/raw.rs",
        "//! Raw.\npub fn ok() -> u8 { 1 }\n#[cfg(test)]\nmod tests {\n    fn x() { let a = r#\" {name}=\"\"#; Some(a).unwrap(); }\n    fn y() { let b = r#\"viewBox=\"0 0 1 2\"\"#; let c = r##\"}\"#\"##; Some(b).unwrap(); Some(c).unwrap(); }\n    fn z() { Some(br#\"}\"#).unwrap(); }\n}\npub fn after() -> u8 { 2 }\n",
    );
    let card = t.score();
    let c = check(&card, "robustness.unwrap");
    assert!(
        !c.evidence
            .iter()
            .any(|e| e.contains("crates/core/src/raw.rs")),
        "{c:?}"
    );
    // …and the code after the module is runtime code again.
    t.file("crates/core/src/raw.rs", "//! Raw.\n#[cfg(test)]\nmod tests {\n    fn x() { let a = r#\"x=\"\"#; }\n}\npub fn after() { Some(2).unwrap(); }\n");
    let card = t.score();
    let c = check(&card, "robustness.unwrap");
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("crates/core/src/raw.rs:6")),
        "{c:?}"
    );
}

#[test]
fn tooling_crates_are_not_runtime_code() {
    let t = good("tooling");
    t.file("crates/mock-upcloud/Cargo.toml", &krate("mock-upcloud"))
        .file("crates/mock-upcloud/src/lib.rs", "//! Mock.\npub fn run() { std::process::Command::new(\"podman\"); Some(1).unwrap(); }\n")
        .file("uitest/Cargo.toml", &krate("uitest"))
        .file("uitest/src/lib.rs", "pub fn run() { std::process::Command::new(\"chrome\"); }\n");
    let card = t.score();
    assert!(
        check(&card, "robustness.no-spawn").pass,
        "{}",
        card.render()
    );
    assert!(
        check(&card, "docs.crates").pass,
        "uitest is tooling: {}",
        card.render()
    );
}

#[test]
fn the_supervisor_may_spawn_and_nothing_else() {
    let t = good("spawn");
    t.file("crates/monetize-admin/Cargo.toml", &krate("monetize-admin"))
        .file("crates/monetize-admin/README.md", "# admin\n")
        .file(
            "crates/monetize-admin/src/lib.rs",
            "//! Admin.\npub mod supervise;\n",
        )
        .file(
            "crates/monetize-admin/src/supervise.rs",
            "pub fn keep() { std::process::Command::new(std::env::current_exe().ok()?); }\n",
        );
    assert!(check(&t.score(), "robustness.no-spawn").pass);
    t.file(
        "crates/monetize-admin/src/other.rs",
        "pub fn bad() { tokio::process::Command::new(\"cargo\"); }\n",
    );
    let card = t.score();
    let c = check(&card, "robustness.no-spawn");
    assert!(!c.pass);
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("crates/monetize-admin/src/other.rs:1")),
        "{c:?}"
    );
}

#[test]
fn a_swallowed_save_is_named_and_a_swallowed_send_is_not() {
    let t = good("letunder");
    // A send, a write to a buffer, and the mark-unused idiom over a tuple or a name
    // (`let _ = (set, journal);`) swallow no save.
    t.file(
        "crates/core/src/send.rs",
        "pub fn f(tx: &Tx) { let _ = tx.send(1); let _ = writeln!(out, \"x\"); }\npub fn g(set: S, journal: J) {\n    let _ = (set, journal);\n    let _ = &journal;\n    let _ = journal;\n}\n",
    );
    let card = t.score();
    assert!(
        check(&card, "robustness.no-swallowed-save").pass,
        "{}",
        card.render()
    );
    t.file(
        "crates/core/src/save.rs",
        "pub fn f(s: &S) {\n    let _ = s.journal.append(1);\n}\n",
    );
    let card = t.score();
    let c = check(&card, "robustness.no-swallowed-save");
    assert!(!c.pass);
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("crates/core/src/save.rs:2")),
        "{c:?}"
    );
}

#[test]
fn the_unwrap_bar_is_per_thousand_runtime_lines() {
    let t = good("density");
    // 2,000 runtime lines and 2 unwraps: exactly the bar.
    let mut body = String::from("//! Lines.\n");
    for i in 0..1997 {
        body.push_str(&format!("pub const C{i}: u32 = {i};\n"));
    }
    body.push_str("pub fn a() { Some(1).unwrap(); }\npub fn b() { Some(1).expect(\"b\"); }\n");
    t.file("crates/core/src/lines.rs", &body);
    let card = t.score();
    assert!(check(&card, "robustness.unwrap").pass, "{}", card.render());
    t.file(
        "crates/core/src/one_more.rs",
        "pub fn c() { Some(1).unwrap(); }\n",
    );
    assert!(!check(&t.score(), "robustness.unwrap").pass);
    // unwrap_or, unwrap_or_default and unwrap_or_else are not unwraps.
    t.file("crates/core/src/one_more.rs", "pub fn c() { Some(1).unwrap_or(2); Some(1).unwrap_or_default(); Some(1).unwrap_or_else(|| 3); }\n");
    assert!(check(&t.score(), "robustness.unwrap").pass);
}

#[test]
fn an_ignored_test_misses_the_ignore_bar() {
    let t = good("ignore");
    t.file(
        "crates/core/tests/slow.rs",
        "#[test]\n#[ignore = \"needs KVM\"]\nfn slow() {}\n",
    );
    let card = t.score();
    let c = check(&card, "tests.no-ignore");
    assert!(!c.pass);
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("crates/core/tests/slow.rs")),
        "{c:?}"
    );
}

#[test]
fn a_suite_that_nothing_runs_misses_the_conformance_bar() {
    let t = good("conformance");
    t.remove("crates/monetize-pay/tests/sandbox.rs");
    let card = t.score();
    let c = check(&card, "tests.conformance");
    assert!(!c.pass);
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("payment") && e.contains("0 test")),
        "{c:?}"
    );
    // A missing suite is named as missing.
    t.remove("crates/monetize-cloud/src/conformance.rs");
    let card = t.score();
    let c = check(&card, "tests.conformance");
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("cloud") && e.contains("missing")),
        "{c:?}"
    );
    assert_eq!(
        card.facts
            .iter()
            .find(|(k, _)| k == "conformance_suites_present")
            .map(|(_, v)| *v),
        Some(2)
    );
}

#[test]
fn a_test_on_a_native_plugin_breaks_the_owners_rule() {
    let t = good("native");
    t.file(
        "crates/core/tests/native.rs",
        "#[test]\nfn n() { let l = Linked::commercial(); }\n",
    );
    let card = t.score();
    let c = check(&card, "tests.production-code");
    assert!(!c.pass);
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("crates/core/tests/native.rs")),
        "{c:?}"
    );
    assert_eq!(area(&card, Area::Tests), 4);
}

#[test]
fn lock_drift_names_the_lock_and_the_crate() {
    let t = good("drift");
    assert!(check(&t.score(), "release.lock-drift").pass);
    // The sibling moved; the lock did not.
    t.file(
        "../sib/crates/sib-crate/Cargo.toml",
        "[package]\nname = \"sib-crate\"\nversion = \"1.5.0\"\n",
    );
    let card = t.score();
    let c = check(&card, "release.lock-drift");
    assert!(!c.pass);
    assert!(
        c.evidence.iter().any(|e| e.contains("Cargo.lock")
            && e.contains("sib-crate 1.4.0")
            && e.contains("1.5.0")),
        "{c:?}"
    );
}

#[test]
fn lock_drift_cannot_be_proved_without_the_sibling() {
    let t = good("nosib");
    fs::remove_dir_all(t.top.join("sib")).unwrap();
    let card = t.score();
    let c = check(&card, "release.lock-drift");
    assert!(!c.pass);
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("sib-crate") && e.contains("not checked out")),
        "{c:?}"
    );
}

#[test]
fn a_refused_crate_twice_in_a_lock_is_drift() {
    let t = good("twice");
    t.file(
        "Cargo.lock",
        "version = 4\n\n[[package]]\nname = \"monetize-core\"\nversion = \"0.2.0\"\n\n[[package]]\nname = \"sib-crate\"\nversion = \"1.4.0\"\n\n[[package]]\nname = \"korp-installer\"\nversion = \"0.2.6\"\nsource = \"registry+x\"\n\n[[package]]\nname = \"korp-installer\"\nversion = \"0.2.7\"\nsource = \"registry+x\"\n",
    );
    let c = check(&t.score(), "release.lock-drift").clone();
    assert!(!c.pass);
    assert!(
        c.evidence.iter().any(|e| e.contains("korp-installer")),
        "{c:?}"
    );
}

#[test]
fn more_than_five_locks_outside_xtask_misses_the_structure_bar() {
    // A28 M1 (T14): the root and five detached workspaces, xtask one of them, so five outside xtask.
    let t = good("locks");
    t.file(
        "xtask/Cargo.toml",
        "[package]\nname = \"xtask\"\nversion = \"0.1.0\"\n",
    )
    .file("xtask/Cargo.lock", "version = 4\n");
    for c in ["a", "b", "d", "e"] {
        t.file(&format!("crates/{c}/Cargo.toml"), &krate(c))
            .file(&format!("crates/{c}/Cargo.lock"), "version = 4\n")
            .file(&format!("crates/{c}/src/lib.rs"), "//! A crate.\n")
            .file(&format!("crates/{c}/tests/t.rs"), "#[test]\nfn t() {}\n");
    }
    assert!(
        check(&t.score(), "release.locks").pass,
        "5 locks outside xtask is the bar"
    );
    t.file("crates/f/Cargo.toml", &krate("f"))
        .file("crates/f/Cargo.lock", "version = 4\n");
    let card = t.score();
    let c = check(&card, "release.locks");
    assert!(!c.pass);
    assert!(c.evidence.iter().any(|e| e.contains("6")), "{c:?}");
}

#[test]
fn the_op_table_must_name_exactly_the_ops() {
    let t = good("ops");
    t.file("docs/integration-guide.md", &guide(&["id"], true, true));
    let card = t.score();
    let c = check(&card, "docs.op-table");
    assert!(!c.pass);
    assert!(c.evidence.iter().any(|e| e.contains("meters")), "{c:?}");
}

#[test]
fn a_known_stale_claim_is_found_where_it_is() {
    let t = good("stale");
    t.file(
        "crates/core/src/wire.rs",
        "/// Holger's are keep-only). Absent in a guest.\npub struct W;\n",
    );
    let card = t.score();
    let c = check(&card, "docs.no-stale");
    assert!(!c.pass);
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("crates/core/src/wire.rs:1")),
        "{c:?}"
    );
}

#[test]
fn a_crate_without_docs_or_tests_is_named() {
    let t = good("undoc");
    t.file("crates/bare/Cargo.toml", &krate("bare"))
        .file("crates/bare/src/lib.rs", "pub fn f() {}\n");
    let card = t.score();
    assert!(check(&card, "docs.crates")
        .evidence
        .iter()
        .any(|e| e.contains("crates/bare")));
    assert!(!check(&card, "docs.crates").pass);
    assert!(!check(&card, "tests.suite").pass);
    assert!(check(&card, "tests.suite")
        .evidence
        .iter()
        .any(|e| e.contains("crates/bare")));
}

#[test]
fn the_dead_grants_miss_the_grant_bar() {
    let t = good("grants");
    t.file(
        "crates/monetize-product-guest/src/lib.rs",
        "//! SDK.\npub mod conformance;\npub mod ops {\n    pub const ALL: &[(u32, &str)] = &[\n        (ID, \"id\"),\n        (METERS, \"meters\"),\n    ];\n}\npub mod grants {\n    pub const VOCABULARY: &[&str] = &[HTTP, SIGN, HMAC];\n    pub const PRODUCT: &[&str] = &[HTTP, SIGN, HMAC];\n}\n",
    );
    let card = t.score();
    let c = check(&card, "security.no-dead-grants");
    assert!(!c.pass);
    assert!(c.evidence.iter().any(|e| e.contains("SIGN")), "{c:?}");
}

/// ★ Owner decision 6 (2026-10-26): **`env` is no generic door.** A product kind that grants
/// nothing does not pass while the host's vocabulary still carries `env` for anyone.
#[test]
fn an_env_door_left_in_the_vocabulary_misses_the_grant_bar() {
    let t = good("grants-env");
    t.file(
        "crates/monetize-product-guest/src/lib.rs",
        "//! SDK.\npub mod conformance;\npub mod ops {\n    pub const ALL: &[(u32, &str)] = &[\n        (ID, \"id\"),\n        (METERS, \"meters\"),\n    ];\n}\npub mod grants {\n    pub const VOCABULARY: &[&str] = &[HTTP, ENV, HMAC];\n    pub const PRODUCT: &[&str] = &[];\n}\n",
    );
    let card = t.score();
    let c = check(&card, "security.no-dead-grants");
    assert!(!c.pass, "{c:?}");
    assert!(
        c.evidence
            .iter()
            .any(|e| e.contains("ENV") && e.contains("VOCABULARY")),
        "{c:?}"
    );
}

/// ★ Owner decision 6: **the vocabulary is split by guest kind.** One shared list (the old
/// `grants::ALL`) is not the product kind's set, however short it is.
#[test]
fn a_vocabulary_not_split_by_kind_misses_the_grant_bar() {
    let t = good("grants-unsplit");
    t.file(
        "crates/monetize-product-guest/src/lib.rs",
        "//! SDK.\npub mod conformance;\npub mod ops {\n    pub const ALL: &[(u32, &str)] = &[\n        (ID, \"id\"),\n        (METERS, \"meters\"),\n    ];\n}\npub mod grants {\n    pub const ALL: &[&str] = &[HTTP, HMAC];\n}\n",
    );
    let card = t.score();
    let c = check(&card, "security.no-dead-grants");
    assert!(!c.pass, "{c:?}");
    assert!(
        c.evidence.iter().any(|e| e.contains("grants::PRODUCT")),
        "{c:?}"
    );
}

#[test]
fn score_json_is_written_with_every_area_and_check() {
    let t = good("json");
    let card = t.score();
    let out = t.top.join("out/score.json");
    score::write_json(&card, &out).unwrap();
    let j = fs::read_to_string(&out).unwrap();
    assert!(j.contains("\"total\": 30"), "{j}");
    assert!(j.contains("\"baseline_total\": 16"), "{j}");
    for a in [
        "tests",
        "robustness",
        "security",
        "observability",
        "docs",
        "release",
    ] {
        assert!(
            j.contains(&format!("\"area\": \"{a}\"")),
            "{a} missing in {j}"
        );
    }
    assert!(j.contains("\"id\": \"release.lock-drift\""));
    // Balanced and escaped: the evidence carries quotes and backslashes.
    assert_eq!(j.matches('{').count(), j.matches('}').count());
    assert_eq!(score::json_string("a\"b\\c\n"), "\"a\\\"b\\\\c\\n\"");
}

#[test]
fn the_report_prints_each_area_with_its_evidence() {
    let t = bad("render");
    let text = t.score().render();
    for a in [
        "tests",
        "robustness",
        "security",
        "observability",
        "docs",
        "release",
    ] {
        assert!(text.contains(a), "{a} missing:\n{text}");
    }
    assert!(text.contains("6 / 30"), "{text}");
    assert!(text.contains("baseline 16"), "{text}");
    assert!(
        text.contains("crates/core/src/lib.rs:1"),
        "the evidence names the line:\n{text}"
    );
}

#[test]
fn the_scorer_reads_only_the_tree() {
    // No network, no cargo, no git: a tree with no .git scores, and twice the same.
    let t = good("static");
    assert!(!Path::new(&t.root().join(".git")).exists());
    let (a, b) = (t.score(), t.score());
    assert_eq!(a.to_json(), b.to_json());
}

/// ★ **The CI plan is read where it lives** (2026-10-25: monetize's plan moved from
/// `xtask/src/main.rs` to `xtask/src/ci.rs`, and the bar read it as missing): a main.rs that
/// only dispatches and a ci.rs that holds the steps meet the bar; a tree with neither does not.
#[test]
fn the_ci_bar_reads_the_plan_in_xtask_ci_rs() {
    let t = good("ci-rs");
    t.file(
        "xtask/src/main.rs",
        "fn task_ci() { xtask::ci::plan(&root); }\n",
    );
    let c = check(&t.score(), "release.ci").clone();
    assert!(!c.pass, "the plan is nowhere yet: {c:?}");
    t.file(
        "xtask/src/ci.rs",
        "pub fn plan() { let s = vec![nornir_xtask_support::lockcheck::step(root), Step::cargo(\"test\", &[\"test\", \"--workspace\", \"--locked\"])]; }\n",
    );
    let c = check(&t.score(), "release.ci").clone();
    assert!(c.pass, "the plan in xtask/src/ci.rs: {c:?}");
    assert!(
        c.evidence.iter().any(|e| e.contains("xtask/src/ci.rs")),
        "the evidence names where it read: {c:?}"
    );
}