pushkin 0.2.1

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! Distribution stream S2(b) — the native git pre-commit shim (D4,
//! IDEAS.md:11). `init --agent git` writes `<git-dir>/hooks/pre-commit`
//! as a pushkin-owned executable shim carrying the SAME N13 guard the
//! lefthook floor emits — no lefthook binary in the loop, no baked paths,
//! fail open only on positively probed absence. Detect-don't-clobber: a
//! `core.hooksPath` override or a foreign hook means a manager owns the
//! hooks; pushkin names what it can, prints the integration snippet, and
//! writes nothing.
//!
//! Committed first (RED), read-only hereafter (N10, product-gated).

use assert_cmd::Command;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::Command as StdCommand;

type TestResult = Result<(), Box<dyn std::error::Error>>;

const MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"

[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod"]

[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"

[gates]
protected_paths = ["pushkin.toml"]
"#;

/// Mirrors the canonical contract fixture used by `check_staged.rs`.
const CONTRACT: &str = "import { z } from \"zod\";\n\
                        export const UserCreateSchema = z.object({ name: z.string() });\n\
                        export type UserCreate = z.infer<typeof UserCreateSchema>;\n";

/// An unvalidated boundary handler — the "blocks" shape.
const VIOLATION: &str = "export async function POST(req: Request) {\n  \
                         const body = await req.json();\n  \
                         return Response.json({ name: body.name });\n}\n";

/// A handler that parses at the boundary — the "passes" shape.
const COMPLIANT: &str = "import { UserCreateSchema } from \"../../../contracts/user.zod\";\n\n\
                         export async function POST(req: Request) {\n  \
                         const body = UserCreateSchema.parse(await req.json());\n  \
                         return Response.json(body);\n}\n";

const SHIM: &str = ".git/hooks/pre-commit";

fn git(root: &Path, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
    let status = StdCommand::new("git")
        .current_dir(root)
        .args(args)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()?;
    if !status.success() {
        return Err(format!("git {args:?} failed").into());
    }
    Ok(())
}

fn binary_dir() -> Result<String, Box<dyn std::error::Error>> {
    Ok(Path::new(env!("CARGO_BIN_EXE_pushkin"))
        .parent()
        .ok_or("binary under test has no parent")?
        .to_string_lossy()
        .into_owned())
}

/// A git-backed gated repo with a staged handler.
fn staged_repo(handler: &str) -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
    let dir = tempfile::tempdir()?;
    let root = dir.path();
    fs::write(root.join("pushkin.toml"), MANIFEST)?;
    fs::create_dir_all(root.join("contracts"))?;
    fs::create_dir_all(root.join("app/api/users"))?;
    fs::write(root.join("contracts/user.zod.ts"), CONTRACT)?;
    fs::write(root.join("app/api/users/route.ts"), handler)?;
    git(root, &["init", "-q", "."])?;
    git(root, &["add", "-A"])?;
    Ok(dir)
}

fn run(dir: &Path, args: &[&str]) -> Result<(Option<i32>, String), Box<dyn std::error::Error>> {
    let output = Command::cargo_bin("pushkin")?
        .current_dir(dir)
        // The shim feature shells out to git, so PATH carries the system
        // bins beside the binary under test (the fail-open suite's
        // provisioned convention) — still never inherited.
        .env("PATH", provisioned_path()?)
        .env("HERMES_HOME", dir.join(".hermes-home"))
        .args(args)
        .output()?;
    Ok((
        output.status.code(),
        format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        ),
    ))
}

/// `git commit` with the hook chain live and PATH controlled explicitly.
fn commit(dir: &Path, path: &str) -> Result<(bool, String), Box<dyn std::error::Error>> {
    let output = StdCommand::new("git")
        .current_dir(dir)
        .env("PATH", path)
        .args([
            "-c",
            "user.name=Shim Suite",
            "-c",
            "user.email=shim@test",
            "commit",
            "-qm",
            "through the shim",
        ])
        .output()?;
    Ok((
        output.status.success(),
        format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        ),
    ))
}

fn provisioned_path() -> Result<String, Box<dyn std::error::Error>> {
    Ok(format!("{}:/usr/bin:/bin", binary_dir()?))
}

// ---------- install shape ----------

#[test]
fn install_writes_an_executable_guarded_shim() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    let (code, output) = run(dir.path(), &["init", "--agent", "git"])?;
    assert_eq!(code, Some(0), "install must succeed: {output}");

    let shim = fs::read_to_string(dir.path().join(SHIM))?;
    assert!(
        shim.starts_with("#!/bin/sh"),
        "a hook is a shell script: {shim}"
    );
    assert!(
        shim.contains("GENERATED by pushkin init"),
        "the shim is marked as pushkin-owned: {shim}"
    );
    assert!(
        shim.contains("command -v pushkin") && shim.contains("exec pushkin check --staged --json"),
        "the shim carries the N13 guard around the spec command: {shim}"
    );
    for forbidden in ["$?", "case ", "-eq 1", "-eq 2", "elif"] {
        assert!(
            !shim.contains(forbidden),
            "no exit-code case analysis in the guard ({forbidden:?}): {shim}"
        );
    }
    let mode = fs::metadata(dir.path().join(SHIM))?.permissions().mode();
    assert_eq!(mode & 0o111, 0o111, "the shim must be executable: {mode:o}");
    Ok(())
}

#[test]
fn install_outside_a_git_repo_refuses_with_a_named_reason() -> TestResult {
    let dir = tempfile::tempdir()?;
    fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;

    let (code, output) = run(dir.path(), &["init", "--agent", "git"])?;
    assert_ne!(code, Some(0), "no git repo, no shim: {output}");
    assert!(
        output.contains("not a git repository"),
        "the refusal names the reason: {output}"
    );
    Ok(())
}

#[test]
fn a_foreign_hook_is_never_clobbered() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    let foreign = "#!/bin/sh\necho custom hook\n";
    fs::create_dir_all(dir.path().join(".git/hooks"))?;
    fs::write(dir.path().join(SHIM), foreign)?;

    let (code, output) = run(dir.path(), &["init", "--agent", "git"])?;
    assert_eq!(
        code,
        Some(0),
        "detect-don't-clobber is not an error: {output}"
    );
    assert_eq!(
        fs::read_to_string(dir.path().join(SHIM))?,
        foreign,
        "the existing hook is the user's and must survive byte-identical"
    );
    assert!(
        output.contains("pushkin check --staged --json"),
        "the integration snippet names the command to add: {output}"
    );
    Ok(())
}

#[test]
fn a_hooks_path_override_defers_to_the_manager() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    git(dir.path(), &["config", "core.hooksPath", ".husky"])?;

    let (code, output) = run(dir.path(), &["init", "--agent", "git"])?;
    assert_eq!(code, Some(0), "deferring is not an error: {output}");
    assert!(
        !dir.path().join(SHIM).exists(),
        "hooks are owned elsewhere; nothing may be written: {output}"
    );
    assert!(
        output.contains("core.hooksPath"),
        "the deferral names what owns the hooks: {output}"
    );
    Ok(())
}

#[test]
fn reinstall_is_idempotent() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    run(dir.path(), &["init", "--agent", "git"])?;
    let first = fs::read_to_string(dir.path().join(SHIM))?;
    run(dir.path(), &["init", "--agent", "git"])?;
    let second = fs::read_to_string(dir.path().join(SHIM))?;
    assert_eq!(first, second, "a second install is a byte-level no-op");
    Ok(())
}

// ---------- removal ----------

#[test]
fn remove_deletes_a_pushkin_shim() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    run(dir.path(), &["init", "--agent", "git"])?;

    let (code, output) = run(dir.path(), &["init", "--remove-agent", "git"])?;
    assert_eq!(code, Some(0), "remove must succeed: {output}");
    assert!(
        !dir.path().join(SHIM).exists(),
        "a pushkin-owned shim is removed entirely"
    );
    Ok(())
}

#[test]
fn remove_leaves_a_foreign_hook_alone() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    let foreign = "#!/bin/sh\necho custom hook\n";
    fs::create_dir_all(dir.path().join(".git/hooks"))?;
    fs::write(dir.path().join(SHIM), foreign)?;

    let (code, output) = run(dir.path(), &["init", "--remove-agent", "git"])?;
    assert_eq!(
        code,
        Some(0),
        "remove on a foreign hook is a no-op: {output}"
    );
    assert_eq!(
        fs::read_to_string(dir.path().join(SHIM))?,
        foreign,
        "the foreign hook survives byte-identical"
    );
    Ok(())
}

// ---------- proven through real `git commit`, not just file shape ----------

#[test]
fn a_staged_violation_blocks_the_commit_through_the_shim() -> TestResult {
    let dir = staged_repo(VIOLATION)?;
    run(dir.path(), &["init", "--agent", "git"])?;
    git(dir.path(), &["add", "-A"])?;

    let (ok, output) = commit(dir.path(), &provisioned_path()?)?;
    assert!(
        !ok,
        "a provisioned commit with a staged violation must be blocked: {output}"
    );
    Ok(())
}

#[test]
fn a_compliant_tree_commits_through_the_shim() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    run(dir.path(), &["init", "--agent", "git"])?;
    git(dir.path(), &["add", "-A"])?;

    let (ok, output) = commit(dir.path(), &provisioned_path()?)?;
    assert!(ok, "a compliant staged tree must commit: {output}");
    Ok(())
}

#[test]
fn an_unprovisioned_commit_fails_open_with_the_notice() -> TestResult {
    let dir = staged_repo(VIOLATION)?;
    run(dir.path(), &["init", "--agent", "git"])?;
    git(dir.path(), &["add", "-A"])?;

    // lefthook-free by construction; PATH carries git and sh, never pushkin.
    let (ok, output) = commit(dir.path(), "/usr/bin:/bin")?;
    assert!(
        ok,
        "an unprovisioned teammate's commit must NOT be blocked: {output}"
    );
    assert!(
        output.contains("failing open") && output.contains("cargo install pushkin"),
        "the notice reaches the developer with the actionable hint: {output}"
    );
    Ok(())
}

// ---------- doctor coverage ----------

#[test]
fn doctor_stays_healthy_when_no_shim_is_installed() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    run(dir.path(), &["init", "--agent", "claude"])?;

    let (_, output) = run(dir.path(), &["doctor"])?;
    assert!(
        output.contains("hooks healthy"),
        "an absent shim is never a finding (presence-keyed): {output}"
    );
    Ok(())
}

#[test]
fn doctor_flags_a_non_executable_shim_and_repair_restores_it() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    run(dir.path(), &["init", "--agent", "claude"])?;
    run(dir.path(), &["init", "--agent", "git"])?;
    let shim = dir.path().join(SHIM);
    fs::set_permissions(&shim, fs::Permissions::from_mode(0o644))?;

    let (code, output) = run(dir.path(), &["doctor"])?;
    assert_eq!(code, Some(1), "a dead hook is a red finding: {output}");
    assert!(
        output.contains("executable"),
        "the finding names the lost executable bit: {output}"
    );

    run(dir.path(), &["doctor", "--repair"])?;
    let mode = fs::metadata(&shim)?.permissions().mode();
    assert_eq!(mode & 0o111, 0o111, "repair restores the bit: {mode:o}");
    let (_, after) = run(dir.path(), &["doctor"])?;
    assert!(
        after.contains("hooks healthy"),
        "the finding clears: {after}"
    );
    Ok(())
}

#[test]
fn doctor_reports_a_foreign_hook_as_not_ours_and_repair_leaves_it() -> TestResult {
    let dir = staged_repo(COMPLIANT)?;
    run(dir.path(), &["init", "--agent", "claude"])?;
    let foreign = "#!/bin/sh\necho custom hook\n";
    fs::create_dir_all(dir.path().join(".git/hooks"))?;
    fs::write(dir.path().join(SHIM), foreign)?;

    let (_, output) = run(dir.path(), &["doctor"])?;
    assert!(
        output.contains("hooks healthy"),
        "a foreign hook is not ours to flag: {output}"
    );

    run(dir.path(), &["doctor", "--repair"])?;
    assert_eq!(
        fs::read_to_string(dir.path().join(SHIM))?,
        foreign,
        "repair never rewrites a hook pushkin does not own"
    );
    Ok(())
}