feat: add all services (dotfiles, packages, bootstrap, remote, containers, projects)
- DotfilesService: package discovery, module sync, link/unlink/status - PackageService: install/remove/list with PM and binary support - BootstrapService: profile-based system setup orchestration - RemoteService: SSH target resolution and connection - ContainerService: docker container lifecycle management - ProjectService: git repo status checking 26 service tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
172
tests/test_service_dotfiles.py
Normal file
172
tests/test_service_dotfiles.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user