"""Tests for dotfiles path resolution.""" from pathlib import Path import pytest from flow.core.errors import PlanConflict from flow.domain.dotfiles.models import ModuleRef, Package from flow.domain.dotfiles.resolution import resolve_all_targets, resolve_package_targets RESERVED_ROOT = "_root" HOME = Path("/home/testuser") def _pkg(name, layer="_shared", files=(), module=None): return Package( name=name, layer=layer, package_id=f"{layer}/{name}", source_dir=Path(f"/dots/{layer}/{name}"), module=module, local_files=tuple(files), ) class TestResolvePackageTargets: def test_simple_file(self): pkg = _pkg("zsh", files=[ (Path("/dots/_shared/zsh/.zshrc"), Path(".zshrc")), ]) targets = resolve_package_targets(pkg, HOME, set()) assert len(targets) == 1 assert targets[0].target == HOME / ".zshrc" assert targets[0].source == Path("/dots/_shared/zsh/.zshrc") assert targets[0].from_module is False def test_nested_config(self): pkg = _pkg("git", files=[ (Path("/dots/_shared/git/.config/git/config"), Path(".config/git/config")), ]) targets = resolve_package_targets(pkg, HOME, set()) assert targets[0].target == HOME / ".config" / "git" / "config" def test_root_marker(self): pkg = _pkg("dns", files=[ (Path("/dots/_shared/dns/_root/etc/hosts"), Path("_root/etc/hosts")), ]) targets = resolve_package_targets(pkg, HOME, set()) assert targets[0].target == Path("/etc/hosts") assert targets[0].needs_sudo is True def test_root_marker_skipped_when_in_skip_set(self): pkg = _pkg("dns", files=[ (Path("/dots/_shared/dns/_root/etc/hosts"), Path("_root/etc/hosts")), ]) targets = resolve_package_targets(pkg, HOME, {"_root"}) assert len(targets) == 0 def test_bare_root_marker_raises(self): """A package containing a file literally named `_root` (no children) should be rejected, not silently dropped.""" pkg = _pkg("bad", files=[ (Path("/dots/_shared/bad/_root"), Path("_root")), ]) with pytest.raises(PlanConflict, match="_root"): resolve_package_targets(pkg, HOME, set()) def test_skip_package_by_name(self): pkg = _pkg("nvim", files=[ (Path("/dots/_shared/nvim/.config/nvim/init.lua"), Path(".config/nvim/init.lua")), ]) targets = resolve_package_targets(pkg, HOME, {"nvim"}) assert len(targets) == 0 def test_module_files_linked_under_mount_path(self): module = ModuleRef( source="https://github.com/org/nvim-config.git", ref_type="branch", ref_value="main", mount_path=Path(".config/nvim"), cache_dir=Path("/modules/_shared--nvim"), module_files=( (Path("/modules/_shared--nvim/init.lua"), Path("init.lua")), (Path("/modules/_shared--nvim/lua/plugins.lua"), Path("lua/plugins.lua")), ), ) pkg = _pkg("nvim", files=[ (Path("/dots/_shared/nvim/.local/bin/nvim-wrapper"), Path(".local/bin/nvim-wrapper")), ], module=module) targets = resolve_package_targets(pkg, HOME, set()) # Local file outside mount_path local_targets = [t for t in targets if not t.from_module] assert len(local_targets) == 1 assert local_targets[0].target == HOME / ".local" / "bin" / "nvim-wrapper" # Module files under mount_path module_targets = [t for t in targets if t.from_module] assert len(module_targets) == 2 module_target_paths = {t.target for t in module_targets} assert HOME / ".config" / "nvim" / "init.lua" in module_target_paths assert HOME / ".config" / "nvim" / "lua" / "plugins.lua" in module_target_paths def test_module_yaml_file_not_linked(self): """The _module.yaml marker itself should never be linked.""" pkg = _pkg("nvim", files=[ (Path("/dots/_shared/nvim/.config/nvim/_module.yaml"), Path(".config/nvim/_module.yaml")), ], module=ModuleRef( source="x", ref_type="branch", ref_value="main", mount_path=Path(".config/nvim"), cache_dir=Path("/m"), module_files=(), )) targets = resolve_package_targets(pkg, HOME, set()) assert not any(t.target.name == "_module.yaml" for t in targets) def test_root_level_module_skips_all_local_files(self): """When mount_path is '.', all local files are from the module, not dotfiles repo.""" module = ModuleRef( source="x", ref_type="branch", ref_value="main", mount_path=Path("."), cache_dir=Path("/m"), module_files=( (Path("/m/init.lua"), Path("init.lua")), ), ) pkg = _pkg("nvim", files=[ (Path("/dots/_shared/nvim/_module.yaml"), Path("_module.yaml")), (Path("/dots/_shared/nvim/stale-file.txt"), Path("stale-file.txt")), ], module=module) targets = resolve_package_targets(pkg, HOME, set()) # Only module files should appear, local files skipped assert len(targets) == 1 assert targets[0].from_module is True assert targets[0].target == HOME / "init.lua" class TestResolveAllTargets: def test_no_conflicts(self): pkgs = [ _pkg("zsh", files=[(Path("/a/.zshrc"), Path(".zshrc"))]), _pkg("git", files=[(Path("/a/.gitconfig"), Path(".gitconfig"))]), ] targets = resolve_all_targets(pkgs, HOME, set()) assert len(targets) == 2 def test_duplicate_target_raises(self): pkgs = [ _pkg("zsh", layer="_shared", files=[(Path("/a/.zshrc"), Path(".zshrc"))]), _pkg("zsh", layer="work", files=[(Path("/b/.zshrc"), Path(".zshrc"))]), ] with pytest.raises(PlanConflict): resolve_all_targets(pkgs, HOME, set())