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