"""Tests for flow.core.stow — GNU Stow-style tree folding/unfolding.""" import os from pathlib import Path import pytest from flow.core.stow import LinkOperation, LinkTree, TreeFolder @pytest.fixture def temp_home(tmp_path): """Create a temporary home directory.""" home = tmp_path / "home" home.mkdir() return home @pytest.fixture def temp_dotfiles(tmp_path): """Create a temporary dotfiles repository.""" dotfiles = tmp_path / "dotfiles" dotfiles.mkdir() return dotfiles def test_linktree_add_remove(): """Test basic LinkTree operations.""" tree = LinkTree() source = Path("/dotfiles/zsh/.zshrc") target = Path("/home/user/.zshrc") tree.add_link(target, source, "zsh", is_dir_link=False) assert target in tree.links assert tree.links[target] == source assert tree.packages[target] == "zsh" assert not tree.is_directory_link(target) tree.remove_link(target) assert target not in tree.links assert target not in tree.packages def test_linktree_directory_link(): """Test directory link tracking.""" tree = LinkTree() source = Path("/dotfiles/nvim/.config/nvim") target = Path("/home/user/.config/nvim") tree.add_link(target, source, "nvim", is_dir_link=True) assert tree.is_directory_link(target) def test_linktree_can_fold_single_package(): """Test can_fold with single package.""" tree = LinkTree() target_dir = Path("/home/user/.config/nvim") # Add files from same package tree.add_link(target_dir / "init.lua", Path("/dotfiles/nvim/.config/nvim/init.lua"), "nvim") tree.add_link(target_dir / "lua" / "config.lua", Path("/dotfiles/nvim/.config/nvim/lua/config.lua"), "nvim") # Should be able to fold since all files are from same package assert tree.can_fold(target_dir, "nvim") def test_linktree_can_fold_multiple_packages(): """Test can_fold with multiple packages.""" tree = LinkTree() target_dir = Path("/home/user/.config") # Add files from different packages tree.add_link(target_dir / "nvim", Path("/dotfiles/nvim/.config/nvim"), "nvim", is_dir_link=True) tree.add_link(target_dir / "tmux", Path("/dotfiles/tmux/.config/tmux"), "tmux", is_dir_link=True) # Cannot fold .config since it has files from multiple packages assert not tree.can_fold(target_dir, "nvim") def test_linktree_from_state_old_format_rejected(): """Old state format should be rejected (no backward compatibility).""" state = { "links": { "zsh": { "/home/user/.zshrc": "/dotfiles/zsh/.zshrc", "/home/user/.zshenv": "/dotfiles/zsh/.zshenv", } } } with pytest.raises(RuntimeError, match="Unsupported linked state format"): LinkTree.from_state(state) def test_linktree_from_state_new_format(): """Test loading from new state format (with is_directory_link).""" state = { "version": 2, "links": { "nvim": { "/home/user/.config/nvim": { "source": "/dotfiles/nvim/.config/nvim", "is_directory_link": True, } } } } tree = LinkTree.from_state(state) target = Path("/home/user/.config/nvim") assert target in tree.links assert tree.is_directory_link(target) assert tree.packages[target] == "nvim" def test_linktree_to_state(): """Test converting LinkTree to state format.""" tree = LinkTree() tree.add_link( Path("/home/user/.config/nvim"), Path("/dotfiles/nvim/.config/nvim"), "nvim", is_dir_link=True, ) tree.add_link( Path("/home/user/.zshrc"), Path("/dotfiles/zsh/.zshrc"), "zsh", is_dir_link=False, ) state = tree.to_state() assert state["version"] == 2 assert "nvim" in state["links"] assert "zsh" in state["links"] nvim_link = state["links"]["nvim"]["/home/user/.config/nvim"] assert nvim_link["is_directory_link"] is True zsh_link = state["links"]["zsh"]["/home/user/.zshrc"] assert zsh_link["is_directory_link"] is False def test_treefolder_plan_link_simple(temp_home, temp_dotfiles): """Test planning a simple file link.""" tree = LinkTree() folder = TreeFolder(tree) source = temp_dotfiles / "zsh" / ".zshrc" target = temp_home / ".zshrc" # Create source file source.parent.mkdir(parents=True) source.write_text("# zshrc") ops = folder.plan_link(source, target, "zsh") assert len(ops) == 1 assert ops[0].type == "create_symlink" assert ops[0].source == source assert ops[0].target == target assert ops[0].package == "zsh" def test_treefolder_detect_conflicts_existing_file(temp_home, temp_dotfiles): """Test conflict detection for existing files.""" tree = LinkTree() folder = TreeFolder(tree) source = temp_dotfiles / "zsh" / ".zshrc" target = temp_home / ".zshrc" # Create existing file target.parent.mkdir(parents=True, exist_ok=True) target.write_text("# existing") source.parent.mkdir(parents=True) source.write_text("# zshrc") ops = folder.plan_link(source, target, "zsh") conflicts = folder.detect_conflicts(ops) assert len(conflicts) == 1 assert "already exists" in conflicts[0] def test_treefolder_detect_conflicts_different_package(temp_home, temp_dotfiles): """Test conflict detection for links from different packages.""" tree = LinkTree() target = temp_home / ".bashrc" # Add existing link from different package tree.add_link(target, Path("/dotfiles/bash/.bashrc"), "bash") folder = TreeFolder(tree) source = temp_dotfiles / "zsh" / ".bashrc" source.parent.mkdir(parents=True) source.write_text("# bashrc") ops = folder.plan_link(source, target, "zsh") conflicts = folder.detect_conflicts(ops) assert len(conflicts) == 1 assert "bash" in conflicts[0] def test_treefolder_execute_operations_dry_run(temp_home, temp_dotfiles, capsys): """Test dry-run mode.""" tree = LinkTree() folder = TreeFolder(tree) source = temp_dotfiles / "zsh" / ".zshrc" target = temp_home / ".zshrc" source.parent.mkdir(parents=True) source.write_text("# zshrc") ops = folder.plan_link(source, target, "zsh") folder.execute_operations(ops, dry_run=True) # Check output captured = capsys.readouterr() assert "FILE LINK" in captured.out assert str(target) in captured.out # No actual symlink created assert not target.exists() def test_treefolder_execute_operations_create_symlink(temp_home, temp_dotfiles): """Test creating actual symlinks.""" tree = LinkTree() folder = TreeFolder(tree) source = temp_dotfiles / "zsh" / ".zshrc" target = temp_home / ".zshrc" source.parent.mkdir(parents=True) source.write_text("# zshrc") ops = folder.plan_link(source, target, "zsh") folder.execute_operations(ops, dry_run=False) # Check symlink created assert target.is_symlink() assert target.resolve() == source.resolve() # Check tree updated assert target in folder.tree.links def test_treefolder_plan_unlink(temp_home, temp_dotfiles): """Test planning unlink operations.""" tree = LinkTree() target = temp_home / ".zshrc" source = temp_dotfiles / "zsh" / ".zshrc" tree.add_link(target, source, "zsh") folder = TreeFolder(tree) ops = folder.plan_unlink(target, "zsh") assert len(ops) == 1 assert ops[0].type == "remove" assert ops[0].target == target def test_treefolder_plan_unlink_directory_link(temp_home, temp_dotfiles): """Test planning unlink for directory symlink.""" tree = LinkTree() target = temp_home / ".config" / "nvim" source = temp_dotfiles / "nvim" / ".config" / "nvim" tree.add_link(target, source, "nvim", is_dir_link=True) folder = TreeFolder(tree) ops = folder.plan_unlink(target, "nvim") # Should remove the directory link assert len(ops) >= 1 assert ops[-1].type == "remove" assert ops[-1].is_directory_link def test_linkoperation_str(): """Test LinkOperation string representation.""" op1 = LinkOperation( type="create_symlink", source=Path("/src"), target=Path("/dst"), package="test", is_directory_link=False, ) assert "FILE LINK" in str(op1) op2 = LinkOperation( type="create_symlink", source=Path("/src"), target=Path("/dst"), package="test", is_directory_link=True, ) assert "DIR LINK" in str(op2) op3 = LinkOperation( type="unfold", source=Path("/src"), target=Path("/dst"), package="test", ) assert "UNFOLD" in str(op3)