"""Integration tests for dotfiles tree folding behavior.""" import os from pathlib import Path import pytest from flow.commands.dotfiles import _discover_packages, _walk_package from flow.core.config import AppConfig, FlowContext from flow.core.console import ConsoleLogger from flow.core.platform import PlatformInfo from flow.core.stow import LinkTree, TreeFolder @pytest.fixture def ctx(): """Create a mock FlowContext.""" return FlowContext( config=AppConfig(), manifest={}, platform=PlatformInfo(), console=ConsoleLogger(), ) @pytest.fixture def dotfiles_with_nested(tmp_path): """Create dotfiles with nested directory structure for folding tests.""" common = tmp_path / "common" # nvim package with nested config nvim = common / "nvim" / ".config" / "nvim" nvim.mkdir(parents=True) (nvim / "init.lua").write_text("-- init") (nvim / "lua").mkdir() (nvim / "lua" / "config.lua").write_text("-- config") (nvim / "lua" / "plugins.lua").write_text("-- plugins") # zsh package with flat structure zsh = common / "zsh" zsh.mkdir(parents=True) (zsh / ".zshrc").write_text("# zshrc") (zsh / ".zshenv").write_text("# zshenv") return tmp_path @pytest.fixture def home_dir(tmp_path): """Create a temporary home directory.""" home = tmp_path / "home" home.mkdir() return home def test_tree_folding_single_package(dotfiles_with_nested, home_dir): """Test that a single package can be folded into directory symlink.""" # Discover nvim package packages = _discover_packages(dotfiles_with_nested) nvim_source = packages["nvim"] # Build link tree tree = LinkTree() folder = TreeFolder(tree) # Plan links for all nvim files operations = [] for src, dst in _walk_package(nvim_source, home_dir): ops = folder.plan_link(src, dst, "nvim") operations.extend(ops) # Execute operations folder.execute_operations(operations, dry_run=False) # Check that we created efficient symlinks # In ideal case, we'd have one directory symlink instead of 3 file symlinks nvim_config = home_dir / ".config" / "nvim" # Verify links work assert (nvim_config / "init.lua").exists() assert (nvim_config / "lua" / "config.lua").exists() def test_tree_unfolding_conflict(dotfiles_with_nested, home_dir): """Test that tree unfolds when second package needs same directory.""" common = dotfiles_with_nested / "common" # Create second package that shares .config tmux = common / "tmux" / ".config" / "tmux" tmux.mkdir(parents=True) (tmux / "tmux.conf").write_text("# tmux") # First, link nvim (can fold .config/nvim) tree = LinkTree() folder = TreeFolder(tree) nvim_source = common / "nvim" for src, dst in _walk_package(nvim_source, home_dir): ops = folder.plan_link(src, dst, "nvim") folder.execute_operations(ops, dry_run=False) # Now link tmux (should unfold if needed) tmux_source = common / "tmux" for src, dst in _walk_package(tmux_source, home_dir): ops = folder.plan_link(src, dst, "tmux") folder.execute_operations(ops, dry_run=False) # Both packages should be linked assert (home_dir / ".config" / "nvim" / "init.lua").exists() assert (home_dir / ".config" / "tmux" / "tmux.conf").exists() def test_state_format_with_directory_links(dotfiles_with_nested, home_dir): """Test that state file correctly tracks directory vs file links.""" tree = LinkTree() # Add a directory link tree.add_link( home_dir / ".config" / "nvim", dotfiles_with_nested / "common" / "nvim" / ".config" / "nvim", "nvim", is_dir_link=True, ) # Add a file link tree.add_link( home_dir / ".zshrc", dotfiles_with_nested / "common" / "zsh" / ".zshrc", "zsh", is_dir_link=False, ) # Convert to state state = tree.to_state() # Verify format assert state["version"] == 2 nvim_link = state["links"]["nvim"][str(home_dir / ".config" / "nvim")] assert nvim_link["is_directory_link"] is True zsh_link = state["links"]["zsh"][str(home_dir / ".zshrc")] assert zsh_link["is_directory_link"] is False def test_state_backward_compatibility_rejected(home_dir): """Old state format should be rejected (no backward compatibility).""" old_state = { "links": { "zsh": { str(home_dir / ".zshrc"): str(home_dir.parent / "dotfiles" / "zsh" / ".zshrc"), } } } with pytest.raises(RuntimeError, match="Unsupported linked state format"): LinkTree.from_state(old_state) def test_discover_packages_with_flow_package(tmp_path): """Test discovering the flow package itself from dotfiles.""" common = tmp_path / "common" # Create flow package flow_pkg = common / "flow" / ".config" / "flow" flow_pkg.mkdir(parents=True) (flow_pkg / "manifest.yaml").write_text("profiles: {}") (flow_pkg / "config").write_text("[repository]\n") packages = _discover_packages(tmp_path) # Flow package should be discovered like any other assert "flow" in packages assert packages["flow"] == common / "flow" def test_walk_flow_package(tmp_path): """Test walking the flow package structure.""" flow_pkg = tmp_path / "flow" flow_config = flow_pkg / ".config" / "flow" flow_config.mkdir(parents=True) (flow_config / "manifest.yaml").write_text("profiles: {}") (flow_config / "config").write_text("[repository]\n") home = Path("/tmp/fakehome") pairs = list(_walk_package(flow_pkg, home)) # Should find both files assert len(pairs) == 2 targets = [str(t) for _, t in pairs] assert str(home / ".config" / "flow" / "manifest.yaml") in targets assert str(home / ".config" / "flow" / "config") in targets def test_conflict_detection_before_execution(dotfiles_with_nested, home_dir): """Test that conflicts are detected before any changes are made.""" # Create existing file that conflicts existing = home_dir / ".zshrc" existing.parent.mkdir(parents=True, exist_ok=True) existing.write_text("# existing zshrc") # Try to link package that wants .zshrc tree = LinkTree() folder = TreeFolder(tree) zsh_source = dotfiles_with_nested / "common" / "zsh" operations = [] for src, dst in _walk_package(zsh_source, home_dir): ops = folder.plan_link(src, dst, "zsh") operations.extend(ops) # Should detect conflict conflicts = folder.detect_conflicts(operations) assert len(conflicts) > 0 assert any("already exists" in c for c in conflicts) # Original file should be unchanged assert existing.read_text() == "# existing zshrc" def test_profile_switching_relink(tmp_path): """Test switching between profiles maintains correct links.""" # Create profiles common = tmp_path / "common" profiles = tmp_path / "profiles" # Common zsh (common / "zsh").mkdir(parents=True) (common / "zsh" / ".zshrc").write_text("# common zsh") # Work profile override (profiles / "work" / "zsh").mkdir(parents=True) (profiles / "work" / "zsh" / ".zshrc").write_text("# work zsh") # Personal profile override (profiles / "personal" / "zsh").mkdir(parents=True) (profiles / "personal" / "zsh" / ".zshrc").write_text("# personal zsh") # Test that profile discovery works correctly work_packages = _discover_packages(tmp_path, profile="work") personal_packages = _discover_packages(tmp_path, profile="personal") # Both should find zsh, but from different sources assert "zsh" in work_packages assert "zsh" in personal_packages assert work_packages["zsh"] != personal_packages["zsh"] def test_can_fold_empty_directory(): """Test can_fold with empty directory.""" tree = LinkTree() target_dir = Path("/home/user/.config/nvim") # Empty directory - should be able to fold assert tree.can_fold(target_dir, "nvim") def test_can_fold_with_subdirectories(): """Test can_fold with nested directory structure.""" tree = LinkTree() base = Path("/home/user/.config/nvim") # Add nested files from same package tree.add_link(base / "init.lua", Path("/dotfiles/nvim/init.lua"), "nvim") tree.add_link(base / "lua" / "config.lua", Path("/dotfiles/nvim/lua/config.lua"), "nvim") tree.add_link(base / "lua" / "plugins" / "init.lua", Path("/dotfiles/nvim/lua/plugins/init.lua"), "nvim") # Should be able to fold at base level assert tree.can_fold(base, "nvim") # Add file from different package tree.add_link(base / "other.lua", Path("/dotfiles/other/other.lua"), "other") # Now cannot fold assert not tree.can_fold(base, "nvim") def test_execute_operations_creates_parent_dirs(tmp_path): """Test that execute_operations creates necessary parent directories.""" tree = LinkTree() folder = TreeFolder(tree) source = tmp_path / "dotfiles" / "nvim" / ".config" / "nvim" / "init.lua" target = tmp_path / "home" / ".config" / "nvim" / "init.lua" # Create source source.parent.mkdir(parents=True) source.write_text("-- init") # Target parent doesn't exist yet assert not target.parent.exists() # Plan and execute ops = folder.plan_link(source, target, "nvim") folder.execute_operations(ops, dry_run=False) # Parent should be created assert target.parent.exists() assert target.is_symlink()