"""Tests for DotfilesService.""" from pathlib import Path from unittest.mock import MagicMock import yaml from flow.core.config import AppConfig, FlowContext from flow.core.console import Console from flow.core.platform import PlatformInfo from flow.core.runtime import SystemRuntime from flow.core import paths from flow.services.dotfiles import DotfilesService def _make_ctx(tmp_path, console=None): """Build a FlowContext for testing.""" return FlowContext( config=AppConfig(), manifest={}, platform=PlatformInfo(), console=console or Console(color=False), runtime=SystemRuntime(), ) def _setup_dotfiles(tmp_path, packages_files): """Set up a fake dotfiles directory structure. packages_files: dict of {package_name: {relative_path: content}} """ dotfiles = tmp_path / "dotfiles" shared = dotfiles / "_shared" for pkg_name, files in packages_files.items(): pkg_dir = shared / pkg_name for rel_path, content in files.items(): file_path = pkg_dir / rel_path file_path.parent.mkdir(parents=True, exist_ok=True) file_path.write_text(content) return dotfiles class TestDotfilesServiceLink: def test_link_creates_symlinks(self, tmp_path, monkeypatch): home = tmp_path / "home" home.mkdir() dotfiles = _setup_dotfiles(tmp_path, { "zsh": {".zshrc": "# zsh config"}, "git": {".config/git/config": "[user]\n name = test"}, }) monkeypatch.setattr(paths, "HOME", home) monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles) monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules") monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json") ctx = _make_ctx(tmp_path) svc = DotfilesService(ctx) svc.link() assert (home / ".zshrc").is_symlink() assert (home / ".config" / "git" / "config").is_symlink() def test_link_with_module(self, tmp_path, monkeypatch): home = tmp_path / "home" home.mkdir() dotfiles = tmp_path / "dotfiles" modules = tmp_path / "modules" # Set up package with _module.yaml pkg_dir = dotfiles / "_shared" / "nvim" config_dir = pkg_dir / ".config" / "nvim" config_dir.mkdir(parents=True) (config_dir / "_module.yaml").write_text(yaml.dump({ "source": "github:test/nvim-config", "ref": {"branch": "main"}, })) # Set up local file outside mount path (pkg_dir / ".local" / "bin").mkdir(parents=True) (pkg_dir / ".local" / "bin" / "nvim-wrapper").write_text("#!/bin/sh") # Set up cloned module module_dir = modules / "_shared--nvim" module_dir.mkdir(parents=True) (module_dir / "init.lua").write_text("-- init") (module_dir / "lua").mkdir() (module_dir / "lua" / "plugins.lua").write_text("-- plugins") monkeypatch.setattr(paths, "HOME", home) monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles) monkeypatch.setattr(paths, "MODULES_DIR", modules) monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json") ctx = _make_ctx(tmp_path) svc = DotfilesService(ctx) svc.link() # Module files should be linked under .config/nvim/ assert (home / ".config" / "nvim" / "init.lua").is_symlink() assert (home / ".config" / "nvim" / "lua" / "plugins.lua").is_symlink() # Local file outside mount path should be linked assert (home / ".local" / "bin" / "nvim-wrapper").is_symlink() def test_unlink_removes_symlinks(self, tmp_path, monkeypatch): home = tmp_path / "home" home.mkdir() dotfiles = _setup_dotfiles(tmp_path, { "zsh": {".zshrc": "# zsh"}, }) monkeypatch.setattr(paths, "HOME", home) monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles) monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules") monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json") ctx = _make_ctx(tmp_path) svc = DotfilesService(ctx) # Link first svc.link() assert (home / ".zshrc").is_symlink() # Then unlink svc.unlink() assert not (home / ".zshrc").exists() def test_link_dry_run_no_changes(self, tmp_path, monkeypatch): home = tmp_path / "home" home.mkdir() dotfiles = _setup_dotfiles(tmp_path, { "zsh": {".zshrc": "# zsh"}, }) monkeypatch.setattr(paths, "HOME", home) monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles) monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules") monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json") ctx = _make_ctx(tmp_path) svc = DotfilesService(ctx) svc.link(dry_run=True) # No symlinks should exist assert not (home / ".zshrc").exists() def test_status_shows_packages(self, tmp_path, monkeypatch, capsys): home = tmp_path / "home" home.mkdir() dotfiles = _setup_dotfiles(tmp_path, { "zsh": {".zshrc": "# zsh"}, }) monkeypatch.setattr(paths, "HOME", home) monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles) monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules") monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json") ctx = _make_ctx(tmp_path) svc = DotfilesService(ctx) # Link first to populate state svc.link() # Check status svc.status() output = capsys.readouterr().out assert "zsh" in output