ai-dispatch 8.99.1

Multi-AI CLI team orchestrator
// Worktree tests for reuse/prune behavior. Exports test helpers and validates Cargo sync deps.
// Purpose: Guard worktree creation when branches already exist. Deps: tempfile, std.
use super::*;
use crate::test_subprocess;
use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use tempfile::TempDir;

fn git(repo_dir: &Path, args: &[&str]) {
    let repo_dir = repo_dir.to_string_lossy().to_string();
    assert!(Command::new("git")
        .args(["-C", repo_dir.as_str()])
        .args(args)
        .status()
        .unwrap()
        .success());
}

fn git_output(repo_dir: &Path, args: &[&str]) -> String {
    let repo_dir = repo_dir.to_string_lossy().to_string();
    let out = Command::new("git")
        .args(["-C", repo_dir.as_str()])
        .args(args)
        .output()
        .unwrap();
    assert!(out.status.success());
    String::from_utf8(out.stdout).unwrap()
}

fn unique_branch(prefix: &str) -> String {
    format!(
        "{prefix}-{}-{}",
        std::process::id(),
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    )
}

#[test]
fn validate_git_repo_fails_on_nonrepo() {
    assert!(validate_git_repo(Path::new("/tmp")).is_err());
}

#[test]
fn validate_git_repo_succeeds_on_real_repo() {
    assert!(validate_git_repo(Path::new(env!("CARGO_MANIFEST_DIR"))).is_ok());
}

#[test]
fn create_worktree_rejects_invalid_branch_name() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);

    let err = create_worktree(repo.path(), "../escape", None).unwrap_err();
    assert!(err.to_string().contains("Invalid branch name"));
}

#[test]
fn create_worktree_with_base_branch_inherits_base_content() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("base.txt"), "main\n").unwrap();
    git(repo.path(), &["add", "base.txt"]);
    git(repo.path(), &["commit", "-m", "init"]);

    let base_branch = unique_branch("base");
    git(repo.path(), &["checkout", "-b", base_branch.as_str()]);
    std::fs::write(repo.path().join("inherited.txt"), "from base\n").unwrap();
    git(repo.path(), &["add", "inherited.txt"]);
    git(repo.path(), &["commit", "-m", "base"]);
    git(repo.path(), &["checkout", "main"]);

    let retry_branch = unique_branch("retry");
    let info = create_worktree(
        repo.path(),
        retry_branch.as_str(),
        Some(base_branch.as_str()),
    )
    .unwrap();

    assert_eq!(
        std::fs::read_to_string(info.path.join("inherited.txt")).unwrap(),
        "from base\n"
    );
    git(
        repo.path(),
        &[
            "worktree",
            "remove",
            "--force",
            &info.path.to_string_lossy(),
        ],
    );
}

#[test]
fn create_worktree_syncs_cargo_lock() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("file.txt"), "hello\n").unwrap();
    std::fs::write(repo.path().join("Cargo.lock"), "# lock content\n").unwrap();
    git(repo.path(), &["add", "."]);
    git(repo.path(), &["commit", "-m", "init"]);

    let branch = unique_branch("feat/cargo-lock-test");
    let info = create_worktree(repo.path(), branch.as_str(), None).unwrap();
    assert!(info.path.join("Cargo.lock").exists());
    std::fs::write(repo.path().join("Cargo.lock"), "# updated lock\n").unwrap();
    let info = create_worktree(repo.path(), branch.as_str(), None).unwrap();
    assert_eq!(
        std::fs::read_to_string(info.path.join("Cargo.lock")).unwrap(),
        "# updated lock\n"
    );

    git(
        repo.path(),
        &[
            "worktree",
            "remove",
            "--force",
            &info.path.to_string_lossy(),
        ],
    );
}

#[test]
fn sync_context_files_copies_missing_files() {
    let repo = TempDir::new().unwrap(); let wt = TempDir::new().unwrap();
    std::fs::write(repo.path().join("new_file.rs"), "fn main() {}\n").unwrap();
    let synced = sync_context_files_into_worktree(repo.path(), wt.path(), &["new_file.rs".to_string()]);
    assert_eq!(synced, vec!["new_file.rs"]);
    assert_eq!(std::fs::read_to_string(wt.path().join("new_file.rs")).unwrap(), "fn main() {}\n");
}

#[test]
fn sync_context_files_skips_existing_files() {
    let repo = TempDir::new().unwrap(); let wt = TempDir::new().unwrap();
    std::fs::write(repo.path().join("existing.rs"), "original\n").unwrap();
    std::fs::write(wt.path().join("existing.rs"), "modified\n").unwrap();
    let synced = sync_context_files_into_worktree(repo.path(), wt.path(), &["existing.rs".to_string()]);
    assert!(synced.is_empty());
    assert_eq!(std::fs::read_to_string(wt.path().join("existing.rs")).unwrap(), "modified\n");
}

#[test]
fn create_worktree_reuses_existing_branch_worktree() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("file.txt"), "hello\n").unwrap();
    git(repo.path(), &["add", "."]);
    git(repo.path(), &["commit", "-m", "init"]);

    let branch = unique_branch("feat/reuse");
    let existing_root = TempDir::new().unwrap();
    let existing_path = existing_root.path().join("worktree");
    git(
        repo.path(),
        &[
            "worktree",
            "add",
            "-b",
            branch.as_str(),
            existing_path.to_str().unwrap(),
        ],
    );

    let info = create_worktree(repo.path(), branch.as_str(), None).unwrap();
    // Canonicalize to handle macOS /var → /private/var symlink
    let actual = info.path.canonicalize().unwrap_or(info.path.clone());
    let expected = existing_path
        .canonicalize()
        .unwrap_or(existing_path.clone());
    assert_eq!(actual, expected);

    git(
        repo.path(),
        &[
            "worktree",
            "remove",
            "--force",
            &existing_path.to_string_lossy(),
        ],
    );
}

#[test]
fn create_worktree_cleans_stale_directory_and_recreates_worktree() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("file.txt"), "hello\n").unwrap();
    git(repo.path(), &["add", "."]);
    git(repo.path(), &["commit", "-m", "init"]);

    let branch = unique_branch("feat/stale");
    let expected_path = aid_worktree_path(repo.path(), &branch);
    std::fs::create_dir_all(&expected_path).unwrap();
    std::fs::write(expected_path.join("stale.txt"), "stale\n").unwrap();

    let info = create_worktree(repo.path(), branch.as_str(), None).unwrap();
    assert_eq!(info.path, expected_path);
    assert!(is_valid_git_worktree(repo.path(), &info.path).unwrap());
    assert!(!info.path.join("stale.txt").exists());

    git(
        repo.path(),
        &[
            "worktree",
            "remove",
            "--force",
            &info.path.to_string_lossy(),
        ],
    );
}

#[test]
fn create_worktree_prunes_conflicting_branch_and_recreates_worktree() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("file.txt"), "hello\n").unwrap();
    git(repo.path(), &["add", "."]);
    git(repo.path(), &["commit", "-m", "init"]);

    let branch = unique_branch("feat/orphan");
    let orphan_root = TempDir::new().unwrap();
    let orphan_path = orphan_root.path().join("worktree");
    git(
        repo.path(),
        &[
            "worktree",
            "add",
            "-b",
            branch.as_str(),
            orphan_path.to_str().unwrap(),
        ],
    );
    std::fs::remove_dir_all(&orphan_path).unwrap();

    let info = create_worktree(repo.path(), branch.as_str(), None).unwrap();
    let expected_path = aid_worktree_path(repo.path(), &branch);
    assert_eq!(info.path, expected_path);
    assert!(is_valid_git_worktree(repo.path(), &info.path).unwrap());
    let worktrees = git_output(repo.path(), &["worktree", "list", "--porcelain"]);
    assert!(worktrees.contains(expected_path.to_string_lossy().as_ref()));
    assert!(!worktrees.contains(orphan_path.to_string_lossy().as_ref()));

    git(
        repo.path(),
        &[
            "worktree",
            "remove",
            "--force",
            &info.path.to_string_lossy(),
        ],
    );
}

#[test]
fn worktree_changed_files_reports_committed_files() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("base.txt"), "main").unwrap();
    git(repo.path(), &["add", "base.txt"]);
    git(repo.path(), &["commit", "-m", "base"]);
    git(repo.path(), &["checkout", "-b", "agent-branch"]);

    std::fs::write(repo.path().join("agent.txt"), "one").unwrap();
    git(repo.path(), &["add", "agent.txt"]);
    git(repo.path(), &["commit", "-m", "agent one"]);

    std::fs::write(repo.path().join("agent2.txt"), "two").unwrap();
    git(repo.path(), &["add", "agent2.txt"]);
    git(repo.path(), &["commit", "-m", "agent two"]);

    let files = worktree_changed_files(repo.path()).unwrap();
    assert!(files.contains(&"agent.txt".to_string()));
    assert!(files.contains(&"agent2.txt".to_string()));
}

#[test]
fn create_worktree_rejects_non_aid_branch_on_force_reset_fallback() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("file.txt"), "hello\n").unwrap();
    git(repo.path(), &["add", "file.txt"]);
    git(repo.path(), &["commit", "-m", "init"]);

    let branch = unique_branch("legacy");
    git(repo.path(), &["branch", branch.as_str()]);

    let err = create_worktree(repo.path(), branch.as_str(), None).unwrap_err();
    assert!(err.to_string().contains("Refusing to force-reset branch"));
}

#[test]
fn create_worktree_allows_aid_branch_on_force_reset_fallback() {
    let _permit = test_subprocess::acquire();
    let repo = TempDir::new().unwrap();
    git(repo.path(), &["init", "-b", "main"]);
    git(repo.path(), &["config", "user.email", "test@example.com"]);
    git(repo.path(), &["config", "user.name", "Test User"]);
    std::fs::write(repo.path().join("file.txt"), "hello\n").unwrap();
    git(repo.path(), &["add", "file.txt"]);
    git(repo.path(), &["commit", "-m", "init"]);

    let branch = unique_branch("feat/reset");
    git(repo.path(), &["branch", branch.as_str()]);

    let info = create_worktree(repo.path(), branch.as_str(), None).unwrap();
    assert!(info.path.exists());
    git(
        repo.path(),
        &[
            "worktree",
            "remove",
            "--force",
            &info.path.to_string_lossy(),
        ],
    );
}

#[test]
fn worktree_lock_write_and_read() {
    let dir = TempDir::new().unwrap();
    assert!(check_worktree_lock(dir.path()).is_none());

    write_worktree_lock(dir.path(), "t-1234");
    let holder = check_worktree_lock(dir.path());
    assert_eq!(holder.as_deref(), Some("t-1234"));

    clear_worktree_lock(dir.path());
    assert!(check_worktree_lock(dir.path()).is_none());
}

#[test]
fn worktree_lock_stale_pid_is_cleared() {
    let dir = TempDir::new().unwrap();
    let lock_path = dir.path().join(".aid-lock");
    // Write a lock with a PID that definitely doesn't exist
    std::fs::write(&lock_path, "task=t-stale\npid=999999999\n").unwrap();

    // Should return None because the PID is dead
    assert!(check_worktree_lock(dir.path()).is_none());
    // Lock file should have been cleaned up
    assert!(!lock_path.exists());
}