balls 0.5.12

Git-native task tracker for parallel agent workflows
Documentation
//! Unit tests for [`crate::speculate`] — fixture repos in tempdirs, no env
//! reads, no real toolchain (the toolchain string is an argument).

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use tempfile::TempDir;

use super::{check, gate_fingerprint, read, record, verdict_path, worktree_oid, write, Verdict};

/// A throwaway git repo: one gate script and one source file, both tracked
/// once `git add -A` stages them — which is how the tree oid sees the gate.
fn repo() -> (TempDir, PathBuf) {
    let tmp = TempDir::new().unwrap();
    let root = tmp.path().join("repo");
    fs::create_dir_all(root.join("scripts")).unwrap();
    fs::write(root.join("scripts/pre-commit"), "#!/bin/sh\ncargo test\n").unwrap();
    fs::write(root.join("code.rs"), "fn main() {}\n").unwrap();
    assert!(Command::new("git").arg("-C").arg(&root).arg("init").arg("-q").status().unwrap().success());
    (tmp, root)
}

fn scratch(tmp: &TempDir) -> PathBuf {
    tmp.path().join("scratch")
}

#[test]
fn worktree_oid_is_stable_and_content_sensitive() {
    let (tmp, root) = repo();
    let a = worktree_oid(&root, &scratch(&tmp)).unwrap();
    let b = worktree_oid(&root, &scratch(&tmp)).unwrap();
    assert_eq!(a, b, "same worktree, same tree");
    assert_eq!(a.len(), 40, "a full object id, not a truncation");
    fs::write(root.join("code.rs"), "fn main() { let _ = 1; }\n").unwrap();
    let c = worktree_oid(&root, &scratch(&tmp)).unwrap();
    assert_ne!(a, c, "an edited worktree is a different tree");
}

#[test]
fn worktree_oid_outside_a_repo_reports_gits_voice() {
    let tmp = TempDir::new().unwrap();
    let err = worktree_oid(tmp.path(), &scratch(&tmp)).unwrap_err();
    assert!(err.to_string().contains("git add -A"), "names the failing act: {err}");
}

/// The fingerprint is the toolchain and nothing else (bl-6a84): a gate-script
/// edit leaves it alone, because the TREE already moved — see the check test.
#[test]
fn fingerprint_is_the_toolchain_alone() {
    let (tmp, root) = repo();
    let a = gate_fingerprint(&root, &scratch(&tmp), "rustc 1.0").unwrap();
    assert_eq!(a, gate_fingerprint(&root, &scratch(&tmp), "rustc 1.0").unwrap());
    assert_eq!(a.len(), 40, "a git object id, importable as a verdict key");
    let b = gate_fingerprint(&root, &scratch(&tmp), "rustc 2.0").unwrap();
    assert_ne!(a, b, "a toolchain bump is a different gate");
    fs::write(root.join("scripts/pre-commit"), "#!/bin/sh\ncargo test && leak-scan\n").unwrap();
    let c = gate_fingerprint(&root, &scratch(&tmp), "rustc 1.0").unwrap();
    assert_eq!(a, c, "the gate scripts are the tree's business, not the fingerprint's");
}

#[test]
fn store_roundtrip_absence_and_corruption() {
    let tmp = TempDir::new().unwrap();
    let territory = tmp.path().join("territory");
    assert_eq!(read(&territory, "t", "g").unwrap(), None, "absence is a miss, not an error");
    let v = Verdict { pass: true, builder: "Gushed".into() };
    write(&territory, "t", "g", &v).unwrap();
    assert_eq!(read(&territory, "t", "g").unwrap(), Some(v));
    fs::write(verdict_path(&territory, "t", "g"), "not = [toml").unwrap();
    assert!(read(&territory, "t", "g").is_err(), "a corrupt record must not half-work");
    fs::create_dir_all(verdict_path(&territory, "dir", "key")).unwrap();
    assert!(read(&territory, "dir", "key").is_err(), "a non-NotFound IO failure surfaces");
}

#[test]
fn check_sees_only_a_pass_on_the_exact_tree() {
    let (tmp, root) = repo();
    let territory = tmp.path().join("territory");
    let s = scratch(&tmp);
    assert!(!check(&root, &s, &territory, "rustc 1.0").unwrap(), "empty store misses");
    record(&root, &s, &territory, "rustc 1.0", false, "Gushed").unwrap();
    assert!(!check(&root, &s, &territory, "rustc 1.0").unwrap(), "a recorded FAIL is not a pass");
    record(&root, &s, &territory, "rustc 1.0", true, "Gushed").unwrap();
    assert!(check(&root, &s, &territory, "rustc 1.0").unwrap(), "recorded pass hits");
    assert!(!check(&root, &s, &territory, "rustc 2.0").unwrap(), "another gate misses");
    fs::write(root.join("code.rs"), "fn main() { let _ = 2; }\n").unwrap();
    assert!(!check(&root, &s, &territory, "rustc 1.0").unwrap(), "another tree misses");
    // bl-6a84: a gate that grew a step is a different tree, whatever the
    // binary knows about the repo's gate files — nothing, now.
    record(&root, &s, &territory, "rustc 1.0", true, "Gushed").unwrap();
    fs::write(root.join("scripts/pre-commit"), "#!/bin/sh\ncargo test && leak-scan\n").unwrap();
    assert!(!check(&root, &s, &territory, "rustc 1.0").unwrap(), "an edited gate script misses");
}

#[test]
fn scratch_holds_no_leftovers_after_any_call() {
    let (tmp, root) = repo();
    let s = scratch(&tmp);
    let territory = tmp.path().join("territory");
    record(&root, &s, &territory, "rustc 1.0", true, "Gushed").unwrap();
    check(&root, &s, &territory, "rustc 1.0").unwrap();
    let leftovers: Vec<_> = fs::read_dir(&s).unwrap().collect();
    assert!(leftovers.is_empty(), "scratch must be swept: {leftovers:?}");
}

/// The gate-blob scratch file is removed even when git cannot hash it — the
/// cleanup-invariant discipline the design's cleanup section demands.
#[test]
fn fingerprint_failure_still_sweeps_scratch() {
    let (tmp, root) = repo();
    let s = scratch(&tmp);
    fs::remove_dir_all(root.join(".git")).unwrap();
    fs::write(root.join(".git"), "gitdir: /nonexistent\n").unwrap();
    assert!(gate_fingerprint(&root, &s, "rustc 1.0").is_err());
    assert!(!s.join("gate-blob").exists());
}

/// Path arithmetic is visible: the key IS the filename.
#[test]
fn verdict_path_is_the_key() {
    let p = verdict_path(Path::new("/t"), "abc", "def");
    assert_eq!(p, Path::new("/t/verdicts/abc-def.toml"));
}

#[test]
fn import_adopts_a_foreign_store_file_and_refuses_malformation() {
    let tmp = TempDir::new().unwrap();
    let remote = tmp.path().join("remote");
    let local = tmp.path().join("local");
    let tree = "a".repeat(40);
    let gate = "b".repeat(40);
    let v = Verdict { pass: true, builder: "github-actions".into() };
    write(&remote, &tree, &gate, &v).unwrap();
    let file = verdict_path(&remote, &tree, &gate);
    let key = super::import(&local, &file).unwrap();
    assert_eq!(key, (tree.clone(), gate.clone()));
    assert_eq!(read(&local, &tree, &gate).unwrap(), Some(v), "the record crossed whole");
    for bad in ["short-name.toml", "nosuffix", &format!("{tree}-{gate}.json")] {
        let p = tmp.path().join(bad);
        fs::write(&p, "pass = true\nbuilder = \"x\"\n").unwrap();
        assert!(super::import(&local, &p).is_err(), "{bad} must be refused");
    }
    let corrupt = tmp.path().join(format!("{tree}-{gate}.toml"));
    fs::write(&corrupt, "not a verdict").unwrap();
    assert!(super::import(&local, &corrupt).is_err(), "a malformed body is refused");
    assert!(super::import(&local, Path::new("/")).is_err(), "no filename at all");
}