pushkin 0.2.1

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! Phase 3 task 3: per-dir AGENTS.md marker injection (spec §15 Phase 3;
//! addendum §6 standing-file row). Committed first, read-only hereafter
//! (charter §4.1). The managed block renders from the manifest — the same
//! single source of truth as `instructions --digest` — between pushkin
//! markers; user content outside the markers is never touched, and
//! `init --remove-agent agents-md` strips exactly the block.

use assert_cmd::Command;
use std::fs;

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"]
"#;

fn repo() -> std::io::Result<tempfile::TempDir> {
    let dir = tempfile::tempdir()?;
    fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
    Ok(dir)
}

fn run(dir: &tempfile::TempDir, args: &[&str]) -> (i32, String) {
    let Ok(mut command) = Command::cargo_bin("pushkin") else {
        return (-1, "cargo_bin resolution failed".to_owned());
    };
    let Ok(output) = command.current_dir(dir.path()).args(args).output() else {
        return (-1, "spawn failed".to_owned());
    };
    (
        output.status.code().unwrap_or(-1),
        format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        ),
    )
}

#[test]
fn agents_md_managed_block_written_between_markers() {
    let dir = repo().unwrap();
    let (code, output) = run(&dir, &["init", "--agent", "agents-md"]);
    assert_eq!(code, 0, "install succeeds: {output}");
    let agents = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    assert!(
        agents.contains("<!-- pushkin:begin"),
        "begin marker present: {agents}"
    );
    assert!(
        agents.contains("<!-- pushkin:end"),
        "end marker present: {agents}"
    );
    assert!(
        agents.contains("app/api/**/*.ts"),
        "block carries manifest facts (single source of truth): {agents}"
    );
}

#[test]
fn user_content_outside_markers_preserved() {
    let dir = repo().unwrap();
    fs::write(
        dir.path().join("AGENTS.md"),
        "# Team rules\n\nAlways use conventional commits.\n",
    )
    .unwrap();

    run(&dir, &["init", "--agent", "agents-md"]);
    let agents = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    assert!(
        agents.contains("Always use conventional commits."),
        "user content preserved: {agents}"
    );
    assert!(
        agents.contains("<!-- pushkin:begin"),
        "block appended alongside user content: {agents}"
    );
}

#[test]
fn managed_block_regenerates_deterministically() {
    let dir = repo().unwrap();
    fs::write(dir.path().join("AGENTS.md"), "# Team rules\n").unwrap();

    run(&dir, &["init", "--agent", "agents-md"]);
    let first = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    run(&dir, &["init", "--agent", "agents-md"]);
    let second = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    assert_eq!(
        first, second,
        "reinstall replaces the block in place — byte-identical, never stacked"
    );
    let marker_count = second.matches("<!-- pushkin:begin").count();
    assert_eq!(marker_count, 1, "exactly one managed block");
}

#[test]
fn remove_strips_only_managed_block() {
    let dir = repo().unwrap();
    fs::write(
        dir.path().join("AGENTS.md"),
        "# Team rules\n\nAlways use conventional commits.\n",
    )
    .unwrap();

    run(&dir, &["init", "--agent", "agents-md"]);
    let (code, output) = run(&dir, &["init", "--remove-agent", "agents-md"]);
    assert_eq!(code, 0, "remove succeeds: {output}");
    let agents = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    assert!(
        !agents.contains("pushkin:begin"),
        "managed block gone: {agents}"
    );
    assert!(
        agents.contains("Always use conventional commits."),
        "user content survives removal: {agents}"
    );
}