openmoat 0.14.0

Policy checks for AI coding agents: decides allow, defer, review 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_core::DEFAULT_POLICY;
use serde_json::{Value, json};

use super::super::settings::{apply, in_sync, remove};
use super::super::tests::generated;
use super::super::{BLOCK_READS, Generated};
use super::apply_rules;

/// The default policy's settings with `rules` as the `permissions.deny` rules.
fn with_rules(rules: &[&str]) -> Generated {
    let mut out = generated(DEFAULT_POLICY);
    out.deny_rules = rules.iter().map(|r| (*r).to_owned()).collect();
    out
}

/// `moat uninstall` takes exactly the deny rules OpenMoat added: a user's rule
/// spelled like one of them stays, and the file comes back byte for byte.
#[test]
fn uninstall_removes_exactly_the_deny_rules_it_added() {
    let out = with_rules(&["Read(//Users/me/.ssh)", "Edit(//Users/me/.moat)"]);
    let original = json!({
        "permissions": { "deny": ["Read(//Users/me/.ssh)", "Edit(//tmp/x)", "Bash(rm *)"] },
        "theme": "dark",
    });
    let text = |v: &Value| serde_json::to_string_pretty(v).unwrap();
    let mut root = original.clone();
    let mut owned = Vec::new();
    assert!(apply(&mut root, &out, &mut owned).unwrap());
    assert_eq!(
        owned,
        ["Edit(//Users/me/.moat)"],
        "the user's rule is not ours"
    );
    assert!(in_sync(&root, &out, &owned));
    assert!(!apply(&mut root, &out, &mut owned).unwrap(), "idempotent");
    assert!(remove(&mut root, &owned));
    assert_eq!(text(&root), text(&original));
}

/// A rule OpenMoat added that the policy no longer produces is out of sync,
/// and the next apply takes it.
#[test]
fn a_rule_the_policy_dropped_goes_on_the_next_apply() {
    let out = with_rules(&["Edit(//Users/me/.moat)"]);
    let mut root = json!({ "permissions": { "deny": ["Read(//Users/me/.old)", "Bash(rm *)"] } });
    let mut owned = vec!["Read(//Users/me/.old)".to_owned()];
    assert!(!in_sync(&root, &out, &owned));
    apply(&mut root, &out, &mut owned).unwrap();
    assert!(in_sync(&root, &out, &owned));
    assert_eq!(
        root["permissions"]["deny"],
        json!(["Bash(rm *)", "Edit(//Users/me/.moat)"])
    );
}

/// Native Windows has no Claude Code sandbox, but its deny rules still apply.
#[test]
fn apply_rules_writes_only_the_deny_rules() {
    let out = generated(DEFAULT_POLICY);
    let original = json!({ "theme": "dark", "permissions": { "allow": ["Bash(ls)"] } });
    let mut root = original.clone();
    let mut owned = Vec::new();
    apply(&mut root, &out, &mut owned).unwrap();
    assert!(apply_rules(&mut root, &out, &mut owned).unwrap());
    assert!(
        root.get("sandbox").is_none(),
        "an older version's sandbox keys go"
    );
    assert!(root["permissions"].get(BLOCK_READS).is_none());
    assert_eq!(owned, out.deny_rules);
    assert!(
        !apply_rules(&mut root, &out, &mut owned).unwrap(),
        "idempotent"
    );
    assert!(remove(&mut root, &owned));
    assert_eq!(root, original);
}