agent-source-repository 0.1.0

Agent Source Repository local context registry for coding agents
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use serde_json::Value;

fn temp_dir(name: &str) -> PathBuf {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let root = std::env::temp_dir().join(format!("asr-read-diff-test-{name}-{unique}"));
    fs::create_dir_all(&root).unwrap();
    root
}

fn run_asr(args: &[&str], cwd: &Path, asr_home: &Path) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_asr"))
        .args(args)
        .current_dir(cwd)
        .env("ASR_HOME", asr_home)
        .output()
        .unwrap()
}

fn git(args: &[&str], cwd: &Path) {
    let output = Command::new("git")
        .args(args)
        .current_dir(cwd)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git {:?} failed\nstdout: {}\nstderr: {}",
        args,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn stdout_json(output: &std::process::Output) -> Value {
    serde_json::from_slice(&output.stdout).unwrap_or_else(|err| {
        panic!(
            "stdout is not JSON: {err}\nstdout: {}\nstderr: {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        )
    })
}

fn stderr_json(output: &std::process::Output) -> Value {
    serde_json::from_slice(&output.stderr).unwrap_or_else(|err| {
        panic!(
            "stderr is not JSON: {err}\nstdout: {}\nstderr: {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        )
    })
}

fn registered_repo() -> (PathBuf, PathBuf, PathBuf) {
    let cwd = temp_dir("cwd");
    let asr_home = temp_dir("home").join("asr-home");
    let repo = temp_dir("repo");
    git(&["init", "-q"], &repo);
    git(&["config", "user.email", "asr@example.invalid"], &repo);
    git(&["config", "user.name", "ASR Test"], &repo);
    fs::create_dir_all(repo.join("src")).unwrap();
    fs::write(
        repo.join("src/retry.rs"),
        "pub fn retry_backoff() -> u64 {\n    100\n}\n",
    )
    .unwrap();
    git(&["add", "."], &repo);
    git(&["commit", "-q", "-m", "initial"], &repo);
    fs::write(
        repo.join("src/retry.rs"),
        "pub fn retry_backoff() -> u64 {\n    200\n}\n",
    )
    .unwrap();
    git(&["add", "."], &repo);
    git(&["commit", "-q", "-m", "change"], &repo);
    assert!(run_asr(&["init", "--json"], &cwd, &asr_home)
        .status
        .success());
    assert!(run_asr(
        &["repo", "add", "app", repo.to_str().unwrap(), "--json"],
        &cwd,
        &asr_home,
    )
    .status
    .success());
    assert!(
        run_asr(&["repo", "index", "app", "--json"], &cwd, &asr_home)
            .status
            .success()
    );
    (cwd, asr_home, repo)
}

fn registered_unindexed_repo() -> (PathBuf, PathBuf, PathBuf) {
    let cwd = temp_dir("live-cwd");
    let asr_home = temp_dir("live-home").join("asr-home");
    let repo = temp_dir("live-repo");
    git(&["init", "-q"], &repo);
    git(&["config", "user.email", "asr@example.invalid"], &repo);
    git(&["config", "user.name", "ASR Test"], &repo);
    fs::create_dir_all(repo.join("src")).unwrap();
    fs::write(
        repo.join("src/retry.rs"),
        "pub fn retry_backoff() -> u64 {\n    100\n}\n",
    )
    .unwrap();
    git(&["add", "."], &repo);
    git(&["commit", "-q", "-m", "initial"], &repo);
    assert!(run_asr(&["init", "--json"], &cwd, &asr_home)
        .status
        .success());
    assert!(run_asr(
        &["repo", "add", "app", repo.to_str().unwrap(), "--json"],
        &cwd,
        &asr_home,
    )
    .status
    .success());
    (cwd, asr_home, repo)
}

#[test]
fn asr_read_returns_only_requested_numbered_lines() {
    let (cwd, asr_home, _repo) = registered_repo();

    let output = run_asr(
        &["read", "app", "src/retry.rs", "--lines", "1:2", "--json"],
        &cwd,
        &asr_home,
    );
    assert!(output.status.success());
    let json = stdout_json(&output);
    let lines = json["lines"].as_array().unwrap();

    assert_eq!(json["repo"], "app");
    assert_eq!(json["path"], "src/retry.rs");
    assert_eq!(
        json["source_policy"]["mode"],
        "indexed_snapshot_fresh_source"
    );
    assert_eq!(json["source_policy"]["snapshot_bound"], true);
    assert_eq!(json["start_line"], 1);
    assert_eq!(json["end_line"], 2);
    assert_eq!(lines.len(), 2);
    assert_eq!(lines[0]["line"], 1);
    assert!(lines[0]["content"]
        .as_str()
        .unwrap()
        .contains("retry_backoff"));
}

#[test]
fn asr_read_requires_snapshot_unless_live_is_explicit() {
    let (cwd, asr_home, _repo) = registered_unindexed_repo();

    let default_read = run_asr(
        &["read", "app", "src/retry.rs", "--lines", "1:1", "--json"],
        &cwd,
        &asr_home,
    );
    assert!(!default_read.status.success());
    assert_eq!(
        stderr_json(&default_read)["error"]["code"],
        "repo_not_indexed"
    );

    let live_read = run_asr(
        &[
            "read",
            "app",
            "src/retry.rs",
            "--lines",
            "1:1",
            "--live",
            "--json",
        ],
        &cwd,
        &asr_home,
    );
    assert!(live_read.status.success());
    let json = stdout_json(&live_read);
    assert_eq!(json["source_policy"]["mode"], "live_registered_source");
    assert_eq!(json["source_policy"]["live"], true);
}

#[test]
fn asr_read_rejects_unsafe_or_invalid_ranges() {
    let (cwd, asr_home, _repo) = registered_repo();

    let invalid = run_asr(
        &["read", "app", "src/retry.rs", "--lines", "2:1", "--json"],
        &cwd,
        &asr_home,
    );
    assert!(!invalid.status.success());
    assert_eq!(stderr_json(&invalid)["error"]["code"], "invalid_line_range");

    let escape = run_asr(
        &["read", "app", "../outside.rs", "--lines", "1:1", "--json"],
        &cwd,
        &asr_home,
    );
    assert!(!escape.status.success());
    assert_eq!(stderr_json(&escape)["error"]["code"], "invalid_path");

    let git_internal = run_asr(
        &["read", "app", ".git/config", "--lines", "1:1", "--json"],
        &cwd,
        &asr_home,
    );
    assert!(!git_internal.status.success());
    assert_eq!(stderr_json(&git_internal)["error"]["code"], "invalid_path");

    let nested_git_component = run_asr(
        &["read", "app", "src/.git/config", "--lines", "1:1", "--json"],
        &cwd,
        &asr_home,
    );
    assert!(!nested_git_component.status.success());
    assert_eq!(
        stderr_json(&nested_git_component)["error"]["code"],
        "invalid_path"
    );

    let out_of_bounds = run_asr(
        &["read", "app", "src/retry.rs", "--lines", "1:99", "--json"],
        &cwd,
        &asr_home,
    );
    assert!(!out_of_bounds.status.success());
    assert_eq!(
        stderr_json(&out_of_bounds)["error"]["code"],
        "invalid_line_range"
    );
}

#[test]
fn asr_diff_returns_hunk_metadata_not_full_files() {
    let (cwd, asr_home, _repo) = registered_repo();

    let output = run_asr(
        &[
            "diff", "app", "--base", "HEAD~1", "--head", "HEAD", "--json",
        ],
        &cwd,
        &asr_home,
    );
    assert!(
        output.status.success(),
        "stderr: {} stdout: {}",
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&output.stdout)
    );
    let json = stdout_json(&output);
    let hunks = json["hunks"].as_array().unwrap();

    assert_eq!(json["repo"], "app");
    assert_eq!(json["source_policy"]["mode"], "git_ref_diff");
    assert_eq!(json["source_policy"]["snapshot_bound"], false);
    assert!(json["hunk_count"].as_u64().unwrap() >= 1);
    assert_eq!(json["changed_files"].as_array().unwrap()[0], "src/retry.rs");
    assert_eq!(hunks[0]["path"], "src/retry.rs");
    assert!(hunks[0]["added_lines"].as_u64().unwrap() >= 1);
    assert!(hunks[0]["removed_lines"].as_u64().unwrap() >= 1);
    assert!(
        hunks[0].get("content").is_none(),
        "diff output must not dump full hunk content"
    );
}

#[test]
fn asr_diff_rejects_option_like_git_refs_before_running_git() {
    let (cwd, asr_home, _repo) = registered_repo();

    let output = run_asr(
        &[
            "diff", "app", "--base", "--help", "--head", "HEAD", "--json",
        ],
        &cwd,
        &asr_home,
    );

    assert!(!output.status.success());
    assert_eq!(stderr_json(&output)["error"]["code"], "invalid_git_ref");
}