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

nordisk/holger-ui

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")])));
}