frigg 0.6.2

Frigg gives AI agents local, source-backed code search and navigation without sending whole repositories through every prompt.
Documentation
#![allow(clippy::panic)]

//! CLI integration tests for project-local Frigg adoption writes.

use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

fn temp_workspace_root(test_name: &str) -> PathBuf {
    let nanos_since_epoch = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!(
        "frigg-cli-adopt-{test_name}-{}-{nanos_since_epoch}",
        std::process::id()
    ))
}

fn cleanup_workspace(root: &Path) {
    let _ = fs::remove_dir_all(root);
}

fn frigg_bin() -> PathBuf {
    std::env::var_os("CARGO_BIN_EXE_frigg")
        .map(PathBuf::from)
        .expect("CARGO_BIN_EXE_frigg should be set for integration tests")
}

fn run_frigg(root: &Path, args: &[&str]) -> Output {
    Command::new(frigg_bin())
        .arg("--workspace-root")
        .arg(root)
        .args(args)
        .output()
        .expect("run frigg binary")
}

fn stdout(output: &Output) -> String {
    String::from_utf8_lossy(&output.stdout).into_owned()
}

fn stderr(output: &Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}

fn assert_success(output: &Output) {
    assert!(
        output.status.success(),
        "expected success\nstdout:\n{}\nstderr:\n{}",
        stdout(output),
        stderr(output)
    );
}

fn assert_failure(output: &Output) {
    assert!(
        !output.status.success(),
        "expected failure\nstdout:\n{}\nstderr:\n{}",
        stdout(output),
        stderr(output)
    );
}

#[test]
fn adopt_cli_dry_run_prints_plan_and_writes_nothing() {
    let root = temp_workspace_root("dry-run");
    fs::create_dir_all(&root).expect("create temp root");

    let output = run_frigg(&root, &["adopt", "--target", "agents-md", "--dry-run"]);

    assert_success(&output);
    assert!(stdout(&output).contains("action=create"));
    assert!(!root.join("AGENTS.md").exists());
    cleanup_workspace(&root);
}

#[test]
fn adopt_cli_check_fails_when_pending_and_passes_when_unchanged() {
    let root = temp_workspace_root("check");
    fs::create_dir_all(&root).expect("create temp root");

    let pending = run_frigg(&root, &["adopt", "--target", "agents-md", "--check"]);
    assert_failure(&pending);
    assert!(stdout(&pending).contains("status=pending"));
    let pending_stderr = stderr(&pending);
    assert!(pending_stderr.contains("error adopt: failed status=failed mode=check pending=1"));
    assert!(!pending_stderr.contains("Error:"));
    assert!(!root.join("AGENTS.md").exists());

    let applied = run_frigg(&root, &["adopt", "--target", "agents-md"]);
    assert_success(&applied);

    let unchanged = run_frigg(&root, &["adopt", "--target", "agents-md", "--check"]);
    assert_success(&unchanged);
    assert!(stdout(&unchanged).contains("action=unchanged"));
    cleanup_workspace(&root);
}

#[test]
fn adopt_cli_applies_markdown_cursor_and_mcp_targets_idempotently() {
    let root = temp_workspace_root("apply-targets");
    fs::create_dir_all(&root).expect("create temp root");

    let first = run_frigg(
        &root,
        &[
            "adopt",
            "--target",
            "agents-md",
            "--target",
            "cursor",
            "--target",
            "mcp-project",
            "--target",
            "mcp-cursor",
        ],
    );
    assert_success(&first);
    assert!(stdout(&first).contains("writes=4"));
    assert!(!stdout(&first).contains("adopt plan"));

    for path in ["AGENTS.md", ".cursor/rules/frigg.mdc"] {
        let contents = fs::read_to_string(root.join(path)).expect("read adopted markdown");
        assert!(contents.contains("frigg-directive:start"));
        assert!(contents.contains("frigg-directive:end"));
    }
    assert!(
        !root.join(".cursorrules").exists(),
        "adopt should no longer create legacy Cursor rules"
    );

    for path in [".mcp.json", ".cursor/mcp.json"] {
        let value: serde_json::Value = serde_json::from_str(
            &fs::read_to_string(root.join(path)).expect("read adopted mcp config"),
        )
        .expect("parse mcp config");
        assert_eq!(
            value["mcpServers"]["frigg"]["url"],
            "http://127.0.0.1:37444/mcp"
        );
    }

    let second = run_frigg(
        &root,
        &[
            "adopt",
            "--target",
            "agents-md",
            "--target",
            "cursor",
            "--target",
            "mcp-project",
            "--target",
            "mcp-cursor",
        ],
    );
    assert_success(&second);
    assert!(stdout(&second).contains("unchanged=4"));
    assert!(stdout(&second).contains("writes=0"));
    assert!(!stdout(&second).contains("adopt apply writes="));
    cleanup_workspace(&root);
}

#[test]
fn adopt_cli_rejects_legacy_cursor_surface() {
    let root = temp_workspace_root("reject-legacy-cursor");
    fs::create_dir_all(&root).expect("create temp root");

    let flag = run_frigg(&root, &["adopt", "--legacy-cursor"]);
    assert_failure(&flag);
    assert!(stderr(&flag).contains("--legacy-cursor"));

    let target = run_frigg(&root, &["adopt", "--target", "legacy-cursor"]);
    assert_failure(&target);
    assert!(stderr(&target).contains("legacy-cursor"));

    cleanup_workspace(&root);
}

#[test]
fn adopt_cli_uninstall_removes_only_frigg_owned_content() {
    let root = temp_workspace_root("uninstall");
    fs::create_dir_all(root.join(".cursor/rules")).expect("create cursor rules dir");
    fs::write(root.join("AGENTS.md"), "# Team notes\n").expect("write agents");

    let applied = run_frigg(
        &root,
        &[
            "adopt",
            "--target",
            "agents-md",
            "--target",
            "cursor",
            "--target",
            "mcp-project",
        ],
    );
    assert_success(&applied);

    fs::write(
        root.join(".mcp.json"),
        r#"{"mcpServers":{"frigg":{"type":"http","url":"http://127.0.0.1:37444/mcp"},"other":{"command":"other"}},"unrelated":true}"#,
    )
    .expect("add sibling server");

    let removed = run_frigg(
        &root,
        &[
            "adopt",
            "--target",
            "agents-md",
            "--target",
            "cursor",
            "--target",
            "mcp-project",
            "--uninstall",
        ],
    );
    assert_success(&removed);

    let agents = fs::read_to_string(root.join("AGENTS.md")).expect("read agents");
    assert_eq!(agents, "# Team notes\n");
    assert!(!root.join(".cursor/rules/frigg.mdc").exists());

    let value: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(root.join(".mcp.json")).expect("read mcp"))
            .expect("parse mcp");
    assert!(value["mcpServers"].get("frigg").is_none());
    assert_eq!(value["mcpServers"]["other"]["command"], "other");
    assert_eq!(value["unrelated"], true);
    cleanup_workspace(&root);
}

#[test]
fn adopt_cli_force_replaces_diverged_frigg_mcp_entry() {
    let root = temp_workspace_root("force");
    fs::create_dir_all(&root).expect("create temp root");
    fs::write(
        root.join(".mcp.json"),
        r#"{"mcpServers":{"frigg":{"command":"custom"},"other":{"command":"other"}}}"#,
    )
    .expect("write diverged mcp");

    let skipped = run_frigg(&root, &["adopt", "--target", "mcp-project"]);
    assert_success(&skipped);
    assert!(stdout(&skipped).contains("skipped=1"));
    assert!(!stdout(&skipped).contains("adopt plan"));
    let value: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(root.join(".mcp.json")).expect("read mcp"))
            .expect("parse mcp");
    assert_eq!(value["mcpServers"]["frigg"]["command"], "custom");

    let forced = run_frigg(&root, &["adopt", "--target", "mcp-project", "--force"]);
    assert_success(&forced);
    let value: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(root.join(".mcp.json")).expect("read mcp"))
            .expect("parse mcp");
    assert_eq!(
        value["mcpServers"]["frigg"]["url"],
        "http://127.0.0.1:37444/mcp"
    );
    assert_eq!(value["mcpServers"]["other"]["command"], "other");
    cleanup_workspace(&root);
}