ref:main
//! Structural enforcement of the `--json` output contract.
//!
//! The contract (see `src/output.rs`) is correct-by-construction: every leaf
//! command returns an `output::Response`, `commands::run` is the single point
//! that writes it to stdout under `--json`, and the human helpers self-suppress
//! — so a leaf can neither forget to emit JSON (the compiler requires a
//! `Response`) nor leak human text (raw `println!` is a clippy error, see
//! `clippy.toml`). This test is the regression tripwire on top of that: it walks
//! the real clap tree and asserts the full set of leaf commands, so adding a new
//! subcommand fails here until its author has consciously wired it through the
//! same contract (and, ideally, added a `--json` test for its shape below).
use clap::CommandFactory;
/// Every leaf (executable) command path in the CLI, as space-joined names,
/// e.g. `"requirement status"`. A command is a leaf when it has no subcommands.
fn leaf_paths() -> Vec<String> {
let cmd = anvil::commands::Cli::command();
let mut leaves = Vec::new();
collect(&cmd, String::new(), &mut leaves);
leaves.sort();
leaves
}
fn collect(cmd: &clap::Command, prefix: String, out: &mut Vec<String>) {
let subs: Vec<&clap::Command> = cmd.get_subcommands().collect();
if subs.is_empty() {
if !prefix.is_empty() {
out.push(prefix);
}
return;
}
for sub in subs {
let name = sub.get_name();
let path = if prefix.is_empty() {
name.to_string()
} else {
format!("{prefix} {name}")
};
collect(sub, path, out);
}
}
/// The complete set of leaf commands, as of the JSON-contract migration. Every
/// one of these returns an `output::Response` that `commands::run` emits under
/// `--json`. When you add, remove, or rename a subcommand this test fails: add
/// the leaf here, and make sure its `run` returns a `Response` with the right
/// envelope (read → `Response::read`/`items`, mutation → `Response::ok`, delete
/// → `Response::deleted`, gate → `Response::gate`) — never a bare `Ok(())`.
const COVERED_LEAVES: &[&str] = &[
"agent approve",
"agent list",
"agent reject",
"agent session",
"agent sessions",
"agent trigger",
"agent view",
"auth git-credential",
"auth login",
"auth logout",
"auth rotate",
"auth status",
"board create-column",
"board delete-column",
"board edit-column",
"board init",
"board list",
"branch list",
"ci cancel",
"ci delete-secret",
"ci job-view",
"ci list",
"ci run",
"ci secrets",
"ci set-secret",
"ci view",
"commit list",
"commit view",
"deploy create",
"deploy env create",
"deploy env list",
"deploy list",
"deploy status",
"epic add-child",
"epic auto-close",
"epic children",
"epic list",
"epic mark",
"epic remove-child",
"epic view",
"issue assign",
"issue close",
"issue comment",
"issue comments",
"issue create",
"issue create-milestone",
"issue edit",
"issue link",
"issue link-req",
"issue links",
"issue list",
"issue milestones",
"issue move",
"issue reopen",
"issue unassign",
"issue unlink",
"issue unlink-req",
"issue view",
"label add",
"label create",
"label delete",
"label edit",
"label list",
"label remove",
"milestone close",
"milestone create",
"milestone delete",
"milestone edit",
"milestone list",
"milestone reopen",
"milestone view",
"pr checkout",
"pr close",
"pr comment",
"pr create",
"pr diff",
"pr edit",
"pr list",
"pr merge",
"pr reopen",
"pr review",
"pr reviews",
"pr view",
"registry token create",
"registry token delete",
"registry token list",
"release assets",
"release changelog",
"release create",
"release delete",
"release delete-asset",
"release download",
"release list",
"release publish",
"release update",
"release upload",
"release view",
"repo clone",
"repo create",
"repo list",
"repo set-default",
"repo view",
"requirement applicability add",
"requirement applicability list",
"requirement applicability remove",
"requirement create",
"requirement delete",
"requirement export",
"requirement import",
"requirement link",
"requirement list",
"requirement matrix",
"requirement seed",
"requirement status",
"requirement unlink",
"requirement update",
"requirement view",
"runner configure",
"runner doctor",
"runner list",
"runner logs",
"runner remove",
"runner restart",
"runner service install",
"runner service list",
"runner service restart",
"runner service start",
"runner service status",
"runner service stop",
"runner service uninstall",
"runner start",
"runner status",
"runner stop",
"runner token",
"runner unconfigure",
"runner update",
"runner view",
"ssh-key add",
"ssh-key list",
"ssh-key remove",
"update",
];
#[test]
fn every_leaf_is_covered() {
let actual = leaf_paths();
let expected: Vec<String> = COVERED_LEAVES.iter().map(|s| s.to_string()).collect();
let missing: Vec<&String> = actual.iter().filter(|p| !expected.contains(p)).collect();
let stale: Vec<&String> = expected.iter().filter(|p| !actual.contains(p)).collect();
assert!(
missing.is_empty() && stale.is_empty(),
"leaf-command inventory drifted from the JSON contract.\n\
NEW leaves not yet covered (make run() return an output::Response, then add here): {missing:?}\n\
STALE entries no longer in the CLI (remove from COVERED_LEAVES): {stale:?}",
);
}