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

nordisk/gunnar

crates/gunnar-grpc/tests/decision_words.rs

//! **F (release-3 test2 mock 2026-09-29): the audit target speaks THREE decision words.**
//!
//! `gunnar-grpc` auth writes `decision = "allow" | "deny" | "narrow"` on
//! `event = "grpc.call"`, and that field is what a reader filters on — the
//! console's audit pane and the ops log gate (private-gunnar-ops
//! `log_gate::DECISIONS`). `service/mirror.rs` wrote `decision = "allowed"` for
//! the twin's `Mirror.LogHead`/`LogFetch` reads (176 + 54 lines per main on
//! the mock): the console rendered every ALLOWED read of the twin as "DENIED
//! Mirror.LogHead gunnar-twin", and the gate failed the receipt on it.
//!
//! The guard reads every source file of the crates that log `grpc.call`, so a
//! fourth word cannot come back unnoticed.

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

const WORDS: &[&str] = &["allow", "deny", "narrow"];

fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
    for e in std::fs::read_dir(dir)
        .unwrap_or_else(|e| panic!("{}: {e}", dir.display()))
        .flatten()
    {
        let p = e.path();
        if p.is_dir() {
            rust_files(&p, out);
        } else if p.extension().is_some_and(|x| x == "rs") {
            out.push(p);
        }
    }
}

#[test]
fn every_decision_word_on_the_audit_target_is_allow_deny_or_narrow() {
    let crates = Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("crates/");
    let mut files = Vec::new();
    for c in ["gunnar-grpc", "gunnar-server", "gunnar-ssh", "gunnar-front"] {
        rust_files(&crates.join(c).join("src"), &mut files);
    }
    assert!(
        files.len() > 50,
        "the crates were read: {} files",
        files.len()
    );
    let mut bad = Vec::new();
    let mut seen = 0;
    for f in &files {
        let text = std::fs::read_to_string(f).unwrap();
        for (n, line) in text.lines().enumerate() {
            let mut rest = line;
            while let Some(i) = rest.find("decision = \"") {
                let after = &rest[i + "decision = \"".len()..];
                let word = after.split('"').next().unwrap_or("");
                seen += 1;
                if !WORDS.contains(&word) {
                    bad.push(format!("{}:{}: decision = \"{word}\"", f.display(), n + 1));
                }
                rest = after;
            }
        }
    }
    assert!(seen >= 20, "the decision fields were found: {seen}");
    assert!(
        bad.is_empty(),
        "a decision word the audit target does not speak ({}):\n{}",
        WORDS.join("|"),
        bad.join("\n")
    );
}