bobbin-ai 0.27.1

Local-first context injection engine for AI coding agents
use super::*;

#[test]
fn the_bundle_is_valid_owned_by_bobbin_and_versioned_by_the_build() {
    let b = bundle();
    assert_eq!(b["schema"], "st.hook-bundle/1");
    assert_eq!(b["name"], "bobbin");
    assert_eq!(b["owner"], "bobbin");
    assert_eq!(b["version"], env!("CARGO_PKG_VERSION"));
}

#[test]
fn a_hook_without_harnesses_applies_to_both() {
    let b = bundle();
    let claude = hooks_for(&b, Harness::Claude);
    let codex = hooks_for(&b, Harness::Codex);
    assert!(claude.len() > codex.len(), "claude-only hooks exist");
    for hook in &codex {
        assert!(claude.contains(hook), "{hook:?} is codex-only");
    }
    assert!(codex.iter().any(|h| h.0 == "UserPromptSubmit"));
}

#[test]
fn merge_is_idempotent_and_keeps_foreign_hooks() {
    let b = bundle();
    let mut cfg = json!({"hooks": {"UserPromptSubmit": [
        {"hooks": [{"type": "command", "command": "other-tool hook"}]}
    ]}, "model": "x"});
    let want = hooks_for(&b, Harness::Claude).len();
    assert_eq!(merge(&mut cfg, &b, Harness::Claude), want);
    assert_eq!(merge(&mut cfg, &b, Harness::Claude), 0);
    assert_eq!(present(&cfg, &b, Harness::Claude), (want, want));
    assert_eq!(cfg["model"], "x");
    let ups = &cfg["hooks"]["UserPromptSubmit"][0]["hooks"];
    assert!(ups
        .as_array()
        .unwrap()
        .iter()
        .any(|h| h["command"] == "other-tool hook"));
}

#[test]
fn remove_takes_only_bobbins_hooks() {
    let b = bundle();
    let mut cfg = json!({"hooks": {"UserPromptSubmit": [
        {"hooks": [{"type": "command", "command": "other-tool hook"}]}
    ]}});
    merge(&mut cfg, &b, Harness::Claude);
    let n = hooks_for(&b, Harness::Claude).len();
    assert_eq!(remove(&mut cfg, &b, Harness::Claude), n);
    assert_eq!(present(&cfg, &b, Harness::Claude).0, 0);
    assert_eq!(
        cfg["hooks"]["UserPromptSubmit"][0]["hooks"]
            .as_array()
            .unwrap()
            .len(),
        1
    );
    assert!(cfg["hooks"].get("PostToolUseFailure").is_none());
}

#[test]
fn codex_config_round_trips_through_toml() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("config.toml");
    std::fs::write(&path, "model = \"gpt\"\n[mcp_servers.x]\ncommand = \"x\"\n").unwrap();
    let b = bundle();
    let mut cfg = read_config(&path, Harness::Codex).unwrap();
    assert!(merge(&mut cfg, &b, Harness::Codex) > 0);
    write_config(&path, Harness::Codex, &cfg).unwrap();
    let back = read_config(&path, Harness::Codex).unwrap();
    let n = hooks_for(&b, Harness::Codex).len();
    assert_eq!(present(&back, &b, Harness::Codex), (n, n));
    assert_eq!(back["model"], "gpt");
    assert_eq!(back["mcp_servers"]["x"]["command"], "x");
    assert!(dir.path().join("config.toml.bak-bobbin").exists());
}