use std::path::Path;
use std::process::{Command, Output};
use regex::Regex;
use tempfile::TempDir;
fn strip_ansi(s: &str) -> String {
let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
re.replace_all(s, "").to_string()
}
fn create_origin_repo() -> TempDir {
let dir = TempDir::new().expect("Failed to create temp dir");
run_git(dir.path(), &["init", "--bare", "--initial-branch=main"]);
dir
}
fn create_local_repo(origin_path: &Path) -> TempDir {
let dir = TempDir::new().expect("Failed to create temp dir");
run_git(dir.path(), &["init"]);
run_git(dir.path(), &["config", "user.email", "test@example.com"]);
run_git(dir.path(), &["config", "user.name", "Test User"]);
run_git(dir.path(), &["checkout", "-b", "main"]);
std::fs::write(dir.path().join("README.md"), "# Test").expect("Failed to write file");
run_git(dir.path(), &["add", "."]);
run_git(dir.path(), &["commit", "-m", "Initial commit"]);
let origin_url = format!("file://{}", origin_path.display());
run_git(dir.path(), &["remote", "add", "origin", &origin_url]);
run_git(dir.path(), &["push", "-u", "origin", "main"]);
dir
}
fn run_git(dir: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.output()
.expect("Failed to run git command");
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
panic!("git {} failed: {}", args.join(" "), stderr);
}
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn run_gw(dir: &Path, args: &[&str]) -> Output {
let gw_path = env!("CARGO_BIN_EXE_gw");
Command::new(gw_path)
.args(args)
.current_dir(dir)
.env("NO_COLOR", "1")
.output()
.expect("Failed to run gw command")
}
fn stdout_str(output: &Output) -> String {
strip_ansi(&String::from_utf8_lossy(&output.stdout))
}
fn stderr_str(output: &Output) -> String {
strip_ansi(&String::from_utf8_lossy(&output.stderr))
}
fn assert_success(output: &Output, context: &str) {
assert!(
output.status.success(),
"{context} failed (exit {}):\nstdout: {}\nstderr: {}",
output.status.code().unwrap_or(-1),
stdout_str(output),
stderr_str(output),
);
}
fn leader_name_for(path: &Path) -> String {
let raw = path.file_name().unwrap().to_string_lossy().to_string();
raw.trim_start_matches('.').to_string()
}
#[test]
fn test_warm_creates_worktrees() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let leader = leader_name_for(local.path());
let prefix = format!("{leader}-pool-");
let output = run_gw(local.path(), &["worktree", "pool", "warm", "3"]);
assert_success(&output, "warm 3");
let out = stdout_str(&output);
assert!(
out.contains(&format!("Created {prefix}001")),
"output: {out}"
);
assert!(
out.contains(&format!("Created {prefix}002")),
"output: {out}"
);
assert!(
out.contains(&format!("Created {prefix}003")),
"output: {out}"
);
assert!(
out.contains("3 created, 3 available, 3 total"),
"output: {out}"
);
assert!(
local
.path()
.join(format!(".worktrees/{prefix}001"))
.exists()
);
assert!(
local
.path()
.join(format!(".worktrees/{prefix}002"))
.exists()
);
assert!(
local
.path()
.join(format!(".worktrees/{prefix}003"))
.exists()
);
let branches = run_git(local.path(), &["branch", "--list", &format!("{prefix}*")]);
assert!(
branches.contains(&format!("{prefix}001")),
"branches: {branches}"
);
assert!(
branches.contains(&format!("{prefix}002")),
"branches: {branches}"
);
assert!(
branches.contains(&format!("{prefix}003")),
"branches: {branches}"
);
let exclude = std::fs::read_to_string(local.path().join(".git/info/exclude"))
.expect(".git/info/exclude should exist");
assert!(
exclude.contains(".worktrees/"),
".git/info/exclude should contain .worktrees/: {exclude}"
);
let gitignore = std::fs::read_to_string(local.path().join(".gitignore")).unwrap_or_default();
assert!(
!gitignore.contains(".worktrees/"),
".gitignore should not contain .worktrees/: {gitignore}"
);
}
#[test]
fn test_warm_exclude_idempotent() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
let exclude = std::fs::read_to_string(local.path().join(".git/info/exclude"))
.expect(".git/info/exclude should exist");
let count = exclude
.lines()
.filter(|l| l.trim() == ".worktrees/")
.count();
assert_eq!(count, 1, ".worktrees/ should appear once: {exclude}");
}
#[test]
fn test_warm_is_idempotent() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let output = run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
assert_success(&output, "warm 2");
let output = run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
assert_success(&output, "warm 2 (idempotent)");
let out = stdout_str(&output);
assert!(out.contains("already has 2 available"), "output: {out}");
}
#[test]
fn test_warm_incremental() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let output = run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
assert_success(&output, "warm 1");
let output = run_gw(local.path(), &["worktree", "pool", "warm", "3"]);
assert_success(&output, "warm 3");
let out = stdout_str(&output);
assert!(
out.contains("2 created, 3 available, 3 total"),
"output: {out}"
);
}
#[test]
fn test_acquire_prints_path_to_stdout() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let leader = leader_name_for(local.path());
let prefix = format!("{leader}-pool-");
run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
let output = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert_success(&output, "acquire");
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
let path_normalized = path.replace('\\', "/");
assert!(
path_normalized.ends_with(&format!(".worktrees/{prefix}001")),
"Expected worktree path, got: {path}"
);
assert!(
Path::new(&path).exists(),
"Acquired path should exist: {path}"
);
let err = stderr_str(&output);
assert!(
err.contains(&format!("Acquired {prefix}001")),
"stderr: {err}"
);
assert!(err.contains("0 remaining"), "stderr: {err}");
}
#[test]
fn test_acquire_fails_when_exhausted() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
let output = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert_success(&output, "acquire 1");
let output = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert!(
!output.status.success(),
"Expected acquire to fail when pool is exhausted"
);
let err = stderr_str(&output);
assert!(err.contains("No available worktrees"), "stderr: {err}");
}
#[test]
fn test_acquire_fails_when_not_initialized() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let output = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert!(!output.status.success());
let err = stderr_str(&output);
assert!(err.contains("not initialized"), "stderr: {err}");
}
#[test]
fn test_status_shows_pool_info() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let leader = leader_name_for(local.path());
let prefix = format!("{leader}-pool-");
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
assert_success(&output, "status");
let out = stdout_str(&output);
assert!(out.contains("1 available"), "output: {out}");
assert!(out.contains("1 acquired"), "output: {out}");
assert!(out.contains("2 total"), "output: {out}");
assert!(out.contains(&format!("{prefix}001")), "output: {out}");
assert!(
out.contains("BRANCH"),
"output should have BRANCH column: {out}"
);
}
#[test]
fn test_status_verbose_shows_all() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let leader = leader_name_for(local.path());
let prefix = format!("{leader}-pool-");
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
let output = run_gw(local.path(), &["worktree", "pool", "status", "-v"]);
assert_success(&output, "status -v");
let out = stdout_str(&output);
assert!(out.contains(&format!("{prefix}001")), "output: {out}");
assert!(out.contains(&format!("{prefix}002")), "output: {out}");
}
#[test]
fn test_status_fails_when_not_initialized() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
assert!(!output.status.success());
}
#[test]
fn test_drain_removes_all_worktrees() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let leader = leader_name_for(local.path());
let prefix = format!("{leader}-pool-");
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
let output = run_gw(local.path(), &["worktree", "pool", "drain"]);
assert_success(&output, "drain");
let out = stdout_str(&output);
assert!(out.contains("Drained 2 worktree(s)"), "output: {out}");
assert!(
!local
.path()
.join(format!(".worktrees/{prefix}001"))
.exists()
);
assert!(
!local
.path()
.join(format!(".worktrees/{prefix}002"))
.exists()
);
let branches = run_git(local.path(), &["branch", "--list", &format!("{prefix}*")]);
assert!(branches.is_empty(), "branches still exist: {branches}");
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
assert!(!output.status.success());
}
#[test]
fn test_drain_refuses_with_acquired_worktrees() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
let output = run_gw(local.path(), &["worktree", "pool", "drain"]);
assert!(
!output.status.success(),
"Expected drain to fail with acquired worktrees"
);
let err = stderr_str(&output);
assert!(err.contains("acquired worktree"), "stderr: {err}");
}
#[test]
fn test_drain_force_with_acquired_worktrees() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
let output = run_gw(local.path(), &["worktree", "pool", "drain", "--force"]);
assert_success(&output, "drain --force");
let out = stdout_str(&output);
assert!(out.contains("Drained 2 worktree(s)"), "output: {out}");
}
#[test]
fn test_drain_then_warm_again() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
run_gw(local.path(), &["worktree", "pool", "drain"]);
let output = run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
assert_success(&output, "re-warm after drain");
let out = stdout_str(&output);
assert!(out.contains("1 created"), "output: {out}");
}
#[test]
fn test_release_returns_worktree_to_pool() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
let out = stdout_str(&output);
assert!(out.contains("1 available"), "before release: {out}");
assert!(out.contains("1 acquired"), "before release: {out}");
let output = run_gw(local.path(), &["worktree", "pool", "release"]);
assert_success(&output, "release");
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
let out = stdout_str(&output);
assert!(out.contains("2 available"), "after release: {out}");
assert!(out.contains("0 acquired"), "after release: {out}");
}
#[test]
fn test_release_by_name() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let leader = leader_name_for(local.path());
let prefix = format!("{leader}-pool-");
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
let name = format!("{prefix}001");
let output = run_gw(local.path(), &["worktree", "pool", "release", &name]);
assert_success(&output, "release by name");
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
let out = stdout_str(&output);
assert!(out.contains("1 available"), "after release by name: {out}");
assert!(out.contains("1 acquired"), "after release by name: {out}");
}
#[test]
fn test_release_all() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "3"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
run_gw(local.path(), &["worktree", "pool", "acquire"]);
let output = run_gw(local.path(), &["worktree", "pool", "release"]);
assert_success(&output, "release all");
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
let out = stdout_str(&output);
assert!(out.contains("3 available"), "after release all: {out}");
assert!(out.contains("0 acquired"), "after release all: {out}");
}
#[test]
fn test_release_fails_when_none_acquired() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
let output = run_gw(local.path(), &["worktree", "pool", "release"]);
assert!(
!output.status.success(),
"Expected release to fail when none acquired"
);
let err = stderr_str(&output);
assert!(
err.contains("No acquired worktrees to release"),
"stderr: {err}"
);
}
#[test]
fn test_full_pool_lifecycle() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
let output = run_gw(local.path(), &["worktree", "pool", "warm", "3"]);
assert_success(&output, "warm");
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
assert_success(&output, "status");
let out = stdout_str(&output);
assert!(out.contains("3 available"), "output: {out}");
let output = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert_success(&output, "acquire");
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert!(Path::new(&path).is_dir());
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
let out = stdout_str(&output);
assert!(out.contains("2 available"), "output: {out}");
assert!(out.contains("1 acquired"), "output: {out}");
let output = run_gw(local.path(), &["worktree", "pool", "release"]);
assert_success(&output, "release");
let output = run_gw(local.path(), &["worktree", "pool", "status"]);
let out = stdout_str(&output);
assert!(out.contains("3 available"), "after release: {out}");
assert!(out.contains("0 acquired"), "after release: {out}");
let output = run_gw(local.path(), &["worktree", "pool", "drain"]);
assert_success(&output, "drain");
let out = stdout_str(&output);
assert!(out.contains("Drained 3 worktree(s)"), "output: {out}");
}
#[test]
fn test_acquire_drain_cycle() {
let origin = create_origin_repo();
let local = create_local_repo(origin.path());
run_gw(local.path(), &["worktree", "pool", "warm", "2"]);
let out1 = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert_success(&out1, "acquire 1");
let out2 = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert_success(&out2, "acquire 2");
let out3 = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert!(!out3.status.success(), "Expected exhaustion");
let output = run_gw(local.path(), &["worktree", "pool", "drain", "--force"]);
assert_success(&output, "drain --force");
run_gw(local.path(), &["worktree", "pool", "warm", "1"]);
let out4 = run_gw(local.path(), &["worktree", "pool", "acquire"]);
assert_success(&out4, "re-acquire after drain+warm");
}