This commit is contained in:
2026-05-13 23:02:47 +03:00
parent d0f8315cf1
commit 78f95bc88e
49 changed files with 2747 additions and 987 deletions

View File

@@ -1,6 +1,5 @@
"""Tests for DotfilesService."""
import subprocess
from pathlib import Path
import yaml
@@ -8,19 +7,10 @@ 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 CommandRunner, SystemRuntime
from flow.core.runtime import SystemRuntime
from flow.core import paths
from flow.services.dotfiles import DotfilesService
class FakeRunner(CommandRunner):
def __init__(self):
self.calls: list[list[str]] = []
def run(self, argv, *, cwd=None, env=None, capture_output=True, check=False, timeout=None):
command = [str(part) for part in argv]
self.calls.append(command)
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
from tests.fakes import FakeRunner
def _make_ctx(tmp_path, console=None):
@@ -206,7 +196,69 @@ class TestDotfilesServiceLink:
assert target.read_text() == "user managed file"
assert not target.is_symlink()
def test_sync_modules_includes_profile_layers(self, tmp_path, monkeypatch):
def test_status_shows_module_info(self, tmp_path, monkeypatch, capsys):
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 cloned module
module_dir = modules / "_shared--nvim"
module_dir.mkdir(parents=True)
(module_dir / "init.lua").write_text("-- init")
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()
svc.status()
output = capsys.readouterr().out
assert "nvim" in output
assert "branch:main" in output
def test_repos_list_shows_dotfiles_and_modules(self, tmp_path, monkeypatch, capsys):
home = tmp_path / "home"
home.mkdir()
dotfiles = tmp_path / "dotfiles"
modules = tmp_path / "modules"
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"},
}))
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.repos_list()
output = capsys.readouterr().out
assert "dotfiles" in output
assert "nvim" in output
assert "module" in output
def test_repos_pull_includes_profile_module_repos(self, tmp_path, monkeypatch):
home = tmp_path / "home"
home.mkdir()
dotfiles = tmp_path / "dotfiles"
@@ -234,5 +286,150 @@ class TestDotfilesServiceLink:
runtime=runtime,
)
DotfilesService(ctx).sync_modules()
DotfilesService(ctx).repos_pull()
assert any("linux-work--nvim" in " ".join(call) for call in runner.calls)
def test_repos_status_shows_repo_names(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")
# Make dotfiles dir look like a git repo for status
(dotfiles / ".git").mkdir()
runtime = SystemRuntime()
runner = FakeRunner()
runtime.runner = runner
runtime.git.runner = runner
ctx = FlowContext(
config=AppConfig(),
manifest={},
platform=PlatformInfo(),
console=Console(color=False),
runtime=runtime,
)
DotfilesService(ctx).repos_status()
output = capsys.readouterr().out
assert "dotfiles" in output
def test_repos_push_calls_git_push(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")
runtime = SystemRuntime()
runner = FakeRunner()
runtime.runner = runner
runtime.git.runner = runner
ctx = FlowContext(
config=AppConfig(),
manifest={},
platform=PlatformInfo(),
console=Console(color=False),
runtime=runtime,
)
DotfilesService(ctx).repos_push()
assert any("push" in " ".join(call) for call in runner.calls)
def test_repos_pull_dry_run_no_calls(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")
runtime = SystemRuntime()
runner = FakeRunner()
runtime.runner = runner
runtime.git.runner = runner
ctx = FlowContext(
config=AppConfig(),
manifest={},
platform=PlatformInfo(),
console=Console(color=False),
runtime=runtime,
)
DotfilesService(ctx).repos_pull(dry_run=True)
output = capsys.readouterr().out
assert "Would" in output
# No git calls should be made in dry run
assert not runner.calls
def test_status_filter_by_package(self, tmp_path, monkeypatch, capsys):
home = tmp_path / "home"
home.mkdir()
dotfiles = _setup_dotfiles(tmp_path, {
"zsh": {".zshrc": "# zsh"},
"git": {".gitconfig": "[user]"},
})
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()
capsys.readouterr() # discard link output
svc.status(package_filter=["zsh"])
output = capsys.readouterr().out
assert "zsh" in output
# Only zsh should appear, not git
assert "_shared/git" not in output
def test_link_repairs_broken_symlinks(self, tmp_path, monkeypatch):
home = tmp_path / "home"
home.mkdir()
dotfiles = _setup_dotfiles(tmp_path, {
"zsh": {".zshrc": "# zsh config"},
})
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 normally
svc.link()
assert (home / ".zshrc").is_symlink()
# Break the symlink by removing its target
real_target = (home / ".zshrc").resolve()
(home / ".zshrc").unlink()
(home / ".zshrc").symlink_to("/nonexistent/path")
# Re-link should repair the broken symlink
svc.link()
assert (home / ".zshrc").is_symlink()
assert (home / ".zshrc").resolve() == real_target.resolve()