lernie 0.0.5

A git-backed agent harness
Documentation
//! The working-directory mark against a real workspace (ARCH §3.3).

use super::{cwd_ref, read, write};
use crate::template::{GitRunner, RealGit};
use crate::workspace::{fixture, repo_git};
use std::path::{Path, PathBuf};

/// A workspace with one root agent — the shape every tool call runs in.
fn agent() -> (tempfile::TempDir, PathBuf) {
    let (holder, ws) = fixture::workspace();
    fixture::spawn_root(&ws, "a");
    (holder, ws)
}

#[test]
fn the_mark_ref_lives_in_the_shared_per_agent_mark_namespace() {
    // §9.2's retention delete enumerates `refs/lernie/`, so a mark that
    // spells its own root would outlive the agent it belongs to.
    assert_eq!(cwd_ref("a-b"), "refs/lernie/cwd/a-b");
}

#[test]
fn an_unset_mark_reads_as_none_so_the_default_applies() {
    let (_h, ws) = agent();
    assert_eq!(read(&ws, "a", &RealGit::new()), None);
}

#[test]
fn a_written_mark_reads_back_the_same_absolute_path() {
    let (_h, ws) = agent();
    let git = RealGit::new();
    let dir = ws.join("agents/a");
    write(&ws, "a", &dir, &git).unwrap();
    assert_eq!(read(&ws, "a", &git), Some(dir));
}

#[test]
fn a_second_write_wins_because_a_cd_replaces_where_the_agent_is() {
    let (_h, ws) = agent();
    let git = RealGit::new();
    write(&ws, "a", Path::new("/tmp"), &git).unwrap();
    write(&ws, "a", Path::new("/usr"), &git).unwrap();
    assert_eq!(read(&ws, "a", &git), Some(PathBuf::from("/usr")));
}

#[test]
fn the_mark_is_per_agent_so_one_agents_cd_never_moves_another() {
    let (_h, ws) = agent();
    let git = RealGit::new();
    fixture::spawn_root(&ws, "b");
    write(&ws, "a", Path::new("/tmp"), &git).unwrap();
    assert_eq!(read(&ws, "b", &git), None);
}

#[test]
fn hashing_the_value_leaves_nothing_behind_beside_the_repo() {
    let (_h, ws) = agent();
    write(&ws, "a", Path::new("/tmp"), &RealGit::new()).unwrap();
    let leftovers: Vec<_> = std::fs::read_dir(repo_git(&ws))
        .unwrap()
        .flatten()
        .map(|e| e.file_name().to_string_lossy().into_owned())
        .filter(|n| n.starts_with("cwd-mark."))
        .collect();
    assert!(
        leftovers.is_empty(),
        "staged value left behind: {leftovers:?}"
    );
}

#[test]
fn a_mark_pointing_at_a_non_blob_reads_as_none_rather_than_failing_a_tool_call() {
    let (_h, ws) = agent();
    let git = RealGit::new();
    let repo = repo_git(&ws);
    let head = git.run_capture(&repo, &["rev-parse", "agents/a"]).unwrap();
    git.run(&repo, &["update-ref", &cwd_ref("a"), &head])
        .unwrap();
    assert_eq!(read(&ws, "a", &git), None);
}

#[test]
fn a_workspace_with_no_repo_reads_as_none() {
    let holder = tempfile::TempDir::new().unwrap();
    assert_eq!(read(holder.path(), "a", &RealGit::new()), None);
}

#[test]
fn a_path_that_trimming_would_change_is_declined_rather_than_stored_wrong() {
    let (_h, ws) = agent();
    let err = write(&ws, "a", Path::new("/tmp/trailing "), &RealGit::new()).unwrap_err();
    assert!(err.to_string().contains("trimmed UTF-8"), "{err}");
    assert_eq!(read(&ws, "a", &RealGit::new()), None);
}

#[test]
fn an_empty_path_is_declined() {
    let (_h, ws) = agent();
    let err = write(&ws, "a", Path::new(""), &RealGit::new()).unwrap_err();
    assert!(err.to_string().contains("trimmed UTF-8"), "{err}");
}

#[test]
fn a_non_utf8_path_is_declined() {
    use std::ffi::OsStr;
    use std::os::unix::ffi::OsStrExt;
    let (_h, ws) = agent();
    let bad = PathBuf::from(OsStr::from_bytes(b"/tmp/\xff"));
    let err = write(&ws, "a", &bad, &RealGit::new()).unwrap_err();
    assert!(err.to_string().contains("trimmed UTF-8"), "{err}");
}

#[test]
fn a_git_that_cannot_hash_the_value_surfaces_the_failure() {
    let (_h, ws) = agent();
    // No `repo.git` under this path, so `hash-object` has no object
    // database to write into.
    let bare = ws.join("nowhere");
    std::fs::create_dir_all(super::repo_git(&bare)).unwrap();
    let err = write(&bare, "a", Path::new("/tmp"), &RealGit::new()).unwrap_err();
    assert!(err.to_string().contains("hash-object"), "{err}");
}

#[test]
fn a_value_that_cannot_be_staged_surfaces_the_io_failure() {
    let holder = tempfile::TempDir::new().unwrap();
    // `repo.git` absent entirely: staging the value has nowhere to land.
    let err = write(holder.path(), "a", Path::new("/tmp"), &RealGit::new()).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}