tests/every_call_is_a_cli_command.rs
//! **Every gRPC call this console makes is a `holger-cli` command** (holger lane B3, owner:
//! "every UI action is a CLI command").
//!
//! The console reaches holger through one accessor per service in `src/grpc.rs`
//! (`grpc::admin()`, `grpc::tokens()`, …). This test finds every `accessor().method(` call
//! in `src/` and asks holger's thin client, `holger-cli/src/table.rs`, for the command that
//! makes the same RPC. A call the console gains without a command fails HERE, in the
//! change that adds it, so the CLI never falls behind the console.
//!
//! The table is read as holger COMMITS it (the sibling checkout's `origin/main`, else its
//! `HEAD`), as `tests/proto_is_holgers.rs` reads the proto: a working tree is not a
//! contract. Without a holger checkout beside this repository the test says so and
//! skips. holger's own `holger-cli/tests/every_ui_call_is_a_command.rs` is the same check
//! from the other side.
//!
//! The console's other doors are not gRPC and are not the client's: the passkey ceremony,
//! invites and step-up (a browser's authenticator, `/-/auth/passkey/*`), the certificates
//! pane (the estate front's page) and the purchases (the shop's).
use std::path::{Path, PathBuf};
use std::process::Command;
/// The accessors in `src/grpc.rs` and the service each one dials.
const ACCESSORS: &[(&str, &str)] = &[
("admin", "AdminService"),
("repositories", "RepositoryService"),
("archives", "ArchiveService"),
("search", "SearchService"),
("sboms", "SbomService"),
("properties", "PropertyService"),
("promotion", "PromotionService"),
("audit", "AuditService"),
("migration", "MigrationService"),
("replication", "ReplicationService"),
("jobs", "JobService"),
("admission", "AdmissionService"),
("usage", "UsageService"),
("room", "RoomService"),
("account", "AccountService"),
("tokens", "TokenService"),
];
fn ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
/// Pure: every `accessor().method(` in `text` as the RPC it calls (`AdminService.WhoAmI`).
fn rpcs_in(text: &str) -> Vec<String> {
let mut out = Vec::new();
for (acc, service) in ACCESSORS {
let needle = format!("{acc}()");
let mut from = 0;
while let Some(at) = text[from..].find(&needle) {
let start = from + at;
from = start + needle.len();
if text[..start].chars().next_back().is_some_and(ident_char) {
continue;
}
let Some(rest) = text[from..].trim_start().strip_prefix('.') else { continue };
let rest = rest.trim_start();
let method: String = rest.chars().take_while(|c| ident_char(*c)).collect();
if method.is_empty() || !rest[method.len()..].trim_start().starts_with('(') {
continue;
}
let camel: String = method
.split('_')
.filter(|p| !p.is_empty())
.map(|p| {
let mut c = p.chars();
c.next().map(|f| f.to_ascii_uppercase().to_string() + c.as_str()).unwrap_or_default()
})
.collect();
let camel = if camel == "WhoAmI" || camel == "WhoAmi" { "WhoAmI".to_owned() } else { camel };
out.push(format!("{service}.{camel}"));
}
}
out.sort();
out.dedup();
out
}
/// Pure: the RPCs holger-cli's table has a command for (its `("Service.Method", "words")` rows),
/// and the console's calls its `SAME_ACT` rows make by another RPC that has a row (holger's
/// `command_for` reads them the same way).
fn table_rpcs(table: &str) -> Vec<String> {
// rustfmt wraps a long row over lines (`(\n "A.B",\n "words",\n),`): read the table
// as one line, its comments and its tests left out.
let code = table.split("#[cfg(test)]").next().unwrap_or("");
let flat = code.lines().map(str::trim).filter(|l| !l.starts_with("//")).collect::<Vec<_>>().join(" ").replace("( \"", "(\"").replace("\", )", "\")");
let (rows, same_act) = match flat.split_once("const SAME_ACT") {
Some((before, rest)) => {
let (body, after) = rest.split_once("];").unwrap_or((rest, ""));
(format!("{before} {after}"), body.to_owned())
}
None => (flat, String::new()),
};
let pairs = |text: &str| -> Vec<(String, String)> {
text.split("(\"")
.skip(1)
.filter_map(|p| p.split_once("\", \""))
.filter_map(|(first, rest)| rest.split_once('"').map(|(second, _)| (first.to_owned(), second.to_owned())))
.collect()
};
let mut have: Vec<String> = pairs(&rows).into_iter().map(|(rpc, _)| rpc).filter(|rpc| rpc.contains('.')).collect();
for (console, ours) in pairs(&same_act) {
if have.contains(&ours) {
have.push(console);
}
}
have
}
fn rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
for e in std::fs::read_dir(dir).into_iter().flatten().flatten() {
let p = e.path();
if p.is_dir() {
rs_files(&p, out);
} else if p.extension().is_some_and(|x| x == "rs") {
out.push(p);
}
}
}
fn holgers_table(repo: &Path) -> Option<(String, &'static str)> {
// The 3.2 line (h32/next) reads holger's 3.2 line first: its console calls 3.2 doors.
for rev in ["origin/h32/next", "origin/main", "HEAD"] {
let out = Command::new("git").arg("-C").arg(repo).args(["show", &format!("{rev}:holger-cli/src/table.rs")]).output().ok()?;
if out.status.success() {
return String::from_utf8(out.stdout).ok().map(|t| (t, rev));
}
}
None
}
#[test]
fn the_scan_reads_a_call_across_lines() {
assert_eq!(rpcs_in("grpc::admin()\n .admin_snapshot(r)\ntokens().mint(x)\nxadmin().health()"), vec!["AdminService.AdminSnapshot", "TokenService.Mint"]);
assert_eq!(table_rpcs(" (\"AdminService.Health\", \"health\"),\n // x\n (\"TokenService.Mint\", \"token mint\"),"), vec!["AdminService.Health", "TokenService.Mint"]);
}
/// POSTPUB-9: holger-cli's `SAME_ACT` rows name a console call it makes by another RPC of the
/// same act (the console's unary `PutArtifact` is `artifacts put`'s `PutArtifactStream`), as
/// holger's own `command_for` reads them; the console call is covered when the other RPC has a row.
#[test]
fn a_same_act_row_covers_the_consoles_call() {
let table = " (\"RepositoryService.PutArtifactStream\", \"artifacts put\"),\n\
pub const SAME_ACT: &[(&str, &str)] = &[(\"RepositoryService.PutArtifact\", \"RepositoryService.PutArtifactStream\"), (\"AdminService.Gone\", \"AdminService.NoRow\")];";
let have = table_rpcs(table);
assert!(have.iter().any(|r| r == "RepositoryService.PutArtifact"), "{have:?}");
assert!(!have.iter().any(|r| r == "AdminService.Gone"), "a same-act row whose RPC has no row covers nothing: {have:?}");
}
/// ★ **A row rustfmt wrapped over lines is still a row** (Henrik, 2026-10-31): holger's
/// `cargo xtask fmt --write` (a3efbf98) wrapped `SAME_ACT`, and a long `COMMANDS` row wraps the
/// same way; the scan reads them as rustfmt writes them, and never a call in the table's tests.
#[test]
fn a_row_rustfmt_wrapped_over_lines_is_read() {
let table = "pub const COMMANDS: &[(&str, &str)] = &[\n (\"AdminService.Health\", \"health\"),\n (\n \"PromotionService.ListIntakeArtifacts\",\n \"intake list --a-long-flag\",\n ),\n (\"RepositoryService.PutArtifactStream\", \"artifacts put\"),\n];\n\n\
/// `(\"AdminService.InComment\", \"x\")`\n\
pub const SAME_ACT: &[(&str, &str)] = &[(\n \"RepositoryService.PutArtifact\",\n \"RepositoryService.PutArtifactStream\",\n)];\n\n\
#[cfg(test)]\nmod tests {\n fn t() {\n assert_eq!(\n x(\"AdminService.InTest\", \"y\"),\n None\n );\n }\n}\n";
let have = table_rpcs(table);
for rpc in ["AdminService.Health", "PromotionService.ListIntakeArtifacts", "RepositoryService.PutArtifactStream", "RepositoryService.PutArtifact"] {
assert!(have.iter().any(|r| r == rpc), "{rpc}: {have:?}");
}
assert!(!have.iter().any(|r| r == "AdminService.InComment" || r == "AdminService.InTest"), "a comment or a test is no row: {have:?}");
}
#[test]
fn every_call_this_console_makes_is_a_holger_cli_command() {
let mut files = Vec::new();
rs_files(&Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), &mut files);
let mut calls: Vec<String> = files.iter().flat_map(|f| rpcs_in(&std::fs::read_to_string(f).unwrap())).collect();
calls.sort();
calls.dedup();
assert!(calls.len() >= 39, "the scan found {} calls: it went blind", calls.len());
let holger = std::env::var_os("HOLGER_CHECKOUT").map(PathBuf::from).unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../holger"));
let Some((table, rev)) = holgers_table(&holger) else {
sibling_missing(&format!("no holger checkout with holger-cli at {} (origin/main or HEAD); set HOLGER_CHECKOUT", holger.display()));
return;
};
let have = table_rpcs(&table);
let missing: Vec<&String> = calls.iter().filter(|c| !have.contains(c)).collect();
assert!(missing.is_empty(), "holger-cli ({rev}:holger-cli/src/table.rs) has no command for:\n{missing:#?}");
}
/// ★ **RA1-8: a missing sibling is RED in CI** — `every_call_this_console_makes_is_a_holger_cli_command` reads the sibling checkout, and
/// says SKIP by name only outside CI. Run here as a child of this test binary with the sibling
/// pointed at a directory that is not there: with the runner's container marker
/// (`NORNIR_CI_IN_CONTAINER=1`) it must fail; with no CI marker it passes and says SKIP.
#[test]
fn a_missing_sibling_is_red_in_ci_and_a_named_skip_elsewhere() {
let gone = std::env::temp_dir().join(format!("ra1-8-no-sibling-{}", std::process::id()));
let child = |ci: bool| {
let mut c = std::process::Command::new(std::env::current_exe().unwrap());
c.args(["--exact", "every_call_this_console_makes_is_a_holger_cli_command", "--nocapture", "--test-threads", "1"]).env("HOLGER_CHECKOUT", &gone);
for k in ["NORNIR_CI_IN_CONTAINER", "NORNIR_CI_HOST", "SIBLINGS_REQUIRED", "CI"] {
c.env_remove(k);
}
if ci {
c.env("NORNIR_CI_IN_CONTAINER", "1");
}
let out = c.output().unwrap();
(out.status.success(), format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)))
};
let (ok, text) = child(false);
assert!(ok && text.contains("SKIP") && text.contains("1 passed"), "outside CI a missing sibling is a named skip: {text}");
let (ok, text) = child(true);
assert!(!ok && text.contains("1 failed"), "in CI a missing sibling is red: {text}");
}
/// **RA1-8: is a missing sibling checkout RED here?** In CI (the runner's container marker
/// `NORNIR_CI_IN_CONTAINER=1`, `NORNIR_CI_HOST=1`, `CI=true`) or when asked
/// (`SIBLINGS_REQUIRED=1`) it is; elsewhere a cross-repo test says SKIP by name.
fn siblings_required(env: &dyn Fn(&str) -> Option<String>) -> bool {
let on = |k: &str| env(k).is_some_and(|v| matches!(v.trim(), "1" | "true"));
on("NORNIR_CI_IN_CONTAINER") || on("NORNIR_CI_HOST") || on("CI") || on("SIBLINGS_REQUIRED")
}
/// A missing sibling: a named SKIP outside CI, a failure in it ([`siblings_required`]).
fn sibling_missing(why: &str) {
assert!(!siblings_required(&|k| std::env::var(k).ok()), "{why}: in CI a missing sibling is red (NORNIR_CI_IN_CONTAINER, NORNIR_CI_HOST, CI or SIBLINGS_REQUIRED is set)");
eprintln!("SKIP: {why}");
}
#[test]
fn siblings_are_required_in_ci_or_when_asked() {
let env = |set: &'static [(&'static str, &'static str)]| move |k: &str| set.iter().find(|(n, _)| *n == k).map(|(_, v)| v.to_string());
assert!(!siblings_required(&env(&[])));
assert!(!siblings_required(&env(&[("CI", "0"), ("NORNIR_CI_IN_CONTAINER", "")])));
for k in ["NORNIR_CI_IN_CONTAINER", "NORNIR_CI_HOST", "CI", "SIBLINGS_REQUIRED"] {
let one: &'static [(&'static str, &'static str)] = Box::leak(vec![(k, "1")].into_boxed_slice());
assert!(siblings_required(&env(one)), "{k}=1");
}
assert!(siblings_required(&env(&[("CI", "true")])));
}