openmoat 0.13.0

Policy checks for AI coding agents: decides allow, defer, ask or deny for every tool call from Claude Code, Codex, Cursor, OpenClaw, GitHub Copilot CLI, pi and Gemini CLI, configures Claude Code's, Codex's and Cursor's own OS sandboxes from the same policy, and keeps a local audit log.
use openmoat_hosts::{Advisory, Host, Platform};

use super::{advisory_line, cached, identify, parse, parse_any, remember};

/// A version kept for a program file is reused while the file stays the
/// same, and asked again once it is replaced (another size or time).
#[test]
fn a_kept_version_lasts_until_the_program_file_changes() {
    let dir = tempfile::tempdir().unwrap();
    let program = dir.path().join("opencode");
    std::fs::write(&program, "one").unwrap();
    let seen = identify(&program).unwrap();
    assert_eq!(cached(dir.path(), &seen), None);
    remember(
        dir.path(),
        super::Seen {
            version: (1, 19, 4),
            ..seen.clone()
        },
    );
    assert_eq!(cached(dir.path(), &seen), Some((1, 19, 4)));
    std::fs::write(&program, "an upgrade").unwrap();
    let upgraded = identify(&program).unwrap();
    assert_eq!(cached(dir.path(), &upgraded), None);
    // A broken file is no version, not an error.
    std::fs::write(dir.path().join("host-versions.json"), "{").unwrap();
    assert_eq!(cached(dir.path(), &seen), None);
}

/// OpenCode 1.3.4 and later and OpenCode 2 take the plugin, each through its
/// own hook; older versions do not, and say why.
#[test]
fn versions_below_the_oldest_are_unsupported_and_others_name_their_hook() {
    let gate = Host::OpenCode
        .spec()
        .install
        .as_ref()
        .and_then(|i| i.versions.as_ref())
        .unwrap();
    assert_ne!(gate.older, "");
    let hook = |text| gate.hook_of(parse(text).unwrap());
    assert_eq!(hook("1.3.3"), None);
    assert_eq!(hook("1.0.0"), None);
    assert!(hook("1.3.4").unwrap().contains("OpenCode 1"));
    assert!(hook("1.19.4").unwrap().contains("tool.execute.before"));
    assert!(hook("2.0.0").unwrap().contains("OpenCode 2"));
    assert!(hook("2.2.0").unwrap().contains("`execute.before`"));
    // No other host names versions.
    assert!(Host::ALL.iter().all(|h| {
        *h == Host::OpenCode
            || h.spec()
                .install
                .as_ref()
                .is_none_or(|i| i.versions.is_none())
    }));
}

#[test]
fn a_version_after_a_program_name_is_read() {
    assert_eq!(parse_any("1.19.4\n"), Some((1, 19, 4)));
    assert_eq!(parse_any("codex-cli 0.164.0"), Some((0, 164, 0)));
    assert_eq!(parse_any("Hermes Agent v0.22.5 (x)"), Some((0, 22, 5)));
    assert_eq!(parse_any("no version here"), None);
}

/// The OpenCode MCP OAuth advisory: Windows and WSL from 1.0.137 up to, not
/// including, 1.18.33.
#[test]
fn the_opencode_sign_in_advisory_names_its_range_and_systems() {
    let spec = &Host::OpenCode.spec().advisories;
    let pillar = |version, platform| {
        spec.affecting(version, platform)
            .any(|a| a.fixed == (1, 18, 33))
    };
    assert!(pillar((1, 18, 32), Platform::Windows));
    assert!(pillar((1, 0, 137), Platform::Wsl));
    assert!(!pillar((1, 0, 136), Platform::Windows));
    assert!(!pillar((1, 18, 33), Platform::Windows));
    assert!(!pillar((1, 18, 32), Platform::MacOs));
    assert!(!pillar((1, 18, 32), Platform::Linux));
}

/// pi's `auth.json` write race (CVE-2026-54327): 0.28.0 up to 0.78.1.
#[test]
fn the_pi_credentials_race_advisory_names_its_range() {
    let spec = &Host::Pi.spec().advisories;
    let race = |version| {
        spec.affecting(version, Platform::MacOs)
            .any(|a| a.id == "GHSA-r95r-rj6r-c39x")
    };
    assert!(!race((0, 27, 9)));
    assert!(race((0, 28, 0)));
    assert!(race((0, 78, 0)));
    assert!(!race((0, 78, 1)));
}

#[test]
fn one_line_names_the_first_advisory_and_the_version_that_fixes_all() {
    let spec = &Host::Pi.spec().advisories;
    let affecting: Vec<&Advisory> = spec.affecting((0, 60, 0), Platform::Linux).collect();
    assert_eq!(affecting.len(), 4);
    let line = advisory_line("pi", (0, 60, 0), &affecting).unwrap();
    assert!(
        line.starts_with("pi 0.60.0 has a known security advisory, "),
        "{line}"
    );
    assert!(
        line.contains(" and 3 more; upgrade to 0.79.0 or later: https://"),
        "{line}"
    );
    assert!(!line.contains('\n'), "{line}");
    // The race in `auth.json` writes is fixed in 0.78.1, the rest by 0.79.0.
    let later: Vec<&Advisory> = spec.affecting((0, 78, 1), Platform::MacOs).collect();
    let line = advisory_line("pi", (0, 78, 1), &later).unwrap();
    assert!(line.contains("; upgrade to 0.79.0 or later"), "{line}");
    assert!(!line.contains("more"), "{line}");
    assert_eq!(advisory_line("pi", (0, 79, 0), &[]), None);
}

/// Every advisory names a source, a non-empty range and a fixed version.
#[test]
fn every_advisory_has_a_source_and_a_fix() {
    let mut count = 0;
    for host in Host::EVERY {
        let advisories = &host.spec().advisories;
        for a in advisories.list {
            count += 1;
            assert!(!advisories.program.is_empty(), "{}", host.id());
            assert!(a.from < a.fixed, "{}", a.id);
            assert!(a.source.starts_with("https://"), "{}", a.id);
            assert!(!a.summary.is_empty() && !a.id.is_empty(), "{}", a.id);
        }
    }
    assert!(count > 30, "{count}");
}