"""Tests for dotfiles link planning.""" from pathlib import Path from typing import Optional from flow.domain.dotfiles.models import ( LinkTarget, LinkedState, ) from flow.domain.dotfiles.planning import plan_link, plan_unlink def _lt(target, source="/a", pkg="_shared/zsh", module=False, sudo=False): return LinkTarget( source=Path(source), target=Path(target), package=pkg, from_module=module, needs_sudo=sudo, ) def _fs_check_none(path: Path) -> Optional[str]: """Fake filesystem_check: nothing exists.""" return None def _fs_check_file(path: Path) -> Optional[str]: """Fake: everything is a file.""" return "file" def _no_link_target(path: Path) -> Optional[Path]: """Default fake link_target: no symlink anywhere.""" return None class TestPlanLink: def test_new_target_creates_link(self): desired = [_lt("/home/x/.zshrc")] plan = plan_link(desired, LinkedState(), _fs_check_none, _no_link_target) assert len(plan.operations) == 1 assert plan.operations[0].type == "create_link" assert plan.summary.added == 1 def test_existing_correct_link_unchanged(self): lt = _lt("/home/x/.zshrc") current = LinkedState(links={Path("/home/x/.zshrc"): lt}) plan = plan_link([lt], current, _fs_check_none, _no_link_target) assert len(plan.operations) == 0 assert plan.summary.unchanged == 1 def test_stale_link_removed(self): old = _lt("/home/x/.old") current = LinkedState(links={Path("/home/x/.old"): old}) plan = plan_link([], current, _fs_check_none, _no_link_target) assert len(plan.operations) == 1 assert plan.operations[0].type == "remove_link" assert plan.summary.removed == 1 def test_changed_source_produces_remove_then_create(self): old = _lt("/home/x/.zshrc", source="/old") new = _lt("/home/x/.zshrc", source="/new") current = LinkedState(links={Path("/home/x/.zshrc"): old}) plan = plan_link([new], current, _fs_check_none, _no_link_target) types = [op.type for op in plan.operations] assert types == ["remove_link", "create_link"] assert plan.summary.updated == 1 assert plan.summary.added == 0 assert plan.summary.removed == 0 def test_unmanaged_file_at_target_is_conflict(self): desired = [_lt("/home/x/.zshrc")] plan = plan_link(desired, LinkedState(), _fs_check_file, _no_link_target) assert len(plan.conflicts) == 1 assert ".zshrc" in plan.conflicts[0] def test_module_targets_counted(self): desired = [_lt("/home/x/.config/nvim/init.lua", module=True)] plan = plan_link(desired, LinkedState(), _fs_check_none, _no_link_target) assert plan.summary.from_modules == 1 def test_broken_symlink_is_repaired(self): lt = _lt("/home/x/.zshrc") current = LinkedState(links={Path("/home/x/.zshrc"): lt}) plan = plan_link( [lt], current, lambda p: "broken_symlink", _no_link_target, ) types = [op.type for op in plan.operations] assert types == ["remove_link", "create_link"] assert plan.summary.updated == 1 assert plan.summary.unchanged == 0 def test_orphan_symlink_matching_source_is_adopted(self): """Untracked symlink that already points at the desired source counts as unchanged, not a conflict. Enables partial-failure rerun.""" desired = [_lt("/home/x/.zshrc", source="/dots/.zshrc")] plan = plan_link( desired, LinkedState(), lambda p: "symlink", lambda p: Path("/dots/.zshrc"), ) assert plan.conflicts == () assert plan.operations == () assert plan.summary.unchanged == 1 assert plan.summary.added == 0 def test_orphan_symlink_pointing_elsewhere_is_conflict(self): """Untracked symlink that points at the wrong source is still a conflict -- we don't silently retarget unmanaged links.""" desired = [_lt("/home/x/.zshrc", source="/dots/.zshrc")] plan = plan_link( desired, LinkedState(), lambda p: "symlink", lambda p: Path("/elsewhere/.zshrc"), ) assert len(plan.conflicts) == 1 assert plan.summary.added == 0 assert plan.summary.unchanged == 0 def test_orphan_module_symlink_counts_as_module(self): """Adopted orphan symlinks for module files still count toward from_modules in the summary.""" desired = [ _lt("/home/x/.config/nvim/init.lua", source="/cache/init.lua", module=True), ] plan = plan_link( desired, LinkedState(), lambda p: "symlink", lambda p: Path("/cache/init.lua"), ) assert plan.summary.unchanged == 1 assert plan.summary.from_modules == 1 class TestPlanUnlink: def test_unlink_all(self): lt = _lt("/home/x/.zshrc") current = LinkedState(links={Path("/home/x/.zshrc"): lt}) plan = plan_unlink(current, packages=None) assert len(plan.operations) == 1 assert plan.operations[0].type == "remove_link" def test_unlink_specific_package(self): zsh = _lt("/home/x/.zshrc", pkg="_shared/zsh") git = _lt("/home/x/.gitconfig", pkg="_shared/git") current = LinkedState(links={ Path("/home/x/.zshrc"): zsh, Path("/home/x/.gitconfig"): git, }) plan = plan_unlink(current, packages=["_shared/zsh"]) assert len(plan.operations) == 1 assert plan.operations[0].target == Path("/home/x/.zshrc") def test_unlink_by_basename(self): zsh = _lt("/home/x/.zshrc", pkg="_shared/zsh") current = LinkedState(links={Path("/home/x/.zshrc"): zsh}) plan = plan_unlink(current, packages=["zsh"]) assert len(plan.operations) == 1