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

nordisk/holger

holger-cli/tests/every_ui_call_is_a_command.rs

//! **Every call the console makes is a command here** (lane B3, owner: "every UI action
//! is a CLI command").
//!
//! The console (holger-ui, `src/`) reaches holger through one accessor per service
//! (`grpc::admin()`, `grpc::tokens()`, …) and calls the RPC on it. This test reads those
//! calls two ways and fails when one has no `holger-cli` command:
//!
//! * [`CONSOLE_CALLS`]: the console's calls as they stood when this client was written
//!   (holger-ui ca3d57f, 39 calls), so a lone clone of holger checks them too;
//! * the sibling checkout `../holger-ui` (or `$HOLGER_UI_DIR`) when there is one: its
//!   source TODAY, so a call the console gains fails here until the client has it.
//!
//! The console's other doors are not gRPC and are not the client's: the passkey
//! ceremony, invites and step-up need a browser's authenticator (`/-/auth/passkey/*`),
//! the certificates pane is the estate front's page, and the purchases are the shop's.

use std::path::{Path, PathBuf};

use holger_cli::table::{command_for, rpc_of};

/// The console's gRPC calls at holger-ui ca3d57f (2026-10-26), `(accessor, method)`.
const CONSOLE_CALLS: &[(&str, &str)] = &[
    ("account", "create_repository"),
    ("account", "grant"),
    ("account", "my_repositories"),
    ("admin", "admin_snapshot"),
    ("admin", "health"),
    ("admin", "list_repositories"),
    ("admin", "server_profile"),
    ("admin", "topology"),
    ("admin", "who_am_i"),
    ("admission", "admit_plan"),
    ("admission", "list_usage"),
    ("archives", "archive_info"),
    ("archives", "list_archive_files"),
    ("audit", "get_artifact_trail"),
    ("jobs", "list_jobs"),
    ("migration", "cancel"),
    ("migration", "discover"),
    ("migration", "start"),
    ("migration", "watch"),
    ("promotion", "advance_artifact"),
    ("promotion", "get_artifact_evidence"),
    ("promotion", "get_artifact_state"),
    ("promotion", "list_intake_artifacts"),
    ("promotion", "promote_artifact"),
    ("properties", "get_properties"),
    ("properties", "set_property"),
    ("replication", "replicate"),
    ("repositories", "fetch_artifact"),
    ("repositories", "list_artifacts"),
    ("repositories", "put_artifact"),
    ("room", "private_estate"),
    ("room", "room"),
    ("sboms", "attach_sbom"),
    ("sboms", "fetch_sbom"),
    ("search", "search"),
    ("tokens", "list"),
    ("tokens", "mint"),
    ("tokens", "revoke"),
    ("usage", "read_usage"),
];

/// The console's service accessors (`src/grpc.rs`).
const ACCESSORS: &[&str] = &[
    "admin",
    "repositories",
    "search",
    "sboms",
    "properties",
    "promotion",
    "audit",
    "migration",
    "replication",
    "archives",
    "jobs",
    "admission",
    "room",
    "account",
    "tokens",
    "usage",
];

fn ident_char(c: char) -> bool {
    c.is_ascii_alphanumeric() || c == '_'
}

/// Pure: every `accessor().method(` call in `text`, whitespace and newlines between the
/// parts allowed (rustfmt breaks long chains), comments included as text is.
pub fn calls_in(text: &str) -> Vec<(String, String)> {
    let mut out = Vec::new();
    for acc 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();
            // A whole word: `grpc::admin()`, ` admin()`, never `xadmin()`.
            if text[..start].chars().next_back().is_some_and(ident_char) {
                continue;
            }
            let rest = text[from..].trim_start();
            let Some(rest) = rest.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;
            }
            out.push((acc.to_string(), method));
        }
    }
    out.sort();
    out.dedup();
    out
}

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 missing(calls: &[(String, String)]) -> Vec<String> {
    calls
        .iter()
        .filter_map(|(a, m)| match rpc_of(a, m) {
            Some(rpc) if command_for(&rpc).is_some() => None,
            Some(rpc) => Some(format!("{a}().{m}() = {rpc}")),
            None => Some(format!("{a}().{m}(): no service for accessor {a}")),
        })
        .collect()
}

#[test]
fn the_scanner_finds_a_call_across_lines_and_only_whole_accessors() {
    let src = "let r = grpc::admin()\n        .admin_snapshot(req).await;\nlet x = tokens().mint(m);\nxadmin().health();\nadmin().health;\n";
    assert_eq!(
        calls_in(src),
        vec![
            ("admin".to_string(), "admin_snapshot".to_string()),
            ("tokens".to_string(), "mint".to_string())
        ]
    );
}

#[test]
fn every_call_the_console_made_is_a_command() {
    let calls: Vec<(String, String)> = CONSOLE_CALLS
        .iter()
        .map(|(a, m)| (a.to_string(), m.to_string()))
        .collect();
    assert_eq!(calls.len(), 39);
    let gaps = missing(&calls);
    assert!(
        gaps.is_empty(),
        "console calls with no holger-cli command:\n{}",
        gaps.join("\n")
    );
}

#[test]
fn every_call_the_console_makes_today_is_a_command() {
    let dir = std::env::var_os("HOLGER_UI_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../holger-ui"));
    let src = dir.join("src");
    if !src.join("grpc.rs").is_file() {
        sibling_missing(&format!(
            "no holger-ui checkout at {} (set HOLGER_UI_DIR); the pinned list was checked",
            dir.display()
        ));
        return;
    }
    let mut files = Vec::new();
    rs_files(&src, &mut files);
    let mut calls = Vec::new();
    for f in &files {
        calls.extend(calls_in(&std::fs::read_to_string(f).unwrap()));
    }
    calls.sort();
    calls.dedup();
    assert!(
        calls.len() >= CONSOLE_CALLS.len(),
        "the scan found {} calls in {}: it went blind",
        calls.len(),
        src.display()
    );
    let gaps = missing(&calls);
    assert!(
        gaps.is_empty(),
        "the console at {} calls these with no holger-cli command:\n{}",
        dir.display(),
        gaps.join("\n")
    );
}

/// ★ **RA1-8: a missing sibling is RED in CI** — `every_call_the_console_makes_today_is_a_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_the_console_makes_today_is_a_command",
            "--nocapture",
            "--test-threads",
            "1",
        ])
        .env("HOLGER_UI_DIR", &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")])));
}