use super::*;
use std::fs;
use tempfile::TempDir;
fn make_dirs() -> (TempDir, PathBuf, PathBuf) {
let tmp = TempDir::new().expect("tempdir");
let ito = tmp.path().join(".ito");
let worktree_ito = tmp.path().join("worktree").join(".ito");
fs::create_dir_all(&ito).unwrap();
fs::create_dir_all(&worktree_ito).unwrap();
(tmp, ito, worktree_ito)
}
#[test]
#[cfg(unix)]
fn create_dir_link_creates_symlink() {
let tmp = TempDir::new().unwrap();
let src = tmp.path().join("real_dir");
let dst = tmp.path().join("link");
fs::create_dir_all(&src).unwrap();
create_dir_link(&src, &dst).expect("symlink creation should succeed");
assert!(dst.exists(), "link path should resolve");
let target = fs::read_link(&dst).expect("should be a symlink");
assert_eq!(target, src);
}
#[test]
#[cfg(unix)]
fn create_dir_link_fails_when_dst_exists() {
let tmp = TempDir::new().unwrap();
let src = tmp.path().join("real_dir");
let dst = tmp.path().join("existing");
fs::create_dir_all(&src).unwrap();
fs::create_dir_all(&dst).unwrap();
let result = create_dir_link(&src, &dst);
assert!(result.is_err(), "should fail when dst already exists");
}
#[test]
#[cfg(unix)]
fn wire_creates_symlinks_for_all_dirs() {
let (_tmp, ito, worktree_ito) = make_dirs();
wire_coordination_symlinks(&ito, &worktree_ito).expect("wire should succeed");
for dir in COORDINATION_DIRS {
let link = ito.join(dir);
assert!(link.exists(), "link '{dir}' should exist");
let target = fs::read_link(&link).expect("should be a symlink");
assert_eq!(
target,
worktree_ito.join(dir),
"link '{dir}' points at wrong target"
);
}
}
#[test]
#[cfg(unix)]
fn wire_is_idempotent() {
let (_tmp, ito, worktree_ito) = make_dirs();
wire_coordination_symlinks(&ito, &worktree_ito).expect("first wire");
wire_coordination_symlinks(&ito, &worktree_ito).expect("second wire should be idempotent");
for dir in COORDINATION_DIRS {
let link = ito.join(dir);
let target = fs::read_link(&link).expect("should still be a symlink");
assert_eq!(target, worktree_ito.join(dir));
}
}
#[test]
#[cfg(unix)]
fn wire_fails_for_non_empty_real_dir() {
let (_tmp, ito, worktree_ito) = make_dirs();
let modules_dir = ito.join("modules");
fs::create_dir_all(&modules_dir).unwrap();
let sentinel = modules_dir.join("sentinel.md");
fs::write(&sentinel, "hello").unwrap();
let err = wire_coordination_symlinks(&ito, &worktree_ito).expect_err("wire should fail");
let message = err.to_string();
assert!(message.contains(&modules_dir.display().to_string()));
assert!(message.contains(&worktree_ito.join("modules").display().to_string()));
assert!(
sentinel.exists(),
"local content should not be moved implicitly"
);
assert!(
fs::read_link(&modules_dir).is_err(),
"directory should remain real"
);
}
#[test]
#[cfg(unix)]
fn wire_handles_empty_real_dir() {
let (_tmp, ito, worktree_ito) = make_dirs();
let workflows_dir = ito.join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
wire_coordination_symlinks(&ito, &worktree_ito).expect("wire should succeed");
let link = ito.join("workflows");
assert!(
fs::read_link(&link).is_ok(),
"workflows should be a symlink"
);
}
#[test]
#[cfg(unix)]
fn wire_repairs_correct_symlink_with_missing_target() {
let (_tmp, ito, worktree_ito) = make_dirs();
wire_coordination_symlinks(&ito, &worktree_ito).expect("wire");
let target = worktree_ito.join("modules");
fs::remove_dir_all(&target).unwrap();
wire_coordination_symlinks(&ito, &worktree_ito).expect("repair should succeed");
assert!(
target.is_dir(),
"missing target directory should be recreated"
);
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
assert_eq!(status, CoordinationHealthStatus::Healthy);
}
#[test]
#[cfg(unix)]
fn wire_fails_for_wrong_symlink_target() {
let (_tmp, ito, worktree_ito) = make_dirs();
let wrong_root = ito.parent().unwrap().join("other-worktree").join(".ito");
fs::create_dir_all(wrong_root.join("modules")).unwrap();
std::os::unix::fs::symlink(wrong_root.join("modules"), ito.join("modules")).unwrap();
let err = wire_coordination_symlinks(&ito, &worktree_ito).expect_err("wire should fail");
let message = err.to_string();
assert!(message.contains(&ito.join("modules").display().to_string()));
assert!(message.contains(&wrong_root.join("modules").display().to_string()));
assert!(message.contains(&worktree_ito.join("modules").display().to_string()));
assert_eq!(
fs::read_link(ito.join("modules")).unwrap(),
wrong_root.join("modules")
);
}
#[test]
#[cfg(unix)]
fn wire_preserves_real_authoritative_git_dirs() {
let (_tmp, ito, worktree_ito) = make_dirs();
for dir in AUTHORITATIVE_GIT_DIRS {
let path = ito.join(dir);
fs::create_dir_all(&path).unwrap();
fs::write(path.join("tracked.md"), format!("tracked {dir}")).unwrap();
}
wire_coordination_symlinks(&ito, &worktree_ito).expect("wire should succeed");
for dir in AUTHORITATIVE_GIT_DIRS {
let path = ito.join(dir);
assert!(path.is_dir(), "{dir} should remain a real directory");
assert!(fs::read_link(&path).is_err(), "{dir} must not be a symlink");
assert_eq!(
fs::read_to_string(path.join("tracked.md")).unwrap(),
format!("tracked {dir}")
);
assert!(
!worktree_ito.join(dir).exists(),
"coordination worktree must not provision {dir}"
);
}
}
#[test]
#[cfg(unix)]
fn wire_migrates_legacy_authority_link_without_deleting_external_content() {
let (_tmp, ito, worktree_ito) = make_dirs();
let external = worktree_ito.join("changes");
fs::create_dir_all(&external).unwrap();
fs::write(external.join("proposal.md"), "legacy proposal").unwrap();
std::os::unix::fs::symlink(&external, ito.join("changes")).unwrap();
wire_coordination_symlinks(&ito, &worktree_ito).expect("migration should succeed");
let local = ito.join("changes");
assert!(local.is_dir());
assert!(fs::read_link(&local).is_err());
assert_eq!(
fs::read_to_string(local.join("proposal.md")).unwrap(),
"legacy proposal"
);
assert_eq!(
fs::read_to_string(external.join("proposal.md")).unwrap(),
"legacy proposal",
"migration must preserve external coordination content"
);
assert!(!ito.join(".changes.main-authority-migration").exists());
assert!(!ito.join(".changes.legacy-coordination-link").exists());
}
#[test]
#[cfg(unix)]
fn wire_rejects_broken_legacy_authority_link_without_creating_empty_authority() {
let (_tmp, ito, worktree_ito) = make_dirs();
let missing = worktree_ito.join("specs");
std::os::unix::fs::symlink(&missing, ito.join("specs")).unwrap();
let error = wire_coordination_symlinks(&ito, &worktree_ito)
.expect_err("missing legacy authority must block migration");
let specs = ito.join("specs");
assert!(
fs::read_link(&specs).is_ok(),
"legacy link must remain intact"
);
assert!(
!missing.exists(),
"migration must not create the external target"
);
assert!(error.to_string().contains("missing target"));
assert!(error.to_string().contains("migrate-to-main"));
}
#[test]
fn gitignore_entries_added_when_missing() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path();
update_gitignore_for_symlinks(project_root).expect("should succeed");
let content = fs::read_to_string(project_root.join(".gitignore")).unwrap();
for dir in COORDINATION_DIRS {
assert!(content.contains(&format!(".ito/{dir}")));
}
}
#[test]
fn gitignore_no_duplicates_on_second_call() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path();
update_gitignore_for_symlinks(project_root).expect("first call");
update_gitignore_for_symlinks(project_root).expect("second call");
let content = fs::read_to_string(project_root.join(".gitignore")).unwrap();
for dir in COORDINATION_DIRS {
let entry = format!(".ito/{dir}");
let count = content.lines().filter(|l| l.trim() == entry).count();
assert_eq!(
count, 1,
".ito/{dir} should appear exactly once, found {count}"
);
}
}
#[test]
fn gitignore_preserves_existing_content() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path();
let gitignore_path = project_root.join(".gitignore");
fs::write(&gitignore_path, "target/\n*.log\n").unwrap();
update_gitignore_for_symlinks(project_root).expect("should succeed");
let content = fs::read_to_string(&gitignore_path).unwrap();
assert!(content.contains("target/"));
assert!(content.contains("*.log"));
}
#[test]
fn gitignore_removes_legacy_authority_entries() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path();
let gitignore_path = project_root.join(".gitignore");
fs::write(&gitignore_path, ".ito/changes\n.ito/specs\n").unwrap();
update_gitignore_for_symlinks(project_root).expect("should succeed");
let content = fs::read_to_string(&gitignore_path).unwrap();
assert!(!content.lines().any(|line| line.trim() == ".ito/changes"));
assert!(!content.lines().any(|line| line.trim() == ".ito/specs"));
assert!(content.contains(".ito/modules"));
assert!(content.contains(".ito/workflows"));
assert!(content.contains(".ito/audit"));
}
#[test]
fn gitignore_created_when_absent() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path();
assert!(!project_root.join(".gitignore").exists());
update_gitignore_for_symlinks(project_root).expect("should succeed");
assert!(project_root.join(".gitignore").exists());
}
#[test]
#[cfg(unix)]
fn remove_restores_real_dirs_with_content() {
let (_tmp, ito, worktree_ito) = make_dirs();
wire_coordination_symlinks(&ito, &worktree_ito).expect("wire");
let via_link = ito.join("modules").join("task.md");
fs::write(&via_link, "task content").unwrap();
remove_coordination_symlinks(&ito, &worktree_ito).expect("remove");
let modules = ito.join("modules");
assert!(modules.is_dir(), "modules should be a real directory");
assert!(
fs::read_link(&modules).is_err(),
"modules should not be a symlink"
);
let restored = modules.join("task.md");
assert!(restored.exists(), "task.md should be restored");
assert_eq!(fs::read_to_string(&restored).unwrap(), "task content");
}
#[test]
#[cfg(unix)]
fn remove_is_noop_for_real_dirs() {
let (_tmp, ito, worktree_ito) = make_dirs();
for dir in COORDINATION_DIRS {
fs::create_dir_all(ito.join(dir)).unwrap();
}
remove_coordination_symlinks(&ito, &worktree_ito).expect("remove");
for dir in COORDINATION_DIRS {
let path = ito.join(dir);
assert!(path.is_dir(), "{dir} should still be a real directory");
assert!(
fs::read_link(&path).is_err(),
"{dir} should not be a symlink"
);
}
}
#[test]
#[cfg(unix)]
fn remove_is_noop_when_dirs_absent() {
let (_tmp, ito, worktree_ito) = make_dirs();
remove_coordination_symlinks(&ito, &worktree_ito).expect("remove on empty ito dir");
}
#[test]
fn health_embedded_returns_embedded() {
let tmp = TempDir::new().unwrap();
let ito = tmp.path().join(".ito");
let worktree_ito = tmp.path().join("worktree").join(".ito");
fs::create_dir_all(&ito).unwrap();
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Embedded);
assert_eq!(status, CoordinationHealthStatus::Embedded);
}
#[test]
fn health_worktree_missing_when_dir_absent() {
let tmp = TempDir::new().unwrap();
let ito = tmp.path().join(".ito");
let worktree_ito = tmp.path().join("nonexistent").join(".ito");
fs::create_dir_all(&ito).unwrap();
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
assert_eq!(
status,
CoordinationHealthStatus::WorktreeMissing {
expected_path: worktree_ito.clone()
}
);
}
#[test]
#[cfg(unix)]
fn health_healthy_when_all_symlinks_correct() {
let (_tmp, ito, worktree_ito) = make_dirs();
wire_coordination_symlinks(&ito, &worktree_ito).expect("wire");
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
assert_eq!(status, CoordinationHealthStatus::Healthy);
}
#[test]
fn health_missing_link_is_not_wired() {
let (_tmp, ito, worktree_ito) = make_dirs();
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
let CoordinationHealthStatus::NotWired { dirs } = status else {
panic!("expected NotWired, got {status:?}");
};
assert!(dirs.contains(&ito.join("modules")));
}
#[test]
#[cfg(unix)]
fn health_broken_symlinks_when_target_missing() {
let (_tmp, ito, worktree_ito) = make_dirs();
wire_coordination_symlinks(&ito, &worktree_ito).expect("wire");
let target = worktree_ito.join("modules");
fs::remove_dir_all(&target).unwrap();
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
let CoordinationHealthStatus::BrokenSymlinks { broken } = status else {
panic!("expected BrokenSymlinks, got {status:?}");
};
assert_eq!(broken.len(), 1);
assert_eq!(broken[0].0, ito.join("modules"));
}
#[test]
#[cfg(unix)]
fn health_wrong_target_when_symlink_points_elsewhere() {
let (_tmp, ito, worktree_ito) = make_dirs();
let wrong_root = ito.parent().unwrap().join("other-worktree").join(".ito");
fs::create_dir_all(&wrong_root).unwrap();
wire_coordination_symlinks(&ito, &wrong_root).expect("wire");
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
let CoordinationHealthStatus::WrongTargets { mismatched } = status else {
panic!("expected WrongTargets, got {status:?}");
};
assert_eq!(mismatched.len(), COORDINATION_DIRS.len());
assert_eq!(mismatched[0].0, ito.join("modules"));
}
#[test]
#[cfg(unix)]
fn health_ignores_authoritative_git_dirs() {
let (_tmp, ito, worktree_ito) = make_dirs();
for dir in COORDINATION_DIRS {
fs::create_dir_all(worktree_ito.join(dir)).unwrap();
std::os::unix::fs::symlink(worktree_ito.join(dir), ito.join(dir)).unwrap();
}
fs::create_dir_all(ito.join("changes")).unwrap();
let external_specs = ito.parent().unwrap().join("legacy-specs");
fs::create_dir_all(&external_specs).unwrap();
std::os::unix::fs::symlink(&external_specs, ito.join("specs")).unwrap();
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
assert_eq!(status, CoordinationHealthStatus::Healthy);
}
#[test]
#[cfg(unix)]
fn health_not_wired_when_real_dirs_present() {
let (_tmp, ito, worktree_ito) = make_dirs();
for dir in COORDINATION_DIRS {
fs::create_dir_all(ito.join(dir)).unwrap();
}
let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
let CoordinationHealthStatus::NotWired { dirs } = status else {
panic!("expected NotWired, got {status:?}");
};
assert_eq!(dirs.len(), COORDINATION_DIRS.len());
}
#[test]
fn format_message_healthy_is_none() {
assert!(format_health_message(&CoordinationHealthStatus::Healthy).is_none());
}
#[test]
fn format_message_embedded_is_none() {
assert!(format_health_message(&CoordinationHealthStatus::Embedded).is_none());
}
#[test]
fn format_message_worktree_missing_contains_path_and_hint() {
let path = PathBuf::from("/some/path/.ito");
let msg = format_health_message(&CoordinationHealthStatus::WorktreeMissing {
expected_path: path.clone(),
})
.expect("should produce a message");
assert!(msg.contains(&path.display().to_string()));
assert!(msg.contains("ito init"));
}
#[test]
fn format_message_broken_symlinks_contains_paths_and_hint() {
let link = PathBuf::from("/project/.ito/changes");
let target = PathBuf::from("../worktree/.ito/changes");
let msg = format_health_message(&CoordinationHealthStatus::BrokenSymlinks {
broken: vec![(link.clone(), target.clone())],
})
.expect("should produce a message");
assert!(msg.contains(&link.display().to_string()));
assert!(msg.contains(&target.display().to_string()));
assert!(msg.contains("ito init"));
}
#[test]
fn format_message_not_wired_contains_dir_and_hint() {
let dir = PathBuf::from("/project/.ito/specs");
let msg = format_health_message(&CoordinationHealthStatus::NotWired {
dirs: vec![dir.clone()],
})
.expect("should produce a message");
assert!(msg.contains(&dir.display().to_string()));
assert!(msg.contains("ito init"));
}
#[test]
fn format_message_wrong_target_contains_paths_and_hint() {
let link = PathBuf::from("/project/.ito/changes");
let actual = PathBuf::from("/other/.ito/changes");
let expected = PathBuf::from("/coord/.ito/changes");
let msg = format_health_message(&CoordinationHealthStatus::WrongTargets {
mismatched: vec![(link.clone(), actual.clone(), expected.clone())],
})
.expect("should produce a message");
assert!(msg.contains(&link.display().to_string()));
assert!(msg.contains(&actual.display().to_string()));
assert!(msg.contains(&expected.display().to_string()));
assert!(msg.contains("ito init"));
}
#[test]
fn gitignore_entries_match_coordination_dirs() {
let entries = gitignore_entries();
assert_eq!(entries.len(), COORDINATION_DIRS.len());
for (entry, dir) in entries.iter().zip(COORDINATION_DIRS.iter()) {
let expected = format!(".ito/{dir}");
assert_eq!(*entry, expected.as_str());
}
}
#[test]
fn gitignore_entries_returns_static_slice() {
let a = gitignore_entries();
let b = gitignore_entries();
assert_eq!(a.as_ptr(), b.as_ptr());
}