use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn init_test_repo(path: &PathBuf) {
Command::new("git")
.args(["init"])
.current_dir(path)
.output()
.expect("Failed to init git repo");
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(path)
.output()
.expect("Failed to set git user name");
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(path)
.output()
.expect("Failed to set git user email");
fs::write(path.join("README.md"), "# Test Repo\n").expect("Failed to write README");
Command::new("git")
.args(["add", "README.md"])
.current_dir(path)
.output()
.expect("Failed to git add");
Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(path)
.output()
.expect("Failed to git commit");
}
fn get_git_status(path: &PathBuf) -> String {
let output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(path)
.output()
.expect("Failed to run git status");
String::from_utf8_lossy(&output.stdout).to_string()
}
fn count_commits(path: &PathBuf) -> usize {
let output = Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.current_dir(path)
.output()
.expect("Failed to count commits");
String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.expect("Failed to parse commit count")
}
fn is_file_committed(path: &PathBuf, filename: &str) -> bool {
let output = Command::new("git")
.args(["ls-tree", "-r", "HEAD", "--name-only"])
.current_dir(path)
.output()
.expect("Failed to list files");
String::from_utf8_lossy(&output.stdout).contains(filename)
}
fn create_worktree(repo_path: &PathBuf, worktree_name: &str) -> PathBuf {
let worktree_path = repo_path.parent().unwrap().join(worktree_name);
Command::new("git")
.args([
"worktree",
"add",
"-b",
worktree_name,
worktree_path.to_str().unwrap(),
])
.current_dir(repo_path)
.output()
.expect("Failed to create worktree");
worktree_path
}
#[test]
fn test_setup_phase_modifies_worktree_not_main_repo() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let main_repo = temp_dir.path().join("main-repo");
fs::create_dir_all(&main_repo).expect("Failed to create main repo dir");
init_test_repo(&main_repo);
let main_status_before = get_git_status(&main_repo);
let main_commits_before = count_commits(&main_repo);
let worktree_path = create_worktree(&main_repo, "setup-worktree");
fs::write(
worktree_path.join("work-items.json"),
r#"{"items": [{"id": 1}]}"#,
)
.expect("Failed to write work-items.json");
Command::new("git")
.args(["add", "work-items.json"])
.current_dir(&worktree_path)
.output()
.expect("Failed to git add in worktree");
Command::new("git")
.args(["commit", "-m", "Setup phase: generate work items"])
.current_dir(&worktree_path)
.output()
.expect("Failed to git commit in worktree");
let main_status_after = get_git_status(&main_repo);
assert_eq!(
main_status_before, main_status_after,
"Main repository should have no modified files after setup phase"
);
let main_commits_after = count_commits(&main_repo);
assert_eq!(
main_commits_before, main_commits_after,
"Main repository branch should have no new commits after setup phase (commits are in worktree branch)"
);
assert!(
!is_file_committed(&main_repo, "work-items.json"),
"work-items.json should not be committed on main branch"
);
let worktree_status = get_git_status(&worktree_path);
assert_eq!(
worktree_status.trim(),
"",
"Worktree should have no uncommitted changes (changes were committed)"
);
let worktree_commits = count_commits(&worktree_path);
assert_eq!(
worktree_commits,
main_commits_before + 1,
"Worktree should have one more commit than main repo"
);
assert!(
worktree_path.join("work-items.json").exists(),
"work-items.json should exist in worktree"
);
assert!(
!main_repo.join("work-items.json").exists(),
"work-items.json should NOT exist in main repo"
);
}
#[test]
fn test_setup_phase_commits_in_worktree() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let main_repo = temp_dir.path().join("main-repo");
fs::create_dir_all(&main_repo).expect("Failed to create main repo dir");
init_test_repo(&main_repo);
let main_commits_before = count_commits(&main_repo);
let worktree_path = create_worktree(&main_repo, "setup-worktree-commits");
for i in 1..=3 {
let filename = format!("setup-file-{}.txt", i);
fs::write(
worktree_path.join(&filename),
format!("Setup content {}", i),
)
.expect("Failed to write setup file");
Command::new("git")
.args(["add", &filename])
.current_dir(&worktree_path)
.output()
.expect("Failed to git add");
Command::new("git")
.args(["commit", "-m", &format!("Setup step {}", i)])
.current_dir(&worktree_path)
.output()
.expect("Failed to git commit");
}
let main_commits_after = count_commits(&main_repo);
assert_eq!(
main_commits_before, main_commits_after,
"Main repository should have no new commits after setup phase"
);
let worktree_commits = count_commits(&worktree_path);
assert_eq!(
worktree_commits,
main_commits_before + 3,
"Worktree should have 3 more commits than main repo"
);
let main_status = get_git_status(&main_repo);
assert_eq!(
main_status.trim(),
"",
"Main repository working directory should be clean"
);
for i in 1..=3 {
let filename = format!("setup-file-{}.txt", i);
assert!(
!main_repo.join(&filename).exists(),
"{} should NOT exist in main repo",
filename
);
assert!(
worktree_path.join(&filename).exists(),
"{} should exist in worktree",
filename
);
}
}
#[tokio::test]
async fn test_setup_phase_execution_context_validation() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let main_repo = temp_dir.path().join("main-repo");
fs::create_dir_all(&main_repo).expect("Failed to create main repo dir");
let worktree_path = temp_dir.path().join("worktrees").join("session-test");
fs::create_dir_all(&worktree_path).expect("Failed to create worktree dir");
let original_dir = std::env::current_dir().expect("Failed to get current dir");
std::env::set_current_dir(&worktree_path).expect("Failed to change to worktree dir");
let current_dir = std::env::current_dir().expect("Failed to get current dir");
let canonical_current = current_dir
.canonicalize()
.expect("Failed to canonicalize current dir");
let canonical_worktree = worktree_path
.canonicalize()
.expect("Failed to canonicalize worktree path");
assert_eq!(
canonical_current, canonical_worktree,
"Current directory should match worktree path"
);
std::env::set_current_dir(original_dir).expect("Failed to restore directory");
let current_dir = std::env::current_dir().expect("Failed to get current dir");
let canonical_current = current_dir
.canonicalize()
.expect("Failed to canonicalize current dir");
let canonical_worktree = worktree_path
.canonicalize()
.expect("Failed to canonicalize worktree path");
assert_ne!(
canonical_current, canonical_worktree,
"Current directory should NOT match worktree path after restoration"
);
}
#[test]
fn test_main_repo_isolation_guarantee() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let main_repo = temp_dir.path().join("main-repo");
fs::create_dir_all(&main_repo).expect("Failed to create main repo dir");
init_test_repo(&main_repo);
let main_files_before: Vec<_> = fs::read_dir(&main_repo)
.expect("Failed to read main repo")
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect();
let main_status_before = get_git_status(&main_repo);
let main_commits_before = count_commits(&main_repo);
let worktree_path = create_worktree(&main_repo, "intensive-setup");
for i in 1..=10 {
let filename = format!("generated-{}.json", i);
fs::write(worktree_path.join(&filename), format!(r#"{{"id": {}}}"#, i))
.expect("Failed to write file");
Command::new("git")
.args(["add", &filename])
.current_dir(&worktree_path)
.output()
.expect("Failed to git add");
Command::new("git")
.args(["commit", "-m", &format!("Generate {}", filename)])
.current_dir(&worktree_path)
.output()
.expect("Failed to git commit");
}
let main_files_after: Vec<_> = fs::read_dir(&main_repo)
.expect("Failed to read main repo")
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect();
assert_eq!(
main_files_before, main_files_after,
"Main repository file list should be unchanged"
);
let main_status_after = get_git_status(&main_repo);
assert_eq!(
main_status_before, main_status_after,
"Main repository git status should be unchanged"
);
let main_commits_after = count_commits(&main_repo);
assert_eq!(
main_commits_before, main_commits_after,
"Main repository commit count should be unchanged"
);
let worktree_commits = count_commits(&worktree_path);
assert_eq!(
worktree_commits,
main_commits_before + 10,
"Worktree should have 10 additional commits"
);
}
#[test]
fn test_worktree_path_isolation() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let main_repo = temp_dir.path().join("main-repo");
fs::create_dir_all(&main_repo).expect("Failed to create main repo dir");
init_test_repo(&main_repo);
let worktree_path = create_worktree(&main_repo, "path-isolation-test");
fs::write(
worktree_path.join("worktree-only.txt"),
"This file exists only in worktree",
)
.expect("Failed to write file");
assert!(
worktree_path.join("worktree-only.txt").exists(),
"File should exist in worktree"
);
assert!(
!main_repo.join("worktree-only.txt").exists(),
"File should NOT exist in main repo"
);
fs::write(
main_repo.join("main-repo-only.txt"),
"This file exists only in main repo",
)
.expect("Failed to write file");
assert!(
main_repo.join("main-repo-only.txt").exists(),
"File should exist in main repo"
);
assert!(
!worktree_path.join("main-repo-only.txt").exists(),
"File should NOT exist in worktree (until committed and checked out)"
);
}