rustcodegraph 1.2.10

Local-first code intelligence graph and CLI for AI agents
Documentation
use super::common::*;

#[test]
fn claude_local_install_writes_mcp_json_project_scope_not_claude_json() {
    with_fixture("partial-claude-local-mcp", |fixture| {
        let claude = target("claude");
        let result = claude.install(Location::Local, install_options(false));
        assert!(
            result
                .files
                .iter()
                .any(|file| normalize(&file.path).ends_with("/.mcp.json"))
        );
        assert!(fixture.cwd().join(".mcp.json").exists());
        assert!(!fixture.cwd().join(".claude.json").exists());
        assert!(
            read_json(fixture.cwd().join(".mcp.json"))
                .pointer("/mcpServers/rustcodegraph")
                .is_some()
        );
    });
}

#[test]
fn claude_install_creates_claude_md_codegraph_block_704() {
    with_fixture("partial-claude-md", |fixture| {
        let claude = target("claude");
        let result = claude.install(Location::Local, install_options(false));
        let claude_md = fixture.cwd().join(".claude").join("CLAUDE.md");
        assert!(claude_md.exists());
        let body = read_text(&claude_md);
        assert!(body.contains("## RustCodeGraph"));
        assert!(body.contains("rustcodegraph explore"));
        assert_eq!(
            find_file(&result.files, "CLAUDE.md").unwrap().action,
            WriteAction::Created
        );
    });
}

#[test]
fn claude_install_leaves_codegraph_block_and_appends_rustcodegraph_block() {
    with_fixture("partial-claude-md-legacy", |fixture| {
        let claude = target("claude");
        let claude_md = fixture.cwd().join(".claude").join("CLAUDE.md");
        write_text(
            &claude_md,
            &format!("# My project rules\n\nUse tabs.\n\n{LEGACY_BLOCK}\n"),
        );
        let result = claude.install(Location::Local, install_options(false));
        let body = read_text(&claude_md);
        assert!(body.contains("# My project rules"));
        assert!(body.contains("Use tabs."));
        assert!(body.contains("Prefer `codegraph_search`"));
        assert!(body.contains("CODEGRAPH_START"));
        assert!(body.contains("rustcodegraph explore"));
        assert_eq!(
            find_file(&result.files, "CLAUDE.md").unwrap().action,
            WriteAction::Updated
        );
    });
}

#[test]
fn claude_global_install_targets_home_claude_json_user_scope() {
    with_fixture("partial-claude-global", |fixture| {
        let claude = target("claude");
        claude.install(Location::Global, install_options(false));
        assert!(
            read_json(fixture.home().join(".claude.json"))
                .pointer("/mcpServers/rustcodegraph")
                .is_some()
        );
    });
}

#[test]
fn claude_local_install_migrates_legacy_project_claude_json_entry_into_mcp_json() {
    with_fixture("partial-claude-migrate", |fixture| {
        let claude = target("claude");
        let legacy = fixture.cwd().join(".claude.json");
        write_json(
            &legacy,
            json!({ "mcpServers": { "rustcodegraph": { "type": "stdio", "command": "rustcodegraph", "args": ["serve", "--mcp"] } } }),
        );
        claude.install(Location::Local, install_options(false));
        assert!(
            read_json(fixture.cwd().join(".mcp.json"))
                .pointer("/mcpServers/rustcodegraph")
                .is_some()
        );
        assert!(!legacy.exists());
    });
}

#[test]
fn claude_legacy_claude_json_migration_preserves_sibling_servers_and_unrelated_keys() {
    with_fixture("partial-claude-migrate-siblings", |fixture| {
        let claude = target("claude");
        let legacy = fixture.cwd().join(".claude.json");
        write_json(
            &legacy,
            json!({
                "mcpServers": {
                    "rustcodegraph": { "type": "stdio", "command": "rustcodegraph", "args": ["serve", "--mcp"] },
                    "other": { "command": "x" }
                },
                "somethingElse": true
            }),
        );
        claude.install(Location::Local, install_options(false));
        let after = read_json(&legacy);
        assert!(after.pointer("/mcpServers/rustcodegraph").is_none());
        assert!(after.pointer("/mcpServers/other").is_some());
        assert_eq!(after.pointer("/somethingElse").unwrap(), true);
        assert!(
            read_json(fixture.cwd().join(".mcp.json"))
                .pointer("/mcpServers/rustcodegraph")
                .is_some()
        );
    });
}

#[test]
fn claude_uninstall_strips_rustcodegraph_from_mcp_json_and_legacy_claude_json() {
    with_fixture("partial-claude-uninstall-legacy", |fixture| {
        let claude = target("claude");
        write_json(
            fixture.cwd().join(".mcp.json"),
            json!({ "mcpServers": { "rustcodegraph": { "command": "rustcodegraph" } } }),
        );
        write_json(
            fixture.cwd().join(".claude.json"),
            json!({ "mcpServers": { "rustcodegraph": { "command": "rustcodegraph" }, "other": { "command": "x" } } }),
        );
        claude.uninstall(Location::Local);
        assert!(
            read_json(fixture.cwd().join(".mcp.json"))
                .get("mcpServers")
                .is_none()
        );
        let legacy = read_json(fixture.cwd().join(".claude.json"));
        assert!(legacy.pointer("/mcpServers/rustcodegraph").is_none());
        assert!(legacy.pointer("/mcpServers/other").is_some());
    });
}

fn seed_settings(base: &Path, loc: Location, settings: Value) -> PathBuf {
    let dir = match loc {
        Location::Global => base.join(".claude"),
        Location::Local => env::current_dir().unwrap().join(".claude"),
    };
    let file = dir.join("settings.json");
    write_json(&file, settings);
    file
}

fn legacy_hook_settings() -> Value {
    json!({
        "hooks": {
            "PostToolUse": [
                { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "rustcodegraph mark-dirty", "async": true }] }
            ],
            "Stop": [
                { "hooks": [{ "type": "command", "command": "rustcodegraph sync-if-dirty" }] },
                { "hooks": [{ "type": "command", "command": "\"/Users/me/gk\" ai hook run --host claude-code" }] }
            ]
        }
    })
}

#[test]
fn claude_install_strips_stale_auto_sync_hooks_but_keeps_user_hook() {
    with_fixture("partial-claude-hooks-install", |fixture| {
        let claude = target("claude");
        let file = seed_settings(fixture.home(), Location::Global, legacy_hook_settings());
        claude.install(Location::Global, install_options(true));
        let after = read_json(&file);
        assert!(after.pointer("/hooks/PostToolUse").is_none());
        let stop_commands = after
            .pointer("/hooks/Stop")
            .and_then(Value::as_array)
            .unwrap()
            .iter()
            .flat_map(|group| group["hooks"].as_array().unwrap().iter())
            .map(|hook| hook["command"].as_str().unwrap().to_owned())
            .collect::<Vec<_>>();
        assert!(
            !stop_commands
                .iter()
                .any(|cmd| cmd == "rustcodegraph sync-if-dirty")
        );
        assert!(
            stop_commands
                .iter()
                .any(|cmd| cmd.contains("gk") && cmd.contains("ai hook run"))
        );
        assert!(
            after
                .pointer("/permissions/allow")
                .unwrap()
                .as_array()
                .unwrap()
                .iter()
                .any(|value| value.as_str() == Some("mcp__rustcodegraph__rustcodegraph_search"))
        );
    });
}

#[test]
fn claude_cleanup_legacy_hooks_preserves_sibling_hook_sharing_matcher_group() {
    with_fixture("partial-claude-hooks-sibling", |fixture| {
        let file = seed_settings(
            fixture.home(),
            Location::Global,
            json!({
                "hooks": {
                    "Stop": [{
                        "hooks": [
                            { "type": "command", "command": "rustcodegraph sync-if-dirty" },
                            { "type": "command", "command": "gk ai hook run --host claude-code" }
                        ]
                    }]
                }
            }),
        );
        assert_eq!(
            cleanup_legacy_hooks(Location::Global).action,
            WriteAction::Removed
        );
        let after = read_json(&file);
        let commands = after
            .pointer("/hooks/Stop/0/hooks")
            .unwrap()
            .as_array()
            .unwrap()
            .iter()
            .map(|hook| hook["command"].as_str().unwrap().to_owned())
            .collect::<Vec<_>>();
        assert_eq!(commands, vec!["gk ai hook run --host claude-code"]);
    });
}

#[test]
fn claude_cleanup_legacy_hooks_is_byte_for_byte_noop_without_rustcodegraph_hooks() {
    with_fixture("partial-claude-hooks-noop", |fixture| {
        let original = serde_json::to_string_pretty(&json!({
            "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "gk ai hook run" }] }] }
        }))
        .unwrap()
            + "\n";
        let file = fixture.home().join(".claude").join("settings.json");
        write_text(&file, &original);
        assert_eq!(
            cleanup_legacy_hooks(Location::Global).action,
            WriteAction::Unchanged
        );
        assert_eq!(read_text(&file), original);
    });
}

#[test]
fn claude_cleanup_legacy_hooks_reports_not_found_when_settings_json_absent() {
    with_fixture("partial-claude-hooks-absent", |_| {
        assert_eq!(
            cleanup_legacy_hooks(Location::Global).action,
            WriteAction::NotFound
        );
    });
}

#[test]
fn claude_re_running_install_after_legacy_cleanup_leaves_settings_json_unchanged() {
    with_fixture("partial-claude-hooks-rerun", |fixture| {
        let claude = target("claude");
        let file = seed_settings(fixture.home(), Location::Global, legacy_hook_settings());
        claude.install(Location::Global, install_options(true));
        let first_pass = read_text(&file);
        claude.install(Location::Global, install_options(true));
        assert_eq!(read_text(&file), first_pass);
    });
}

#[test]
fn claude_uninstall_does_not_strip_stale_codegraph_npx_hooks() {
    with_fixture("partial-claude-hooks-local", |_| {
        let claude = target("claude");
        let file = seed_settings(
            Path::new("unused"),
            Location::Local,
            json!({
                "hooks": {
                    "PostToolUse": [
                        { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "npx rustcodegraph mark-dirty", "async": true }] }
                    ],
                    "Stop": [
                        { "hooks": [{ "type": "command", "command": "npx rustcodegraph sync-if-dirty" }] }
                    ]
                }
            }),
        );
        claude.uninstall(Location::Local);
        let after = read_json(&file);
        assert!(after.pointer("/hooks/PostToolUse").is_some());
        assert!(after.pointer("/hooks/Stop").is_some());
    });
}