code-system-graph-hooks 1.0.0

Repository host hook installation and lifecycle for Code System Graph.
Documentation
//! Cross-host self-tests for surgical installation and runtime policy.

use std::error::Error;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use code_system_graph_hooks::{
    HookMode, HostKind, InstallRequest, RoutingIntent, RoutingRequest, classify_prompt, install, route, status, uninstall
};
use serde_json::json;
use tempfile::TempDir;

const HOSTS: &[HostKind] = &[
    HostKind::ClaudeCode,
    HostKind::Codex,
    HostKind::Gemini,
    HostKind::Antigravity,
    HostKind::Cursor,
];

#[test]
fn every_host_install_is_surgical_idempotent_and_uninstallable() -> Result<(), Box<dyn Error>> {
    for &host in HOSTS {
        let fixture = Fixture::new(host)?;
        fixture.seed_host_file()?;
        let request = fixture.request(HookMode::Advisory);

        let first = install(&request)?;
        assert!(first.changed, "first install did not change {host:?}");
        assert!(
            !first.backups.is_empty(),
            "existing host file was not backed up for {host:?}"
        );
        assert!(
            !first.warnings.is_empty(),
            "duplicate Code System Graph integration was not reported for {host:?}"
        );
        assert!(
            fs::read_to_string(&first.host_file)?.contains("keep-me"),
            "unrelated host content was replaced for {host:?}"
        );
        assert_eq!(
            fs::read_to_string(
                first
                    .gitignore_path
                    .as_deref()
                    .ok_or("Git worktree did not receive an ignore path")?,
            )?,
            ".code-system-graph/\n",
            "generated hook state was not ignored for {host:?}"
        );

        let second = install(&request)?;
        assert!(!second.changed, "second install changed {host:?}");
        assert!(
            !second.gitignore_updated,
            "second install duplicated the ignore rule for {host:?}"
        );
        assert!(
            second.backups.is_empty(),
            "idempotent install created a backup for {host:?}"
        );
        assert!(status(&request)?.installed, "status missed {host:?}");

        let removal = uninstall(&request)?;
        assert!(removal.changed, "uninstall changed nothing for {host:?}");
        if first.host_file.exists() {
            let remaining = fs::read_to_string(&first.host_file)?;
            assert!(
                remaining.contains("keep-me"),
                "uninstall deleted unrelated content for {host:?}"
            );
            assert!(
                !remaining.contains("# BEGIN code-system-graph-hooks:v1"),
                "uninstall left marker content for {host:?}"
            );
            assert!(
                !remaining.contains("--marker code-system-graph-hooks:v1"),
                "uninstall left JSON marker content for {host:?}"
            );
        }
        assert!(
            !status(&request)?.installed,
            "status remained installed for {host:?}"
        );
    }
    Ok(())
}

#[test]
fn every_host_reinstalls_changed_codegraph_policy() -> Result<(), Box<dyn Error>> {
    for &host in HOSTS {
        let fixture = Fixture::new(host)?;
        let native = fixture.request(HookMode::Advisory);
        install(&native)?;
        let host_file = host_file(fixture.root(), host);
        let native_content = fs::read_to_string(&host_file)?;
        if matches!(
            host,
            HostKind::ClaudeCode | HostKind::Codex | HostKind::Gemini
        ) {
            assert!(native_content.contains("--codegraph-enabled false"));
        } else {
            assert!(!native_content.contains("explore"));
        }

        let enriched = InstallRequest {
            codegraph_enabled: true,
            ..native.clone()
        };
        assert!(
            install(&enriched)?.changed,
            "policy change did not update {host:?}"
        );
        assert!(status(&enriched)?.installed);
        assert!(!status(&native)?.installed);

        let enriched_content = fs::read_to_string(&host_file)?;
        if matches!(
            host,
            HostKind::ClaudeCode | HostKind::Codex | HostKind::Gemini
        ) {
            assert!(enriched_content.contains("--codegraph-enabled true"));
        } else {
            assert!(enriched_content.contains("explore"));
        }
    }
    Ok(())
}

#[cfg(unix)]
#[test]
fn every_host_state_is_restrictive_and_strict_gate_fails_closed() -> Result<(), Box<dyn Error>> {
    use std::os::unix::fs::{PermissionsExt, symlink};

    for &host in HOSTS {
        let fixture = Fixture::new(host)?;
        let fake = fixture.root().join("fake-code-system-graph");
        fs::write(&fake, "#!/bin/sh\nexit 9\n")?;
        fs::set_permissions(&fake, fs::Permissions::from_mode(0o700))?;
        let request = InstallRequest {
            code_system_graph_binary: fake,
            ..fixture.request(HookMode::Strict)
        };
        let report = install(&request)?;
        let mode = fs::metadata(&report.state_file)?.permissions().mode() & 0o777;
        assert_eq!(mode, 0o600, "state permissions differ for {host:?}");

        let gate = fixture.root().join(".git/hooks/pre-commit");
        let gate_source = fs::read_to_string(&gate)?;
        assert!(
            gate_source.contains("changes --scope staged"),
            "strict gate omitted staged changes for {host:?}"
        );
        assert!(
            gate_source.contains("exact_diff_fingerprint"),
            "strict gate omitted exact fingerprint for {host:?}"
        );
        assert!(
            !gate_source.contains(" scan ")
                && !gate_source.contains("codegraph init")
                && !gate_source.contains("codegraph sync"),
            "strict gate contains a forbidden operation for {host:?}"
        );

        let runner = fixture.root().join("run-pre-commit");
        symlink(&gate, &runner)?;
        let output = Command::new(&runner).current_dir(fixture.root()).output()?;
        assert!(
            !output.status.success(),
            "strict failure did not block for {host:?}"
        );
    }
    Ok(())
}

#[test]
fn every_host_routes_with_ttl_without_persisting_prompt_source() -> Result<(), Box<dyn Error>> {
    for &host in HOSTS {
        let fixture = Fixture::new(host)?;
        let prompt = "Show cross-repo API contract impact; source_password=never-store-this";
        let request = RoutingRequest {
            host,
            root: fixture.root().to_path_buf(),
            event: json!({"session_id": "session-1", "prompt": prompt}),
            codegraph_enabled: false,
            ttl_seconds: 300,
        };
        let first = route(&request)?;
        assert_eq!(first.intent, RoutingIntent::Federated);
        assert!(first.guidance.is_some(), "first route omitted {host:?}");
        let second = route(&request)?;
        assert!(second.deduplicated, "TTL did not deduplicate {host:?}");
        assert!(
            second.guidance.is_none(),
            "TTL repeated guidance for {host:?}"
        );

        let state = fs::read_to_string(fixture.root().join(format!(
            ".code-system-graph/hooks/dedup-{}.json",
            host.as_str()
        )))?;
        assert!(
            !state.contains(prompt) && !state.contains("source_password"),
            "routing state retained prompt source for {host:?}"
        );
    }
    Ok(())
}

#[test]
fn status_should_detect_routing_when_request_policy_differs_from_install_state()
-> Result<(), Box<dyn Error>> {
    let fixture = Fixture::new(HostKind::Codex)?;
    let enriched = InstallRequest {
        codegraph_enabled: true,
        ..fixture.request(HookMode::Advisory)
    };
    install(&enriched)?;
    let mismatched = InstallRequest {
        codegraph_enabled: false,
        ..enriched.clone()
    };
    let observed = status(&mismatched)?;

    assert!(observed.routing_installed);
    assert!(!observed.installed);
    assert!(status(&enriched)?.installed);

    Ok(())
}

#[test]
fn every_host_runtime_fails_open_on_invalid_input() -> Result<(), Box<dyn Error>> {
    let binary = env!("CARGO_BIN_EXE_code-system-graph-hooks");
    for &host in HOSTS {
        let fixture = Fixture::new(host)?;
        let root = fixture.root().to_string_lossy().into_owned();
        let mut child = Command::new(binary)
            .args([
                "route",
                "--host",
                host.as_str(),
                "--root",
                &root,
                "--codegraph-enabled",
                "false",
                "--marker",
                "code-system-graph-hooks:v1",
            ])
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .spawn()?;
        let mut stdin = child
            .stdin
            .take()
            .ok_or("child process stdin was unavailable")?;
        stdin.write_all(b"not-json")?;
        drop(stdin);
        let output = child.wait_with_output()?;
        assert!(output.status.success(), "advisory hook blocked {host:?}");
        let value: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        assert!(
            value.get("decision").and_then(serde_json::Value::as_str) != Some("deny")
                && value.get("continue").and_then(serde_json::Value::as_bool) != Some(false),
            "advisory output denied {host:?}"
        );
    }
    Ok(())
}

#[test]
fn classifier_prefers_federated_signals_over_local_signals() {
    assert_eq!(
        classify_prompt("Find this function's impact across repos"),
        RoutingIntent::Federated
    );
}

struct Fixture {
    directory: TempDir,
    host: HostKind,
}

impl Fixture {
    fn new(host: HostKind) -> Result<Self, Box<dyn Error>> {
        let directory = tempfile::tempdir()?;
        fs::create_dir_all(directory.path().join(".git/hooks"))?;
        fs::write(directory.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
        Ok(Self { directory, host })
    }

    fn root(&self) -> &Path {
        self.directory.path()
    }

    fn request(&self, mode: HookMode) -> InstallRequest {
        InstallRequest {
            root: self.root().to_path_buf(),
            host: self.host,
            mode,
            code_system_graph_binary: PathBuf::from("/usr/bin/csgraph"),
            database: self.root().join("mesh.sqlite"),
            workspace: "workspace-a".to_owned(),
            repository: "service-a".to_owned(),
            codegraph_enabled: false,
        }
    }

    fn seed_host_file(&self) -> Result<(), Box<dyn Error>> {
        let path = host_file(self.root(), self.host);
        let parent = path.parent().ok_or("host file has no parent")?;
        fs::create_dir_all(parent)?;
        let source = match self.host {
            HostKind::ClaudeCode | HostKind::Codex => {
                r#"{
  "keep-me": true,
  "hooks": {
    "UserPromptSubmit": [
      {"hooks": [{"type": "command", "command": "other-code-system-graph-hook"}]}
    ]
  }
}
"#
            }
            HostKind::Gemini => {
                r#"{
  "keep-me": true,
  "hooks": {
    "BeforeAgent": [
      {"hooks": [{"type": "command", "command": "other-code-system-graph-hook"}]}
    ]
  }
}
"#
            }
            HostKind::Antigravity => "keep-me\nOther Code System Graph guidance.\n",
            HostKind::Cursor => {
                "---\ndescription: keep-me\nalwaysApply: true\n---\n\nOther Code System Graph guidance.\n"
            }
            _ => return Err("unsupported test host".into()),
        };
        fs::write(path, source)?;
        Ok(())
    }
}

fn host_file(root: &Path, host: HostKind) -> PathBuf {
    root.join(match host {
        HostKind::ClaudeCode => ".claude/settings.local.json",
        HostKind::Codex => ".codex/hooks.json",
        HostKind::Gemini => ".gemini/settings.json",
        HostKind::Antigravity => ".agents/rules/code-system-graph-routing.md",
        HostKind::Cursor => ".cursor/rules/code-system-graph-routing.mdc",
        _ => ".code-system-graph/unsupported-host",
    })
}