#[cfg(unix)]
mod symlink_tests {
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
use ito_core::change_repository::FsChangeRepository;
use ito_core::module_repository::FsModuleRepository;
use ito_core::task_repository::FsTaskRepository;
use ito_domain::tasks::TaskRepository as DomainTaskRepository;
struct CoordLayout {
_tmp: TempDir,
project_ito: PathBuf,
worktree_ito: PathBuf,
}
fn setup_coord_layout() -> CoordLayout {
let tmp = TempDir::new().expect("tempdir should be created");
let project_ito = tmp.path().join("project").join(".ito");
let worktree_ito = tmp.path().join("worktree").join(".ito");
for dir in ["changes", "modules", "specs"] {
fs::create_dir_all(worktree_ito.join(dir))
.expect("worktree subdirectory should be created");
}
fs::create_dir_all(&project_ito).expect("project .ito should be created");
fs::write(project_ito.join("config.json"), r#"{"version": 1}"#)
.expect("config.json should be written");
for dir in ["changes", "modules", "specs"] {
let link = project_ito.join(dir);
let target = worktree_ito.join(dir);
symlink(&target, &link).expect("symlink should be created");
}
CoordLayout {
_tmp: tmp,
project_ito,
worktree_ito,
}
}
fn create_change_dir(parent_changes_dir: &Path, id: &str) {
let change_dir = parent_changes_dir.join(id);
fs::create_dir_all(&change_dir).expect("change directory should be created");
fs::write(change_dir.join("proposal.md"), "# Proposal\n")
.expect("proposal.md should be written");
}
fn create_module_dir(parent_modules_dir: &Path, id: &str, name: &str) {
let module_dir = parent_modules_dir.join(format!("{id}_{name}"));
fs::create_dir_all(&module_dir).expect("module directory should be created");
}
#[test]
fn change_repo_exists_through_symlink() {
let layout = setup_coord_layout();
create_change_dir(&layout.worktree_ito.join("changes"), "001-01_symlink-test");
let repo = FsChangeRepository::new(&layout.project_ito);
assert!(
repo.exists("001-01_symlink-test"),
"change written to worktree should be visible through project symlink"
);
}
#[test]
fn change_repo_list_through_symlink() {
let layout = setup_coord_layout();
create_change_dir(&layout.worktree_ito.join("changes"), "002-03_list-via-link");
let repo = FsChangeRepository::new(&layout.project_ito);
let summaries = repo.list().expect("list should succeed through symlink");
let ids: Vec<&str> = summaries.iter().map(|s| s.id.as_str()).collect();
assert_eq!(
ids,
vec!["002-03_list-via-link"],
"list should return the change written to the worktree"
);
}
#[test]
fn change_repo_list_multiple_through_symlink() {
let layout = setup_coord_layout();
let changes_dir = layout.worktree_ito.join("changes");
create_change_dir(&changes_dir, "003-01_alpha");
create_change_dir(&changes_dir, "003-02_beta");
create_change_dir(&changes_dir, "003-03_gamma");
let repo = FsChangeRepository::new(&layout.project_ito);
let summaries = repo.list().expect("list should succeed");
let mut ids: Vec<String> = summaries.into_iter().map(|s| s.id).collect();
ids.sort();
assert_eq!(
ids,
vec!["003-01_alpha", "003-02_beta", "003-03_gamma"],
"all three changes should be visible through the symlink"
);
}
#[test]
fn change_repo_get_through_symlink() {
let layout = setup_coord_layout();
create_change_dir(&layout.worktree_ito.join("changes"), "004-01_get-test");
let repo = FsChangeRepository::new(&layout.project_ito);
let change = repo
.get("004-01_get-test")
.expect("get should succeed through symlink");
assert_eq!(change.id, "004-01_get-test");
assert!(
change.proposal.is_some(),
"proposal.md should be readable through symlink"
);
}
#[test]
fn change_written_through_symlink_lands_in_worktree() {
let layout = setup_coord_layout();
let via_link = layout
.project_ito
.join("changes")
.join("005-01_written-via-link");
fs::create_dir_all(&via_link).expect("create via symlink should succeed");
fs::write(via_link.join("proposal.md"), "# Via link\n")
.expect("write via symlink should succeed");
let in_worktree = layout
.worktree_ito
.join("changes")
.join("005-01_written-via-link")
.join("proposal.md");
assert!(
in_worktree.exists(),
"file written through symlink should physically reside in the worktree"
);
let repo = FsChangeRepository::new(&layout.project_ito);
assert!(repo.exists("005-01_written-via-link"));
}
#[test]
fn module_repo_exists_through_symlink() {
let layout = setup_coord_layout();
create_module_dir(&layout.worktree_ito.join("modules"), "010", "core-engine");
let repo = FsModuleRepository::new(&layout.project_ito);
assert!(
repo.exists("010"),
"module written to worktree should be visible through project symlink"
);
}
#[test]
fn module_repo_list_through_symlink() {
let layout = setup_coord_layout();
create_module_dir(&layout.worktree_ito.join("modules"), "011", "auth");
let repo = FsModuleRepository::new(&layout.project_ito);
let summaries = repo.list().expect("list should succeed through symlink");
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].id, "011");
assert_eq!(summaries[0].name, "auth");
}
#[test]
fn module_repo_get_through_symlink() {
let layout = setup_coord_layout();
create_module_dir(&layout.worktree_ito.join("modules"), "012", "payments");
let repo = FsModuleRepository::new(&layout.project_ito);
let module = repo.get("012").expect("get should succeed through symlink");
assert_eq!(module.id, "012");
assert_eq!(module.name, "payments");
}
#[test]
fn module_repo_list_multiple_through_symlink() {
let layout = setup_coord_layout();
let modules_dir = layout.worktree_ito.join("modules");
create_module_dir(&modules_dir, "020", "billing");
create_module_dir(&modules_dir, "021", "reporting");
create_module_dir(&modules_dir, "022", "notifications");
let repo = FsModuleRepository::new(&layout.project_ito);
let summaries = repo.list().expect("list should succeed");
let ids: Vec<&str> = summaries.iter().map(|s| s.id.as_str()).collect();
assert_eq!(
ids,
vec!["020", "021", "022"],
"all modules should be visible and sorted by id"
);
}
#[test]
fn module_repo_change_counts_through_symlink() {
let layout = setup_coord_layout();
create_module_dir(&layout.worktree_ito.join("modules"), "030", "search");
let changes_dir = layout.worktree_ito.join("changes");
create_change_dir(&changes_dir, "030-01_index-rebuild");
create_change_dir(&changes_dir, "030-02_query-cache");
let repo = FsModuleRepository::new(&layout.project_ito);
let summaries = repo.list().expect("list should succeed");
let module = summaries
.iter()
.find(|m| m.id == "030")
.expect("module 030 should be present");
assert_eq!(
module.change_count, 2,
"change count should reflect changes in the worktree"
);
}
#[test]
fn task_repo_load_tasks_through_symlink() {
let layout = setup_coord_layout();
let change_id = "040-01_task-symlink";
let change_dir = layout.worktree_ito.join("changes").join(change_id);
fs::create_dir_all(&change_dir).expect("change dir should be created");
fs::write(
change_dir.join("tasks.md"),
"# Tasks\n- [x] Done task\n- [ ] Pending task\n",
)
.expect("tasks.md should be written");
let repo = FsTaskRepository::new(&layout.project_ito);
let (completed, total) = repo
.get_task_counts(change_id)
.expect("get_task_counts should succeed through symlink");
assert_eq!(completed, 1, "one task should be complete");
assert_eq!(total, 2, "two tasks total");
}
#[test]
fn task_repo_has_tasks_through_symlink() {
let layout = setup_coord_layout();
let change_id = "041-01_has-tasks-check";
let change_dir = layout.worktree_ito.join("changes").join(change_id);
fs::create_dir_all(&change_dir).expect("change dir should be created");
fs::write(change_dir.join("tasks.md"), "# Tasks\n- [ ] One task\n")
.expect("tasks.md should be written");
let repo = FsTaskRepository::new(&layout.project_ito);
assert!(
repo.has_tasks(change_id)
.expect("has_tasks should succeed through symlink"),
"has_tasks should return true for a change with tasks in the worktree"
);
}
#[test]
fn task_written_through_symlink_lands_in_worktree() {
let layout = setup_coord_layout();
let change_id = "042-01_write-tasks-via-link";
let via_link = layout.project_ito.join("changes").join(change_id);
fs::create_dir_all(&via_link).expect("create via symlink should succeed");
fs::write(
via_link.join("tasks.md"),
"# Tasks\n- [x] Task A\n- [x] Task B\n- [ ] Task C\n",
)
.expect("write via symlink should succeed");
let in_worktree = layout
.worktree_ito
.join("changes")
.join(change_id)
.join("tasks.md");
assert!(
in_worktree.exists(),
"tasks.md written through symlink should physically reside in the worktree"
);
let repo = FsTaskRepository::new(&layout.project_ito);
let (completed, total) = repo
.get_task_counts(change_id)
.expect("get_task_counts should succeed");
assert_eq!(completed, 2);
assert_eq!(total, 3);
}
#[test]
fn task_repo_missing_tasks_file_returns_zero_through_symlink() {
let layout = setup_coord_layout();
let change_id = "043-01_no-tasks";
let change_dir = layout.worktree_ito.join("changes").join(change_id);
fs::create_dir_all(&change_dir).expect("change dir should be created");
let repo = FsTaskRepository::new(&layout.project_ito);
let (completed, total) = repo
.get_task_counts(change_id)
.expect("get_task_counts should succeed even without tasks.md");
assert_eq!(completed, 0);
assert_eq!(total, 0);
}
#[test]
fn all_repos_consistent_through_symlinks() {
let layout = setup_coord_layout();
create_module_dir(&layout.worktree_ito.join("modules"), "050", "integration");
let change_id = "050-01_full-stack-test";
let change_dir = layout.worktree_ito.join("changes").join(change_id);
fs::create_dir_all(&change_dir).expect("change dir should be created");
fs::write(change_dir.join("proposal.md"), "# Full stack\n")
.expect("proposal.md should be written");
fs::write(
change_dir.join("tasks.md"),
"# Tasks\n- [x] Step 1\n- [ ] Step 2\n- [ ] Step 3\n",
)
.expect("tasks.md should be written");
let change_repo = FsChangeRepository::new(&layout.project_ito);
let module_repo = FsModuleRepository::new(&layout.project_ito);
let task_repo = FsTaskRepository::new(&layout.project_ito);
assert!(change_repo.exists(change_id));
let change = change_repo.get(change_id).expect("get change");
assert_eq!(change.id, change_id);
assert!(change.proposal.is_some());
assert!(module_repo.exists("050"));
let modules = module_repo.list().expect("list modules");
let module = modules.iter().find(|m| m.id == "050").expect("module 050");
assert_eq!(module.change_count, 1);
let (completed, total) = task_repo
.get_task_counts(change_id)
.expect("get task counts");
assert_eq!(completed, 1);
assert_eq!(total, 3);
}
}