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

nordisk/holger-ui

tests/contract.rs

//! Contract guard: every service and RPC in the vendored proto must be a
//! CONSCIOUS decision in the capability matrix. A new method landing in
//! `proto/holger.proto` without a matrix row fails this test — silence is not
//! a coverage decision. (Pure file parsing; runs natively, no wasm.)

fn proto() -> &'static str {
    include_str!("../proto/holger.proto")
}

fn matrix() -> &'static str {
    include_str!("../docs/ui-backend-capability-coverage.md")
}

/// Every table ROW of the capability matrix. A decision is a row, not a
/// sentence: prose mentions the service names constantly, so matching the whole
/// document would let a new RPC ride in on a paragraph that predates it.
fn matrix_rows() -> Vec<&'static str> {
    matrix()
        .lines()
        .map(str::trim)
        .filter(|l| l.starts_with('|'))
        .collect()
}

/// The console's own source — every module that may issue a call. Since
/// 2026-09-27 the console's WRITES go through its own server (`/api/…`, the
/// session cookie and the role ladder), so the server's handlers that make
/// those calls on the console's behalf are part of the surface too.
const SOURCES: [&str; 17] = [
    include_str!("../src/admin_queue.rs"),
    include_str!("../src/app.rs"),
    include_str!("../src/grpc.rs"),
    include_str!("../src/pages/admin.rs"),
    include_str!("../src/pages/admin_queue.rs"),
    include_str!("../src/pages/artifact_detail.rs"),
    include_str!("../src/pages/dashboard.rs"),
    include_str!("../src/pages/login.rs"),
    include_str!("../src/pages/migrate.rs"),
    include_str!("../src/pages/replication.rs"),
    include_str!("../src/pages/repositories.rs"),
    include_str!("../src/pages/repository_detail.rs"),
    include_str!("../src/pages/search.rs"),
    include_str!("../src/pages/security.rs"),
    include_str!("../src/security.rs"),
    include_str!("../src/session.rs"),
    include_str!("../src/components/feedback.rs"),
];

/// `WhoAmI` → `who_am_i` — how tonic's codegen names the client method, which
/// is the only name the console ever writes.
fn client_method(rpc: &str) -> String {
    let mut out = String::with_capacity(rpc.len() + 4);
    for (i, c) in rpc.char_indices() {
        if c.is_ascii_uppercase() {
            if i > 0 {
                out.push('_');
            }
            out.push(c.to_ascii_lowercase());
        } else {
            out.push(c);
        }
    }
    out
}

/// Every `service X` and `rpc Y` name in the proto, in order of appearance.
fn proto_surface() -> (Vec<String>, Vec<String>) {
    let mut services = Vec::new();
    let mut rpcs = Vec::new();
    for line in proto().lines() {
        let t = line.trim();
        if let Some(rest) = t.strip_prefix("service ") {
            if let Some(name) = rest.split_whitespace().next() {
                services.push(name.trim_end_matches('{').to_string());
            }
        }
        if let Some(rest) = t.strip_prefix("rpc ") {
            if let Some(name) = rest.split('(').next() {
                rpcs.push(name.trim().to_string());
            }
        }
    }
    (services, rpcs)
}

#[test]
fn the_wire_surface_is_the_expected_thirteen_services() {
    let (services, rpcs) = proto_surface();
    assert_eq!(
        services,
        [
            "RepositoryService",
            "ArchiveService",
            "AdminService",
            "PromotionService",
            // The per-artifact audit trail (lane S8): the security view's trail.
            "AuditService",
            "SearchService",
            "SbomService",
            "PropertyService",
            "ReplicationService",
            // Registered only in an `online` build — a server without it answers
            // Unimplemented, which the console renders as "not supported by this
            // server build". Driven by Operations → Migrate.
            "MigrationService",
            "UsageService",
            // Registered only in a `jobs` build. No client in the wasm — the
            // console's server relays ListJobs to the admin view's Queue pane
            // (`/api/admin/jobs`); the matrix row says "Ingen klient".
            "JobService",
            // holger admit-queue: the door's admission (usage book + plan),
            // relayed to the admin view's Admission pane
            // (`/api/admin/admission`); "Ingen klient" in the wasm too.
            "AdmissionService",
        ],
        "the runtime registers exactly these — a change here must be a \
         conscious decision reflected in grpc.rs and the capability matrix"
    );
    assert!(rpcs.len() >= 17, "expected ≥17 RPCs, found {}", rpcs.len());
}

#[test]
fn every_rpc_has_a_row_in_the_capability_matrix() {
    let (services, rpcs) = proto_surface();
    let rows = matrix_rows();
    let mut missing: Vec<&String> = Vec::new();
    for name in services.iter().chain(rpcs.iter()) {
        if !rows.iter().any(|row| row.contains(name.as_str())) {
            missing.push(name);
        }
    }
    assert!(
        missing.is_empty(),
        "proto names with NO decision row in docs/ui-backend-capability-coverage.md: \
         {missing:?} — add a row (covered, CLI-only, or unsuitable) before shipping"
    );
}

#[test]
fn no_two_rpcs_share_a_name() {
    // The matrix is keyed by RPC name. Two services with a same-named method
    // would let ONE row silently stand for both — and a client method call
    // would read as covering whichever the reader assumed.
    let (_, mut rpcs) = proto_surface();
    let before = rpcs.len();
    rpcs.sort();
    rpcs.dedup();
    assert_eq!(
        before,
        rpcs.len(),
        "duplicate RPC names in the proto — the capability matrix cannot key on them"
    );
}

#[test]
fn an_rpc_no_view_calls_is_never_recorded_as_fully_covered() {
    // The compiler proves the console only calls RPCs that EXIST; nothing
    // proves the matrix's coverage claim. So: an RPC whose generated client
    // method appears nowhere in the console's source may be a deliberate gap
    // (StreamArtifact — the browser buffers the blob either way), but its row
    // must then say so rather than claim "Fullt täckt".
    let (_, rpcs) = proto_surface();
    let rows = matrix_rows();
    for rpc in &rpcs {
        let method = format!("{}(", client_method(rpc));
        if SOURCES.iter().any(|src| src.contains(&method)) {
            continue;
        }
        let claiming: Vec<&&str> = rows
            .iter()
            .filter(|row| row.contains(rpc.as_str()) && row.contains("Fullt täckt"))
            .collect();
        assert!(
            claiming.is_empty(),
            "{rpc} is called by no view, yet the matrix calls it fully covered: {claiming:?} \
             — build the surface or downgrade the row"
        );
    }
}

#[test]
fn every_ui_client_matches_a_proto_service() {
    // The generated-client names used by src/grpc.rs must each correspond to a
    // proto service — and there must be one client per service (the gap this
    // guards was real: ArchiveService existed for weeks without a client).
    //
    // A service the console deliberately does NOT speak is allowed, but only
    // out loud: its matrix row has to say "Ingen klient". Silence still fails,
    // which is the whole point of the guard.
    let grpc_rs = include_str!("../src/grpc.rs");
    let (services, _) = proto_surface();
    for s in &services {
        let client = format!("{s}Client");
        if grpc_rs.contains(&client) {
            continue;
        }
        let declared_gap = matrix_rows()
            .iter()
            .any(|row| row.contains(s.as_str()) && row.contains("Ingen klient"));
        assert!(
            declared_gap,
            "proto service {s} has no {client} in src/grpc.rs — either build \
             the client or record the gap in the capability matrix with the \
             words \"Ingen klient\""
        );
    }
}