xtask/tests/one_clone.rs
//! **RED-when-broken: gunnar builds from ONE clone.**
//!
//! The gate itself is two set differences over a directory walk, so it compiles
//! nothing and runs in milliseconds — which matters, because a gate slow enough
//! to skip gets skipped, and this one's whole job is to fire on a one-line
//! manifest edit that looks harmless.
//!
//! ## Why the red half is the longer half here
//!
//! A gate over "did the walk find anything outside the tree" goes green for two
//! very different reasons: nothing is outside the tree, or the walk found
//! nothing at all. The second is the easier failure to write — a scanner that
//! silently skips comment lines can just as silently skip every line, a walk
//! that skips `target/` can skip the detached workspaces beside it, and a
//! lockfile parser that looks for `source = ` can decide every package has one.
//! This file asserts against each of those by name:
//!
//! * [`the_walk_actually_sees_the_tree`] — the manifest and lockfile
//! populations are real, and specifically include the four DETACHED
//! workspaces, which are invisible to `cargo metadata` at the root and are
//! exactly where the five sibling paths of 2026-09-12 were hiding.
//! * [`the_scanner_reads_real_path_lines`] — in-repo `path = "…"` literals are
//! SEEN and judged in-repo, so "no escapes" is a verdict and not a silence.
//! * [`a_sibling_path_is_reported`] — each of the five real lines that were
//! removed, fed back in as text, is reported with the right resolution.
//! * [`a_commented_sibling_path_is_not_a_dependency`] — the prose that
//! FORBIDS the shape must not be mistaken for the shape.
//! * [`a_foreign_local_package_in_a_lockfile_is_reported`] — the lockfile half
//! fires on the shape a stale lock has.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use xtask::one_clone::{
crate_names, declared_siblings, escapes_in, fold, foreign_lock_packages, local_packages_in,
lockfiles, manifests, missing_siblings, path_literal, sibling_of, undeclared,
undeclared_escapes,
};
/// `CARGO_MANIFEST_DIR` is `<root>/xtask`.
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent")
.to_path_buf()
}
/// **KNOWN EXCEPTION — TEMPORARY (release-3 merge, 2026-10-03).** The crates the
/// root `[patch.crates-io]` table points at sibling checkouts until edda (and
/// znippy) publish them on gix 0.88 / russh 0.63 / wasmtime 49: crates.io's
/// releases are on the older stack, and gunnar cannot link both (see the table's
/// own comment in the root `Cargo.toml`; `bench/` restates it because a detached
/// workspace does not inherit `[patch]`).
///
/// They ARE escapes, the scanner is right to see them, and the gate
/// ([`one_clone_is_the_whole_build`]) passes them only because `edda` and
/// `znippy` are declared siblings. The two tests below read real manifests to
/// prove the scanner reads real lines, so they judge these lines by NAME: every
/// escape must be one of these, in a `[patch.crates-io]` table, into a declared
/// sibling — and the root must still carry every name listed, so the day a crate
/// is published and its patch line goes, this list goes red until the name is
/// struck from it. When it is empty, delete it and the helper below.
///
/// **Since 2026-10-22 the table is not in the root manifest at all**: a lone clone
/// must load the root workspace, and cargo refuses a `[patch]` whose path is
/// missing. **Since G-B7 (2026-10-26) the root takes it from nowhere**: each estate
/// workspace (`xtask::siblings::ESTATE_WORKSPACES`) carries the entries its lock takes
/// in its own `[patch.crates-io]`, and together they hold every name listed here
/// (the same list as `xtask::siblings::ESTATE_PATCHES`).
const TEMPORARY_PATCHES: [&str; 31] = [
"gunnar-pack",
"gunnar-delta",
"gunnar-bitmap",
"gunnar-sendpack",
"gunnar-stage",
"gunnar-pktline",
"nornir-git",
"nornir-build-thing",
"storage-git-gix",
"git-storage-trait",
"estate-clock",
"disk-snapshot",
"appliance-pid1",
"wg-appliance-core",
"edda-admit",
"nornir-auth-oidc",
"nornir-auth-saml",
"nornir-auth-webauthn",
"nornir-identity",
"nornir-identity-session",
"nornir-identity-directory",
"nornir-identity-sso",
"nornir-auth-sshsig",
"nornir-service",
"nordisk-pairing",
"nordisk-narrate",
"korp-installer",
// monetize's crates.io line, 0.3.0 (A3, 2026-10-26).
"monetize-product",
"monetize-embed",
"monetize-proto",
"monetize-estate-side",
];
/// The escapes in `manifest` (its real `text`) that the [`TEMPORARY_PATCHES`]
/// exception does NOT cover, plus the names of the exception it found there.
///
/// An escape is covered only when ALL of: its key is a listed name, its line sits
/// in a `[patch.crates-io]` table, and it lands in a declared sibling. Anything
/// else — a member path misjudged as an escape, a new path dependency out of the
/// tree, a patch into an undeclared checkout — is returned as uncovered.
fn beyond_the_temporary_patches(
manifest: &str,
text: &str,
) -> (Vec<xtask::one_clone::Escape>, BTreeSet<String>) {
let siblings = declared_siblings();
let lines: Vec<&str> = text.lines().collect();
let section_of = |line: usize| -> &str {
lines[..line]
.iter()
.rev()
.map(|l| l.trim())
.find(|l| l.starts_with('['))
.unwrap_or("")
};
let mut uncovered = Vec::new();
let mut named = BTreeSet::new();
for e in escapes_in(manifest, text) {
let key = lines[e.line - 1].split('=').next().unwrap_or("").trim();
let covered = TEMPORARY_PATCHES.contains(&key)
&& section_of(e.line - 1) == "[patch.crates-io]"
&& sibling_of(&e.resolved).is_some_and(|s| siblings.iter().any(|d| d == s));
if covered {
named.insert(key.to_string());
} else {
uncovered.push(e);
}
}
(uncovered, named)
}
/// **THE GATE.** Nothing in this repository names a path outside it except into
/// a sibling the recipe declares, and no lockfile records having resolved a
/// local package that neither this repository nor a declared sibling defines.
///
/// Until 2026-09-29 this asserted the stricter sentence — nothing outside the
/// tree at all — and was red on master, release-2 and release-3 alike: 21
/// lines, every one into `edda`, `Skidbladnir`, `monetize` or `gunnar-ui`, all
/// four listed in `[build.toolchain] siblings` in the commit that added the
/// path (see `xtask/src/one_clone.rs`, "Why the rule grew a declared
/// exception"). The escapes are still printed and still judged; what changed
/// is which of them the BUILD can resolve.
#[test]
fn one_clone_is_the_whole_build() {
let siblings = declared_siblings();
let missing = missing_siblings(&root(), &siblings);
assert!(
missing.is_empty(),
"the recipe declares siblings {missing:?} that are not checked out beside {} — the \
workspace cannot resolve without them, and the lockfile half below cannot vouch \
for a package it cannot read",
root().display()
);
let escapes = undeclared_escapes(&root(), &siblings);
assert!(
escapes.is_empty(),
"gunnar must build from ONE clone plus its DECLARED siblings {siblings:?}, and these \
lines say otherwise:\n{}\n\n\
`cargo` reads a path dependency's manifest at RESOLVE time, so each of \
these makes `cargo metadata` — not merely `cargo build` — a hard error in \
the build container, which mounts only the declared siblings. Fix: take the \
dependency to a crates.io VERSION, or declare its sibling in \
`crates/gunnar-recipe/src/recipe.rs` `toolchain().siblings` in the SAME commit.",
escapes
.iter()
.map(|e| format!(" {}", e.describe()))
.collect::<Vec<_>>()
.join("\n")
);
let foreign = foreign_lock_packages(&root(), &siblings);
assert!(
foreign.is_empty(),
"a lockfile records a LOCAL package neither this repository nor a declared sibling \
defines, so a resolve reached a checkout the build container will not have, even if \
the manifests no longer ask for one:\n{}",
foreign
.iter()
.map(|f| format!(" {}", f.describe()))
.collect::<Vec<_>>()
.join("\n")
);
}
/// **The declaration narrows the gate by NAME, and nothing else.**
///
/// A path into a sibling that is not declared is reported, one into a declared
/// sibling is not, and the shapes no declaration can make whole — a path above
/// the checkout's parent, an absolute path — are reported whatever is declared.
/// A gate that took the declaration as "anything goes" would be red here.
#[test]
fn a_path_into_an_undeclared_sibling_is_still_reported() {
let declared = vec!["edda".to_string()];
let line = |l: &str| escapes_in("crates/gunnar-server/Cargo.toml", l);
let into_edda = line(r#"disk-snapshot = { path = "../../../edda/crates/disk-snapshot" }"#);
assert_eq!(into_edda.len(), 1, "the lexical half still SEES the escape");
assert!(
undeclared(into_edda, &declared).is_empty(),
"edda is declared"
);
let into_tunnr = line(r#"tunnr = { path = "../../../tunnr/crates/tunnr" }"#);
assert_eq!(
undeclared(into_tunnr, &declared).len(),
1,
"tunnr is NOT declared, so the build container would not have it"
);
let above = line(r#"far = { path = "../../../../edda/crates/x" }"#);
assert_eq!(above[0].resolved, "../../edda/crates/x");
assert_eq!(
undeclared(above, &declared).len(),
1,
"a path above the checkout's parent is not the sibling `edda`, whatever its last name"
);
let absolute = line(r#"abs = { path = "/home/alice/git/edda" }"#);
assert_eq!(
undeclared(absolute, &declared).len(),
1,
"absolute stays refused"
);
assert_eq!(sibling_of("../edda/crates/admit"), Some("edda"));
assert_eq!(sibling_of("../../edda"), None);
assert_eq!(sibling_of("crates/gunnar-store"), None);
assert_eq!(sibling_of("/abs"), None);
}
/// **The population is real**, and it includes the trees that hid the problem.
#[test]
fn the_walk_actually_sees_the_tree() {
let root = root();
let found: BTreeSet<String> = manifests(&root).into_iter().collect();
assert!(
found.len() >= 20,
"the manifest walk found only {} Cargo.toml files — it is not seeing the \
tree, and a gate over an empty population is not a pass: {found:?}",
found.len()
);
// The four DETACHED workspaces. `cargo metadata` at the root cannot see any
// of them — they are not members and they carry their own `[workspace]` —
// and two of the four are where the sibling paths lived.
for detached in [
"bench/Cargo.toml",
"bench-stock/Cargo.toml",
"deploy/upcloud/Cargo.toml",
"installtest/Cargo.toml",
] {
assert!(
found.contains(detached),
"the walk missed the detached workspace `{detached}`, which is precisely \
the kind of tree a crate-graph walk cannot see and where the sibling \
paths removed on 2026-09-12 were living"
);
}
// And the vendored copy, which is an in-repo path dep and must be judged
// rather than skipped.
assert!(
found.contains("vendor/gix-parallel/Cargo.toml"),
"the walk missed the vendored gix-parallel"
);
let locks: BTreeSet<String> = lockfiles(&root).into_iter().collect();
for lock in [
"Cargo.lock",
"bench/Cargo.lock",
"bench-stock/Cargo.lock",
"deploy/upcloud/Cargo.lock",
"installtest/Cargo.lock",
] {
assert!(locks.contains(lock), "the lockfile walk missed `{lock}`");
}
let ours = crate_names(&root);
for name in [
"gunnar-cli",
"gunnar-store",
"gunnar-upcloud",
"gunnar-bench-stock",
"xtask",
] {
assert!(
ours.contains(name),
"`{name}` is defined by a Cargo.toml in this repository but the name scan \
missed it — an allowlist that misses our own crates makes the lockfile \
half red for the wrong reason"
);
}
}
/// **In-repo `path = "…"` lines are SEEN**, not skipped. Without this the gate
/// above would be green on a scanner that reads nothing.
#[test]
fn the_scanner_reads_real_path_lines() {
assert_eq!(
path_literal(r#"gunnar-store = { version = "1.1.0", path = "crates/gunnar-store" }"#),
Some("crates/gunnar-store")
);
assert_eq!(path_literal(r#"path = "src/main.rs""#), Some("src/main.rs"));
assert_eq!(path_literal(r#"gunnar-core = { version = "1.1.0" }"#), None);
// `rust-version`, `repository` and friends end in something that is not
// `path`; a substring match would misread them.
assert_eq!(path_literal(r#"searchpath = "../../elsewhere""#), None);
// The root manifest names seventeen in-repo crates by path. Every one of
// them is read, and every one of them is judged INSIDE the tree.
let text = std::fs::read_to_string(root().join("Cargo.toml")).expect("the root manifest");
let seen = text.lines().filter(|l| path_literal(l).is_some()).count();
assert!(
seen >= 15,
"the scanner found only {seen} `path =` lines in the root manifest, which \
names every workspace member by path — it is reading almost nothing"
);
// Every escape the root holds is the TEMPORARY patch table, by name — and
// the table is still whole, so the exception cannot outlive it.
let (uncovered, named) = beyond_the_temporary_patches("Cargo.toml", &text);
assert!(
uncovered.is_empty(),
"the root manifest's own member paths were misjudged as escapes, or an escape \
beyond the TEMPORARY patch table appeared:\n{}",
uncovered
.iter()
.map(|e| format!(" {}", e.describe()))
.collect::<Vec<_>>()
.join("\n")
);
assert!(
named.is_empty(),
"the root manifest patches into a sibling again: {named:?}"
);
// The table itself, in the estate workspaces' own manifests (G-B7), each judged at its
// own path; together they name every listed crate.
let mut named = BTreeSet::new();
for ws in xtask::siblings::ESTATE_WORKSPACES {
let rel = format!("{ws}/Cargo.toml");
let text = std::fs::read_to_string(root().join(&rel)).expect(&rel);
let (uncovered, here) = beyond_the_temporary_patches(&rel, &text);
// Only the patch tables are judged here: an estate workspace's direct `path =`
// dependencies into a declared sibling are the gate's business, not this list's.
let lines: Vec<&str> = text.lines().collect();
let in_patch = |line: usize| {
lines[..line]
.iter()
.rev()
.map(|l| l.trim())
.find(|l| l.starts_with('['))
== Some("[patch.crates-io]")
};
let beyond: Vec<_> = uncovered.iter().filter(|e| in_patch(e.line - 1)).collect();
assert!(
beyond.is_empty(),
"{rel}'s sibling patches hold an escape beyond TEMPORARY_PATCHES:\n{}",
beyond
.iter()
.map(|e| format!(" {}", e.describe()))
.collect::<Vec<_>>()
.join("\n")
);
named.extend(here);
}
let listed: BTreeSet<String> = TEMPORARY_PATCHES.iter().map(|s| s.to_string()).collect();
assert_eq!(
named, listed,
"the estate workspaces' TEMPORARY [patch.crates-io] tables no longer match TEMPORARY_PATCHES — \
strike a published crate from the list (and delete the list when it is empty)"
);
let shared: BTreeSet<String> = xtask::siblings::ESTATE_PATCHES
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(
shared, listed,
"xtask::siblings::ESTATE_PATCHES and TEMPORARY_PATCHES name the same crates"
);
// The folding is the part that decides, so it is asserted directly.
assert_eq!(
fold("deploy/upcloud", "../../../edda/crates/nornir-hash"),
"../edda/crates/nornir-hash"
);
assert_eq!(
fold("bench-stock", "../../znippy-zoomies"),
"../znippy-zoomies"
);
assert_eq!(
fold("bench-stock", "../bench/examples/deltachain-probe.rs"),
"bench/examples/deltachain-probe.rs"
);
assert_eq!(
fold("crates/gunnar-cli", "../gunnar-store"),
"crates/gunnar-store"
);
assert_eq!(fold("crates/gunnar-cli", "."), "crates/gunnar-cli");
}
/// **Each of the five real lines, fed back in, is reported.**
///
/// These are the literal dependency lines this repository carried until
/// 2026-09-12, verbatim. A gate proven only against an invented example can be
/// green on the shape that actually occurred.
#[test]
fn a_sibling_path_is_reported() {
let cases: [(&str, &str, &str); 5] = [
(
"deploy/upcloud/Cargo.toml",
r#"nornir-hash = { path = "../../../edda/crates/nornir-hash" }"#,
"../edda/crates/nornir-hash",
),
(
"deploy/upcloud/Cargo.toml",
r#"nornir-xtask-support = { path = "../../../edda/crates/nornir-xtask-support" }"#,
"../edda/crates/nornir-xtask-support",
),
(
"deploy/upcloud/Cargo.toml",
r#"nornir-build-thing = { path = "../../../edda/crates/nornir-build-thing", default-features = false }"#,
"../edda/crates/nornir-build-thing",
),
(
"deploy/upcloud/Cargo.toml",
r#"nornir-git = { version = "0.1.9", path = "../../../edda/crates/nornir-git", default-features = false }"#,
"../edda/crates/nornir-git",
),
(
"bench-stock/Cargo.toml",
r#"znippy-zoomies = { path = "../../znippy-zoomies", default-features = false }"#,
"../znippy-zoomies",
),
];
for (manifest, line, expect) in cases {
let found = escapes_in(manifest, line);
assert_eq!(
found.len(),
1,
"the gate did not report the sibling path it was written for: {manifest} — {line}"
);
assert_eq!(found[0].resolved, expect, "wrong resolution for {line}");
assert_eq!(found[0].line, 1);
assert!(found[0].describe().contains(expect));
}
// An ABSOLUTE path is the other way out of the tree and is reported too.
let abs = escapes_in(
"crates/gunnar-cli/Cargo.toml",
r#"thing = { path = "/home/alice/git/edda" }"#,
);
assert_eq!(
abs.len(),
1,
"an absolute path out of the tree was not reported"
);
}
/// **The prose that forbids the shape is not the shape.**
///
/// `bench/Cargo.toml` spells `path = "../../edda/…"` out in full in order to
/// ban it, and `bench-stock/Cargo.toml` now carries a commented-out
/// `[patch.crates-io]` recipe for local work. Both must read as documentation.
/// This is not a hypothetical: the gate above is red on this repository without
/// it.
#[test]
fn a_commented_sibling_path_is_not_a_dependency() {
assert_eq!(path_literal(r#"# * NEVER `path = "../../edda/…"`."#), None);
assert_eq!(
path_literal(r#"# znippy-zoomies = { path = "../../znippy-zoomies" }"#),
None
);
assert_eq!(
path_literal(" # nornir-hash = { path = \"../../../edda/crates/nornir-hash\" }"),
None
);
assert!(escapes_in("bench/Cargo.toml", "# path = \"../../edda/x\"\n").is_empty());
// And the two real files prove it on their real contents: both DO contain
// the forbidden literal in a comment, so a scanner that ignored comments
// only by accident would be red here.
for f in ["bench/Cargo.toml", "bench-stock/Cargo.toml"] {
let text = std::fs::read_to_string(root().join(f)).expect(f);
assert!(
text.contains("edda/") || text.contains("znippy-zoomies\" }"),
"{f} no longer documents the forbidden shape, so this proof measures nothing"
);
// `bench/` restates the root's TEMPORARY patch table (a detached
// workspace does not inherit `[patch]`); those lines are escapes and are
// judged by name. Nothing else in either file may be.
let (uncovered, _) = beyond_the_temporary_patches(f, &text);
assert!(
uncovered.is_empty(),
"{f} was misjudged on its own comments:\n{}",
uncovered
.iter()
.map(|e| format!(" {}", e.describe()))
.collect::<Vec<_>>()
.join("\n")
);
}
}
/// **The lockfile half fires**, on the shape a stale lock has: a package with
/// no `source =` line whose name this repository does not define.
#[test]
fn a_foreign_local_package_in_a_lockfile_is_reported() {
let lock = r#"
[[package]]
name = "gunnar-store"
version = "1.1.0"
dependencies = [
"gix-hash",
]
[[package]]
name = "nornir-hash"
version = "0.1.0"
dependencies = [
"sha2",
]
[[package]]
name = "nornir-git"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4c150914645358e55af06edebadbaa5a1d94ed9aceea638001ba5929826c07"
"#;
let locals = local_packages_in(lock);
assert_eq!(
locals,
vec!["gunnar-store".to_string(), "nornir-hash".to_string()],
"the parser must call a source-less package LOCAL and a registry one not — \
if it returned everything or nothing the gate would be meaningless"
);
let ours = crate_names(&root());
assert!(
ours.contains("gunnar-store"),
"our own crate must not be foreign"
);
assert!(
!ours.contains("nornir-hash"),
"`nornir-hash` lives in edda; if this repository ever defines a crate by \
that name this proof stops measuring anything"
);
}