use std::fs;
use std::path::Path;
use std::process::Command;
fn init_git_repo(path: &Path) {
fs::create_dir_all(path).unwrap();
run_git(path, &["init", "--initial-branch=main"]);
run_git(path, &["config", "user.email", "test@example.com"]);
run_git(path, &["config", "user.name", "Test"]);
fs::write(path.join("README.md"), "# Test").unwrap();
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "initial"]);
}
fn integrate_change(path: &Path, change_id: &str) {
let change = path.join(".ito/changes").join(change_id);
fs::create_dir_all(change.join("specs/worktree")).unwrap();
fs::write(change.join(".ito.yaml"), "schema: spec-driven\n").unwrap();
fs::write(
change.join("proposal.md"),
"# Proposal\n\nCreate a guarded implementation worktree.\n",
)
.unwrap();
fs::write(
change.join("design.md"),
"# Design\n\nUse the captured authority commit as the worktree base.\n",
)
.unwrap();
fs::write(
change.join("tasks.md"),
"## Wave 1\n- **Depends On**: None\n\n### Task 1.1: Create worktree\n- **Dependencies**: None\n- **Updated At**: 2026-07-13\n- **Status**: [ ] pending\n",
)
.unwrap();
fs::write(
change.join("specs/worktree/spec.md"),
"## ADDED Requirements\n\n### Requirement: Guarded worktree\nIto SHALL create worktrees from accepted proposal history.\n\n#### Scenario: Accepted proposal\n- **WHEN** a worktree is created\n- **THEN** it uses the authority commit\n",
)
.unwrap();
let baseline = path.join(".ito/specs/worktree/spec.md");
fs::create_dir_all(baseline.parent().unwrap()).unwrap();
fs::write(
baseline,
"# Worktree\n\n## Requirements\n\n### Requirement: Existing baseline\nIto SHALL preserve main specifications in implementation worktrees.\n",
)
.unwrap();
run_git(path, &["add", ".ito"]);
run_git(path, &["commit", "-m", "integrate reviewed proposal"]);
}
fn run_git(cwd: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(cwd)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.output()
.unwrap_or_else(|e| panic!("Failed to run git {:?}: {e}", args));
assert!(
output.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
#[cfg(feature = "coordination-branch")]
fn ensure_worktree_creates_and_initializes_with_include_files() {
use ito_config::types::{
CoordinationStorage, ItoConfig, WorktreeInitConfig, WorktreeLayoutConfig, WorktreeStrategy,
};
use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
use ito_core::worktree_ensure::ensure_worktree;
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path().join("repo");
init_git_repo(&project_root);
integrate_change(&project_root, "test-change");
fs::write(project_root.join(".env"), "SECRET=test123").unwrap();
let worktrees_root = tmp.path().join("ito-worktrees");
let coordination_root = tmp.path().join("coordination");
let coordination_ito = coordination_root.join(".ito");
for dir in ["changes", "specs", "modules", "workflows", "audit"] {
fs::create_dir_all(coordination_ito.join(dir)).unwrap();
}
let mut config = ItoConfig::default();
config.changes.proposal.integration_mode =
ito_config::types::ProposalIntegrationMode::DirectMerge;
config.worktrees.enabled = true;
config.worktrees.strategy = WorktreeStrategy::CheckoutSiblings;
config.changes.coordination_branch.storage = CoordinationStorage::Worktree;
config.changes.coordination_branch.worktree_path =
Some(coordination_root.to_string_lossy().to_string());
config.worktrees.layout = WorktreeLayoutConfig {
base_dir: None,
dir_name: "ito-worktrees".to_string(),
};
config.worktrees.init = WorktreeInitConfig {
include: vec![".env".to_string()],
setup: None,
};
let env = ResolvedEnv {
worktree_root: project_root.clone(),
project_root: project_root.clone(),
ito_root: project_root.join(".ito"),
git_repo_kind: GitRepoKind::NonBare,
};
let paths = ResolvedWorktreePaths {
feature: WorktreeFeature::Enabled,
strategy: WorktreeStrategy::CheckoutSiblings,
worktrees_root: Some(worktrees_root.clone()),
main_worktree_root: Some(project_root.clone()),
};
let result = ensure_worktree("test-change", &config, &env, &paths, &project_root);
let wt_path = result.unwrap();
assert_eq!(wt_path, worktrees_root.join("test-change"));
assert!(wt_path.is_dir());
let env_file = wt_path.join(".env");
assert!(env_file.exists(), ".env should be copied to worktree");
assert_eq!(fs::read_to_string(&env_file).unwrap(), "SECRET=test123");
if cfg!(unix) {
assert!(
wt_path.join(".ito/changes/test-change/.ito.yaml").is_file(),
"accepted proposal contents must remain in the Git worktree"
);
assert!(
wt_path.join(".ito/specs/worktree/spec.md").is_file(),
"main specifications must remain in the Git worktree"
);
for dir in ["modules", "workflows", "audit"] {
let path = wt_path.join(".ito").join(dir);
let target = fs::read_link(&path).unwrap_or_else(|err| {
panic!("{dir} should be a coordination symlink: {err}");
});
assert_eq!(target, coordination_ito.join(dir));
}
}
let result2 = ensure_worktree("test-change", &config, &env, &paths, &project_root);
assert_eq!(result2.unwrap(), wt_path);
}
#[test]
#[cfg(all(unix, feature = "coordination-branch"))]
fn ensure_worktree_repairs_missing_coordination_links_in_existing_worktree() {
use ito_config::types::{
CoordinationStorage, ItoConfig, WorktreeInitConfig, WorktreeLayoutConfig, WorktreeStrategy,
};
use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
use ito_core::worktree_ensure::ensure_worktree;
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path().join("repo");
init_git_repo(&project_root);
integrate_change(&project_root, "repair-test");
let worktrees_root = tmp.path().join("ito-worktrees");
let coordination_root = tmp.path().join("coordination");
let coordination_ito = coordination_root.join(".ito");
for dir in ["changes", "specs", "modules", "workflows", "audit"] {
fs::create_dir_all(coordination_ito.join(dir)).unwrap();
}
let mut config = ItoConfig::default();
config.changes.proposal.integration_mode =
ito_config::types::ProposalIntegrationMode::DirectMerge;
config.worktrees.enabled = true;
config.worktrees.strategy = WorktreeStrategy::CheckoutSiblings;
config.changes.coordination_branch.storage = CoordinationStorage::Worktree;
config.changes.coordination_branch.worktree_path =
Some(coordination_root.to_string_lossy().to_string());
config.worktrees.layout = WorktreeLayoutConfig {
base_dir: None,
dir_name: "ito-worktrees".to_string(),
};
config.worktrees.init = WorktreeInitConfig {
include: vec![],
setup: None,
};
let env = ResolvedEnv {
worktree_root: project_root.clone(),
project_root: project_root.clone(),
ito_root: project_root.join(".ito"),
git_repo_kind: GitRepoKind::NonBare,
};
let paths = ResolvedWorktreePaths {
feature: WorktreeFeature::Enabled,
strategy: WorktreeStrategy::CheckoutSiblings,
worktrees_root: Some(worktrees_root.clone()),
main_worktree_root: Some(project_root.clone()),
};
let wt_path = ensure_worktree("repair-test", &config, &env, &paths, &project_root).unwrap();
std::fs::remove_file(wt_path.join(".ito/modules")).expect("remove broken symlink");
let repaired = ensure_worktree("repair-test", &config, &env, &paths, &project_root).unwrap();
assert_eq!(repaired, wt_path);
let target =
std::fs::read_link(wt_path.join(".ito/modules")).expect("modules symlink restored");
assert_eq!(target, coordination_ito.join("modules"));
}
#[test]
fn ensure_worktree_disabled_returns_cwd() {
use ito_config::types::ItoConfig;
use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
use ito_core::worktree_ensure::ensure_worktree;
let tmp = tempfile::tempdir().unwrap();
let cwd = tmp.path();
let config = ItoConfig::default();
let env = ResolvedEnv {
worktree_root: cwd.to_path_buf(),
project_root: cwd.to_path_buf(),
ito_root: cwd.join(".ito"),
git_repo_kind: GitRepoKind::NonBare,
};
let paths = ResolvedWorktreePaths {
feature: WorktreeFeature::Disabled,
strategy: ito_config::types::WorktreeStrategy::CheckoutSubdir,
worktrees_root: None,
main_worktree_root: None,
};
let result = ensure_worktree("any-change", &config, &env, &paths, cwd);
assert_eq!(result.unwrap(), cwd.to_path_buf());
}
#[test]
fn ensure_worktree_with_setup_script() {
use ito_config::types::{
CoordinationStorage, ItoConfig, WorktreeInitConfig, WorktreeLayoutConfig,
WorktreeSetupConfig, WorktreeStrategy,
};
use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
use ito_core::worktree_ensure::ensure_worktree;
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path().join("repo");
init_git_repo(&project_root);
integrate_change(&project_root, "setup-test");
let worktrees_root = tmp.path().join("ito-worktrees");
let mut config = ItoConfig::default();
config.changes.proposal.integration_mode =
ito_config::types::ProposalIntegrationMode::DirectMerge;
config.worktrees.enabled = true;
config.worktrees.strategy = WorktreeStrategy::CheckoutSiblings;
config.changes.coordination_branch.storage = CoordinationStorage::Embedded;
config.worktrees.layout = WorktreeLayoutConfig {
base_dir: None,
dir_name: "ito-worktrees".to_string(),
};
config.worktrees.init = WorktreeInitConfig {
include: vec![],
setup: Some(WorktreeSetupConfig::Single(
"touch .setup-complete".to_string(),
)),
};
let env = ResolvedEnv {
worktree_root: project_root.clone(),
project_root: project_root.clone(),
ito_root: project_root.join(".ito"),
git_repo_kind: GitRepoKind::NonBare,
};
let paths = ResolvedWorktreePaths {
feature: WorktreeFeature::Enabled,
strategy: WorktreeStrategy::CheckoutSiblings,
worktrees_root: Some(worktrees_root.clone()),
main_worktree_root: Some(project_root.clone()),
};
let result = ensure_worktree("setup-test", &config, &env, &paths, &project_root);
let wt_path = result.unwrap();
assert!(
wt_path.join(".setup-complete").exists(),
"Setup command should have created .setup-complete in the worktree"
);
}