xtask/tests/lone_clone.rs
//! **A lone clone of holger loads, and `cargo build --release` gives the server**
//! (owner order 2026-10-22).
//!
//! Somebody who clones holger ALONE, with no `../edda`, `../Skidbladnir`,
//! `../monetize`, `../tunnr`, `../draupnir`, `../modgunn` or `../facett` beside it,
//! must be able to run plain `cargo build --release` at the root and get
//! `target/release/holger`. cargo refuses to load a workspace when ANY member's
//! dependency, or any `[patch]` entry, points at a path that is not there, optional
//! and dev edges included. So:
//!
//! * no root-workspace member reaches a path outside the repository, directly or
//! through an in-repo crate it names by path (a detached workspace included);
//! * the root `[patch]` tables point only inside the repository;
//! * the sibling patches live in `.cargo/siblings.toml`, which is NOT committed: an
//! estate checkout writes it from `.cargo/estate-siblings.toml`
//! (`cargo xtask siblings`, and every other `cargo xtask` verb refreshes it), and
//! `.cargo/config.toml` includes it as OPTIONAL;
//! * `default-members` is `server/cli` alone.
//!
//! No network and no sibling are needed: these read the manifests. The end-to-end
//! proof (clone alone, `cargo build --release`, boot, `/healthz`) is
//! `cargo xtask lone-clone --stage`.
use std::collections::BTreeSet;
use std::path::{Component, Path, PathBuf};
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent")
.to_path_buf()
}
/// Lexical normalisation (no filesystem): what cargo itself does with a declared path.
fn normalise(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
fn manifest(dir: &Path) -> toml::Table {
let text = std::fs::read_to_string(dir.join("Cargo.toml"))
.unwrap_or_else(|e| panic!("{}: {e}", dir.display()));
text.parse()
.unwrap_or_else(|e| panic!("{}: {e}", dir.display()))
}
/// The root workspace's member directories (globs expanded, `exclude` honoured).
fn members(root: &Path) -> Vec<PathBuf> {
let ws = manifest(root)["workspace"]
.as_table()
.expect("[workspace]")
.clone();
let excluded: BTreeSet<PathBuf> = ws
.get("exclude")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.map(|e| root.join(e))
.collect()
})
.unwrap_or_default();
let mut out = Vec::new();
for m in ws["members"].as_array().expect("members") {
let m = m.as_str().unwrap();
match m.strip_suffix("/*") {
Some(base) => {
for e in std::fs::read_dir(root.join(base)).unwrap().flatten() {
if e.path().join("Cargo.toml").is_file() {
out.push(e.path());
}
}
}
None => out.push(root.join(m)),
}
}
out.retain(|p| !excluded.contains(p));
out.sort();
out
}
/// Every `(name, path)` dependency of a manifest, of every kind and target.
fn path_deps(m: &toml::Table) -> Vec<(String, String)> {
let mut tables: Vec<&toml::Table> = Vec::new();
for kind in ["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(t) = m.get(kind).and_then(|v| v.as_table()) {
tables.push(t);
}
if let Some(targets) = m.get("target").and_then(|v| v.as_table()) {
for t in targets.values() {
if let Some(t) = t.get(kind).and_then(|v| v.as_table()) {
tables.push(t);
}
}
}
}
let mut out = Vec::new();
for t in tables {
for (name, spec) in t {
if let Some(p) = spec.get("path").and_then(|v| v.as_str()) {
out.push((name.clone(), p.to_string()));
}
}
}
out
}
#[test]
fn no_member_of_the_root_workspace_reaches_a_sibling_checkout() {
let root = root();
let mut outside = Vec::new();
let mut seen = BTreeSet::new();
let mut todo: Vec<PathBuf> = members(&root);
assert!(
todo.len() > 20,
"only {} members found — the walk went blind",
todo.len()
);
while let Some(dir) = todo.pop() {
if !seen.insert(dir.clone()) {
continue;
}
for (name, p) in path_deps(&manifest(&dir)) {
let target = normalise(&dir.join(&p));
if target.starts_with(&root) {
todo.push(target);
} else {
outside.push(format!(
"{} -> {name} ({p})",
dir.strip_prefix(&root).unwrap().display()
));
}
}
}
assert!(
outside.is_empty(),
"the root workspace reaches sibling checkouts by path, so a lone clone cannot load it. \
Name the crate by version and put its path in .cargo/estate-siblings.toml:\n {}",
outside.join("\n ")
);
}
#[test]
fn the_root_patch_tables_stay_inside_the_repo() {
let root = root();
let m = manifest(&root);
let mut outside = Vec::new();
if let Some(patch) = m.get("patch").and_then(|v| v.as_table()) {
for (registry, t) in patch {
for (name, spec) in t.as_table().unwrap() {
if let Some(p) = spec.get("path").and_then(|v| v.as_str()) {
if !normalise(&root.join(p)).starts_with(&root) {
outside.push(format!("[patch.{registry}] {name} = {p}"));
}
}
}
}
}
assert!(
outside.is_empty(),
"the root [patch] points out of the repo at {outside:?}: a lone clone cannot load the \
workspace. Sibling patches belong in .cargo/estate-siblings.toml"
);
}
#[test]
fn the_default_member_is_the_holger_server_alone() {
let ws = manifest(&root())["workspace"].as_table().unwrap().clone();
let dm: Vec<&str> = ws["default-members"]
.as_array()
.expect("default-members")
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(
dm,
["server/cli"],
"a plain `cargo build --release` must build the server and nothing else"
);
}
/// **B7 (Holger v2.0, the owner's publish of 2026-10-26): the ROOT is the open core and takes
/// NO sibling patch.** Its `.cargo/config.toml` includes no sibling table, and its committed
/// lock is the crates.io lock: every package in it that is not holger's own carries a
/// registry source, so the plain lone clone resolves with `--locked`. (The uncommitted
/// `.cargo/siblings.toml` may still be written by `cargo xtask`; nothing at the root reads it.)
#[test]
fn the_root_takes_no_sibling_patch_and_locks_from_crates_io() {
let root = root();
let config =
std::fs::read_to_string(root.join(".cargo/config.toml")).expect(".cargo/config.toml");
assert!(
!config
.lines()
.map(str::trim)
.any(|l| l.starts_with("include") && l.contains("siblings")),
".cargo/config.toml must not include the sibling patches: the root builds from crates.io"
);
let ignore = std::fs::read_to_string(root.join(".gitignore")).expect(".gitignore");
assert!(
ignore.lines().any(|l| l.trim() == "/.cargo/siblings.toml"),
".cargo/siblings.toml stays ignored"
);
// holger's own packages: every manifest in the repository (the root members and the
// estate crates their optional and dev edges name, `REACHED_BY_AN_EDGE`).
fn walk(d: &Path, out: &mut BTreeSet<String>) {
for e in std::fs::read_dir(d).into_iter().flatten().flatten() {
let p = e.path();
let n = e.file_name().to_string_lossy().to_string();
if p.is_dir() && !n.starts_with('.') && n != "target" {
walk(&p, out);
} else if n == "Cargo.toml" {
if let Some(name) = std::fs::read_to_string(&p)
.ok()
.and_then(|t| t.parse::<toml::Table>().ok())
.and_then(|m| {
m.get("package")
.and_then(|p| p.get("name"))
.and_then(|v| v.as_str())
.map(String::from)
})
{
out.insert(name);
}
}
}
}
let mut own = BTreeSet::new();
walk(&root, &mut own);
let lock: toml::Table = std::fs::read_to_string(root.join("Cargo.lock"))
.unwrap()
.parse()
.unwrap();
let pathed: Vec<String> = lock["package"]
.as_array()
.unwrap()
.iter()
.filter(|p| p.get("source").is_none())
.map(|p| {
format!(
"{} {}",
p["name"].as_str().unwrap(),
p["version"].as_str().unwrap()
)
})
.filter(|nv| !own.contains(nv.split(' ').next().unwrap()))
.collect();
assert!(pathed.is_empty(), "the root lock holds sibling crates by path, so a plain lone clone's --locked build is refused: {pathed:?}");
assert!(
lock.get("patch").is_none(),
"the root lock carries [[patch.unused]] entries: it was written with sibling patches"
);
}
/// The estate workspaces (`estate/`, `holger-estate-front/`) keep the sibling patches, in
/// their own manifests, so they apply there from any directory and never at the root.
/// Each table is `.cargo/estate-siblings.toml`'s, the same siblings, its paths rebased.
#[test]
fn the_estate_workspaces_carry_the_sibling_patches() {
let root = root();
let template: toml::Table = std::fs::read_to_string(root.join(".cargo/estate-siblings.toml"))
.unwrap()
.parse()
.unwrap();
let want = template["patch"]["crates-io"].as_table().unwrap();
for ws in ["estate", "holger-estate-front"] {
let m = manifest(&root.join(ws));
let got = m
.get("patch")
.and_then(|p| p.get("crates-io"))
.and_then(|t| t.as_table())
.unwrap_or_else(|| panic!("{ws}/Cargo.toml carries no [patch.crates-io]"));
let names = |t: &toml::Table| t.keys().cloned().collect::<BTreeSet<_>>();
assert_eq!(
names(got),
names(want),
"{ws}/Cargo.toml patches the template's crates"
);
for (name, spec) in want {
let from_root = normalise(&root.join(spec["path"].as_str().unwrap()));
let from_ws = normalise(&root.join(ws).join(got[name]["path"].as_str().unwrap()));
assert_eq!(
from_ws, from_root,
"{ws}: {name} points elsewhere than the template"
);
}
}
}
/// The estate table: every entry a sibling path, and the siblings it reaches are the
/// ones the launcher checks for (`xtask/boot/src/siblings.rs` SIBLINGS).
#[test]
fn the_estate_table_reaches_only_the_checked_siblings() {
let root = root();
let estate: toml::Table = std::fs::read_to_string(root.join(".cargo/estate-siblings.toml"))
.expect(".cargo/estate-siblings.toml")
.parse()
.expect("estate-siblings.toml parses");
let table = estate["patch"]["crates-io"]
.as_table()
.expect("[patch.crates-io]");
assert!(
table.len() > 20,
"the estate sibling patch table has {} entries",
table.len()
);
let mut reached = BTreeSet::new();
for (name, spec) in table {
let p = spec
.get("path")
.and_then(|v| v.as_str())
.unwrap_or_else(|| panic!("{name} has no path"));
let sibling = p
.strip_prefix("../")
.and_then(|r| r.split('/').next())
.unwrap_or_else(|| panic!("{name} = {p} is not a sibling path"));
reached.insert(sibling.to_string());
}
let code = std::fs::read_to_string(root.join("xtask/boot/src/siblings.rs")).unwrap();
// The whole array, one line or rustfmt's many.
let at = code.find("pub const SIBLINGS").expect("SIBLINGS");
let line = &code[at..at + code[at..].find("];").expect("its end")];
let checked: BTreeSet<String> = line
.split('"')
.skip(1)
.step_by(2)
.map(String::from)
.collect();
assert_eq!(
reached, checked,
"the launcher must check for exactly the siblings the table reaches"
);
}
/// ★ **`cargo xtask` never rewrites a committed lock** (HOL-PRODUCTS-9): the launcher hands
/// every verb to the real `xtask` (an `estate/` member) with `--locked`, so sibling checkouts
/// that are stale (or ahead) refuse the run by name instead of rewriting `estate/Cargo.lock`
/// under it (`cargo xtask products` did, silently).
#[test]
fn cargo_xtask_runs_the_real_xtask_locked() {
let code = std::fs::read_to_string(root().join("xtask/boot/src/main.rs")).unwrap();
let start = code
.find("Command::new(cargo)")
.expect("the launcher runs the real xtask through cargo");
let run = &code[start..start + code[start..].find(".status()").expect("and waits for it")];
assert!(
run.contains(r#""run""#) && run.contains(r#""xtask""#),
"the launcher runs the real xtask with `cargo run --package xtask`: {run}"
);
assert!(
run.contains(r#""--locked""#),
"the launcher must run the real xtask with --locked: {run}"
);
}
/// `cargo xtask` must start where the root workspace cannot resolve: the alias runs
/// the detached, dependency-free launcher, never `--package xtask`.
#[test]
fn cargo_xtask_starts_without_the_root_workspace() {
let root = root();
let config = std::fs::read_to_string(root.join(".cargo/config.toml")).unwrap();
assert!(
config.lines().any(|l| l.trim() == "xtask = \"run --quiet --manifest-path xtask/boot/Cargo.toml --\""),
"the xtask alias must run the launcher by manifest path"
);
let boot = manifest(&root.join("xtask/boot"));
assert!(
boot.contains_key("workspace"),
"xtask/boot must be its own (detached) workspace"
);
for kind in ["dependencies", "dev-dependencies", "build-dependencies"] {
assert!(
boot.get(kind).is_none(),
"xtask/boot has [{kind}]: it must build with nothing resolved"
);
}
assert!(
!root.join(".cargo/estate-siblings.sh").exists(),
"the sibling setup is `cargo xtask siblings`, not a script"
);
}
#[test]
fn the_readme_documents_the_lone_clone_build() {
let readme =
std::fs::read_to_string(root().join("docs/v1/README-v1.md")).expect("docs/v1/README-v1.md");
for needle in [
"cargo build --release",
"target/release/holger",
"cargo xtask siblings",
] {
assert!(
readme.contains(needle),
"docs/v1/README-v1.md does not document `{needle}`"
);
}
}
/// The `(name, version)` rows of `xtask/src/lone_clone.rs`'s publish list.
fn publish_list(root: &Path) -> Vec<(String, String)> {
let src = std::fs::read_to_string(root.join("xtask/src/lone_clone.rs")).unwrap();
// The ROOT_NEEDS table only: the unit tests below it carry fixture rows of the same shape.
let start = src.find("pub const ROOT_NEEDS").expect("ROOT_NEEDS");
let table = &src[start..start + src[start..].find("\n];").expect("the table ends")];
// One line or rustfmt's several: the entries are read with every whitespace gone (the
// names, versions and paths carry none), `("name","version","path")` each.
let flat: String = table.chars().filter(|c| !c.is_whitespace()).collect();
flat.split('(')
.skip(1)
.filter(|e| e.starts_with('"'))
.filter_map(|e| {
// `"name","version","path"` with rustfmt's trailing comma or without.
let mut parts = e.split_once(')')?.0.split(',').map(|p| p.trim_matches('"'));
let (name, version) = (parts.next()?, parts.next()?);
(!name.is_empty() && !version.is_empty())
.then(|| (name.to_string(), version.to_string()))
})
.collect()
}
fn version_key(v: &str) -> Vec<u64> {
v.trim_start_matches(['^', '=', '~', ' '])
.split('.')
.map(|p| p.parse().unwrap_or(0))
.collect()
}
/// **The publish list names at least every version a member requires** (A18 §2.2: the
/// list said `korp-installer 0.2.6` while `crates/holger-live` needed `^0.2.7`, and the
/// staged build failed until it was fixed by hand).
#[test]
fn the_publish_list_names_every_version_a_member_requires() {
let root = root();
let list = publish_list(&root);
assert!(list.len() >= 7, "the list parses: {list:?}");
for dir in members(&root) {
let m = manifest(&dir);
for kind in ["dependencies", "dev-dependencies", "build-dependencies"] {
let Some(t) = m.get(kind).and_then(|v| v.as_table()) else {
continue;
};
for (key, spec) in t {
let name = spec.get("package").and_then(|v| v.as_str()).unwrap_or(key);
let want = match spec {
toml::Value::String(v) => v.as_str(),
other => match other.get("version").and_then(|v| v.as_str()) {
Some(v) => v,
None => continue,
},
};
if let Some((_, have)) = list.iter().find(|(n, _)| n == name) {
assert!(
version_key(have) >= version_key(want),
"{} needs {name} {want}, and the publish list names {have}",
dir.display()
);
}
}
}
}
}
// ── H1 (Holger v2.0, docs/HOLGER-V2.md): the estate workspace ────────────────────────
//
// The root workspace is the open core that `git clone` + `cargo build` gives; the estate,
// appliance and build-description crates are members of the separate `estate/` workspace
// (as holger-estate-front is its own). cargo resolves every MEMBER's dependencies,
// optional and dev included, so a crate that is only a root member drags its dependencies
// into the lone clone even when no default build links it (A31 H2).
/// The crates that are no root member (they are `estate/` members).
const ESTATE_CRATES: &[&str] = &[
"holger-fleet",
"holger-live",
"holger-recipe",
"holger-build-thing",
"holger-build-space",
"xtask",
"holger-monetize-guest",
];
/// The estate crates a root member still NAMES (an optional or a dev edge), with the edge.
/// cargo resolves them, so their dependencies are on the publish list. H2 cuts the edges
/// (the `metered` seam into its own crate, `paired` behind the estate crate); until then
/// each one is named here, and no other estate crate may be reached.
const REACHED_BY_AN_EDGE: &[(&str, &str)] = &[
("holger-fleet", "server-lib `metered` (optional)"),
(
"holger-live",
"server/cli `paired` (optional) and its bench dev edge (server-lib's unused one is gone)",
),
(
"holger-recipe",
"server-lib and server/cli dev edges (the estate declaration tests)",
),
];
fn package_name(dir: &Path) -> String {
manifest(dir)["package"]["name"]
.as_str()
.unwrap()
.to_string()
}
#[test]
fn the_root_workspace_is_the_open_core_and_no_estate_crate_is_a_member() {
let root = root();
let names: Vec<String> = members(&root).iter().map(|d| package_name(d)).collect();
let wrong: Vec<&str> = ESTATE_CRATES
.iter()
.copied()
.filter(|c| names.iter().any(|n| n == c))
.collect();
assert!(
wrong.is_empty(),
"estate crates are root members, so a lone clone resolves their dependencies: {wrong:?}"
);
let ws = manifest(&root)["workspace"].as_table().unwrap().clone();
let excluded: Vec<&str> = ws["exclude"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(
excluded.contains(&"estate"),
"the root must exclude `estate/`: {excluded:?}"
);
}
#[test]
fn the_estate_workspace_holds_every_estate_crate() {
let root = root();
let estate = root.join("estate");
let ws = manifest(&estate)["workspace"]
.as_table()
.expect("estate/Cargo.toml has [workspace]")
.clone();
let mut names = BTreeSet::new();
for m in ws["members"].as_array().unwrap() {
let dir = normalise(&estate.join(m.as_str().unwrap()));
let pkg = manifest(&dir);
names.insert(pkg["package"]["name"].as_str().unwrap().to_string());
if !dir.starts_with(&estate) {
// A member outside estate/ names its workspace (cargo's `package.workspace`).
let w = pkg["package"]
.get("workspace")
.and_then(|v| v.as_str())
.unwrap_or_else(|| panic!("{} names no workspace", dir.display()));
assert_eq!(
normalise(&dir.join(w)),
estate,
"{} names another workspace",
dir.display()
);
}
}
for c in ESTATE_CRATES {
assert!(names.contains(*c), "{c} is no member of estate/: {names:?}");
}
}
/// Every estate crate the root workspace reaches by path is one of [`REACHED_BY_AN_EDGE`].
#[test]
fn the_root_reaches_no_estate_crate_beyond_the_named_edges() {
let root = root();
let mut seen = BTreeSet::new();
let mut todo: Vec<PathBuf> = members(&root);
let mut reached = BTreeSet::new();
while let Some(dir) = todo.pop() {
if !seen.insert(dir.clone()) {
continue;
}
let name = package_name(&dir);
if ESTATE_CRATES.contains(&name.as_str()) {
reached.insert(name);
}
for (_, p) in path_deps(&manifest(&dir)) {
let target = normalise(&dir.join(&p));
if target.starts_with(&root) {
todo.push(target);
}
}
}
let allowed: BTreeSet<String> = REACHED_BY_AN_EDGE
.iter()
.map(|(n, _)| n.to_string())
.collect();
let beyond: Vec<&String> = reached.difference(&allowed).collect();
assert!(
beyond.is_empty(),
"the root workspace reaches estate crates by path beyond the named edges: {beyond:?}"
);
}
/// **The publish list is exactly the sibling crates the root resolution takes from
/// crates.io**, name and version (B7: the root takes no sibling patch, so every sibling
/// crate it resolves is a registry crate). A sibling crate is one the estate's patch table
/// names (`.cargo/estate-siblings.toml`). Read from `cargo metadata --locked
/// --all-features` of the root, which a lone clone and an estate checkout resolve alike.
#[test]
fn the_publish_list_is_exactly_the_sibling_crates_of_the_root_resolution() {
let root = root();
let template: toml::Table = std::fs::read_to_string(root.join(".cargo/estate-siblings.toml"))
.unwrap()
.parse()
.unwrap();
let siblings: BTreeSet<String> = template["patch"]["crates-io"]
.as_table()
.unwrap()
.keys()
.cloned()
.collect();
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let o = std::process::Command::new(cargo)
.current_dir(&root)
.args([
"metadata",
"--locked",
"--all-features",
"--format-version",
"1",
])
.output()
.expect("cargo metadata");
assert!(
o.status.success(),
"cargo metadata --locked at the root: {}",
String::from_utf8_lossy(&o.stderr)
);
let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
let resolved: BTreeSet<(String, String)> = v["packages"]
.as_array()
.unwrap()
.iter()
.filter(|p| {
p["source"]
.as_str()
.is_some_and(|s| s.starts_with("registry+"))
&& siblings.contains(p["name"].as_str().unwrap())
})
.map(|p| {
(
p["name"].as_str().unwrap().to_string(),
p["version"].as_str().unwrap().to_string(),
)
})
.collect();
let path_sibling: Vec<&str> = v["packages"]
.as_array()
.unwrap()
.iter()
.filter(|p| p["source"].is_null() && siblings.contains(p["name"].as_str().unwrap()))
.map(|p| p["name"].as_str().unwrap())
.collect();
assert!(
path_sibling.is_empty(),
"the root resolves sibling crates by path: {path_sibling:?}"
);
let listed: BTreeSet<(String, String)> = publish_list(&root).into_iter().collect();
let missing: Vec<_> = resolved.difference(&listed).collect();
let extra: Vec<_> = listed.difference(&resolved).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"xtask/src/lone_clone.rs's publish list is not the root resolution's sibling set.\n missing: {missing:?}\n not resolved: {extra:?}"
);
}
/// **The root resolution takes ONE monetize line** (A3, 2026-10-26): each `monetize-*` crate
/// once, so no crate of the root build verifies a monetize signature with an older copy of
/// monetize's code (edda's appliance-grow took the published monetize-embed 0.1.3 beside the
/// 0.3.0 line). Read from `cargo metadata --locked --all-features` of the root.
#[test]
fn the_root_resolution_takes_one_monetize_line() {
let root = root();
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let o = std::process::Command::new(cargo)
.current_dir(&root)
.args([
"metadata",
"--locked",
"--all-features",
"--format-version",
"1",
])
.output()
.expect("cargo metadata");
assert!(
o.status.success(),
"cargo metadata --locked at the root: {}",
String::from_utf8_lossy(&o.stderr)
);
let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
let mut seen: std::collections::BTreeMap<String, Vec<String>> = Default::default();
for p in v["packages"].as_array().unwrap() {
let name = p["name"].as_str().unwrap();
if name.starts_with("monetize-") {
seen.entry(name.to_string())
.or_default()
.push(p["version"].as_str().unwrap().to_string());
}
}
assert!(
seen.contains_key("monetize-embed"),
"the root resolution has no monetize-embed at all: {seen:?}"
);
let twice: Vec<_> = seen.iter().filter(|(_, v)| v.len() > 1).collect();
assert!(
twice.is_empty(),
"the root resolution takes more than one monetize line: {twice:?}"
);
}