This commit is contained in:
2026-03-16 08:06:25 +02:00
parent 78d4064853
commit d0f8315cf1
35 changed files with 2493 additions and 624 deletions

View File

@@ -1,10 +1,8 @@
"""Tests for CLI."""
import os
import subprocess
import sys
from unittest.mock import patch
import pytest
def test_version_flag():
@@ -46,3 +44,43 @@ def test_packages_help():
)
assert result.returncode == 0
assert "install" in result.stdout
def test_invalid_config_is_reported_without_traceback(tmp_path):
config_root = tmp_path / "config"
(config_root / "flow").mkdir(parents=True)
(config_root / "flow" / "config.yaml").write_text(":\n bad\n")
env = dict(os.environ)
env["XDG_CONFIG_HOME"] = str(config_root)
result = subprocess.run(
[sys.executable, "-m", "flow", "completion"],
capture_output=True,
text=True,
env=env,
)
assert result.returncode == 1
assert "Invalid YAML" in result.stderr
assert "Traceback" not in result.stderr
def test_local_manifest_is_loaded(tmp_path):
config_root = tmp_path / "config"
flow_dir = config_root / "flow"
flow_dir.mkdir(parents=True)
(flow_dir / "manifest.yaml").write_text(
"profiles:\n"
" demo:\n"
" os: linux\n"
)
env = dict(os.environ)
env["XDG_CONFIG_HOME"] = str(config_root)
result = subprocess.run(
[sys.executable, "-m", "flow", "setup", "list"],
capture_output=True,
text=True,
env=env,
)
assert result.returncode == 0
assert "demo" in result.stdout

View File

@@ -75,3 +75,59 @@ def test_load_manifest_merges_files(tmp_path):
data = load_manifest(tmp_path)
assert "packages" in data
assert "profiles" in data
def test_load_config_merges_local_and_overlay(tmp_path):
local = tmp_path / "local"
overlay = tmp_path / "overlay"
local.mkdir()
overlay.mkdir()
(local / "config.yaml").write_text(
"repository:\n"
" url: git@github.com:user/dots.git\n"
"targets:\n"
" personal@orb: personal.orb\n"
)
(overlay / "config.yaml").write_text(
"repository:\n"
" branch: dev\n"
"defaults:\n"
" tmux-session: main\n"
)
cfg = load_config(local, overlay)
assert cfg.dotfiles_url == "git@github.com:user/dots.git"
assert cfg.dotfiles_branch == "dev"
assert cfg.tmux_session == "main"
assert cfg.targets[0].host == "personal.orb"
def test_load_config_parses_legacy_targets(tmp_path):
(tmp_path / "01-targets.yaml").write_text(
"targets:\n"
" personal: orb personal.orb ~/.ssh/id_personal\n"
)
(tmp_path / "02-targets.yaml").write_text(
"targets:\n"
" - namespace: work\n"
" platform: ec2\n"
" host: work.ec2.internal\n"
" identity: ~/.ssh/id_work\n"
)
cfg = load_config(tmp_path)
assert len(cfg.targets) == 2
assert cfg.targets[0].platform == "orb"
assert cfg.targets[0].identity == "~/.ssh/id_personal"
assert cfg.targets[1].namespace == "work"
def test_load_manifest_merges_local_and_overlay(tmp_path):
local = tmp_path / "local"
overlay = tmp_path / "overlay"
local.mkdir()
overlay.mkdir()
(local / "manifest.yaml").write_text("profiles:\n local:\n os: linux\n")
(overlay / "packages.yaml").write_text("packages:\n - name: fd\n type: pkg\n")
data = load_manifest(local, overlay)
assert "profiles" in data
assert "packages" in data

View File

@@ -33,6 +33,20 @@ class TestParseProfile:
profile = parse_profile("test", raw)
assert len(profile.ssh_keys) == 1
def test_ssh_keygen_alias(self):
raw = {"ssh-keygen": [{"filename": "id_work", "type": "ed25519"}]}
profile = parse_profile("test", raw)
assert profile.ssh_keys[0]["path"] == "~/.ssh/id_work"
def test_requires_alias(self):
profile = parse_profile("test", {"requires": ["USER_EMAIL"]})
assert profile.env_required == ("USER_EMAIL",)
def test_post_link_and_dotfiles_profile(self):
profile = parse_profile("test", {"dotfiles-profile": "linux-work", "post-link": "echo done"})
assert profile.dotfiles_profile == "linux-work"
assert profile.post_link == "echo done"
class TestPlanBootstrap:
def test_basic_plan(self):
@@ -73,6 +87,16 @@ class TestPlanBootstrap:
runcmd_actions = [a for a in plan.actions if "custom command" in a.description.lower()]
assert len(runcmd_actions) == 1
def test_post_link_produces_action(self):
profile = Profile(
name="test", os="linux", arch=None,
hostname=None, locale=None, shell=None,
ssh_keys=[], runcmd=[], packages=[], env_required=[],
post_link="echo done",
)
plan = plan_bootstrap(profile, {})
assert any(action.phase == "post-link" for action in plan.actions)
def test_ssh_keys_action(self):
profile = Profile(
name="test", os="linux", arch=None,

View File

@@ -15,7 +15,7 @@ class TestParseImageRef:
def test_simple_name(self):
ref = parse_image_ref("devbox")
assert ref.registry == "registry.tomastm.com"
assert ref.name == "devbox"
assert ref.repo == "devbox"
assert ref.tag == "latest"
def test_with_tag(self):
@@ -25,7 +25,7 @@ class TestParseImageRef:
def test_full_ref(self):
ref = parse_image_ref("ghcr.io/user/image:main")
assert ref.registry == "ghcr.io"
assert ref.name == "user/image"
assert ref.repo == "user/image"
assert ref.tag == "main"
def test_full_image_string(self):
@@ -35,38 +35,35 @@ class TestParseImageRef:
class TestContainerName:
def test_basic(self):
assert container_name("personal", "devbox") == "flow-personal-devbox"
assert container_name("devbox") == "dev-devbox"
class TestResolveMounts:
def test_projects_mount(self, tmp_path):
projects = tmp_path / "projects"
projects.mkdir()
mounts = resolve_mounts(tmp_path, str(projects))
project_mounts = [m for m in mounts if m.target == "/home/user/projects"]
mounts = resolve_mounts(tmp_path, project_path=str(projects))
project_mounts = [m for m in mounts if m.target == "/workspace"]
assert len(project_mounts) == 1
def test_extra_mounts(self, tmp_path):
mounts = resolve_mounts(
tmp_path, str(tmp_path),
extra_mounts=[{"source": str(tmp_path), "target": "/data"}],
)
extra = [m for m in mounts if m.target == "/data"]
assert len(extra) == 1
def test_dotfiles_mount(self, tmp_path):
dotfiles = tmp_path / "dotfiles"
dotfiles.mkdir()
mounts = resolve_mounts(tmp_path, dotfiles_dir=dotfiles)
assert any(m.target.endswith("/flow/dotfiles") for m in mounts)
class TestBuildContainerSpec:
def test_basic(self):
image = ImageRef(registry="reg", name="img", tag="v1")
spec = build_container_spec("personal", image, [])
assert spec.name == "flow-personal-img"
assert spec.env["DF_NAMESPACE"] == "personal"
assert spec.env["DF_PLATFORM"] == "container"
image = ImageRef(registry="reg", repo="img", tag="v1", label="reg/img")
spec = build_container_spec("api", image, [])
assert spec.name == "dev-api"
assert spec.labels["dev.name"] == "api"
def test_with_mounts(self):
image = ImageRef(registry="reg", name="img", tag="v1")
image = ImageRef(registry="reg", repo="img", tag="v1", label="reg/img")
mounts = [Mount(source=Path("/a"), target="/b")]
spec = build_container_spec("ns", image, mounts)
spec = build_container_spec("api", image, mounts)
assert len(spec.mounts) == 1

View File

@@ -4,16 +4,20 @@ import pytest
from flow.core.errors import ConfigError, FlowError
from flow.domain.packages.catalog import normalize_profile_entry, parse_catalog
from flow.domain.packages.planning import plan_install
from flow.domain.packages.resolution import (
binary_template_context,
detect_package_manager,
pm_cask_install_command,
pm_install_command,
pm_update_command,
resolve_binary_asset,
resolve_download_url,
resolve_extract_dir,
resolve_source_name,
resolve_spec,
)
from flow.domain.packages.models import PackageDef, ProfilePackageRef
from flow.domain.packages.models import InstalledState, PackageDef, ProfilePackageRef
class TestParseCatalog:
@@ -77,6 +81,26 @@ class TestResolveSpec:
assert result.name == "unknown"
assert result.type == "binary"
def test_profile_object_overrides_catalog(self):
catalog = {"docker": PackageDef(
name="docker", type="pkg", sources={"apt": "docker-ce"},
source=None, version=None, asset_pattern=None,
platform_map={}, extract_dir=None, install={},
post_install=None, allow_sudo=False,
)}
ref = ProfilePackageRef(
name="docker",
type=None,
source=None,
version=None,
asset_pattern=None,
post_install="sudo groupadd docker || true",
allow_sudo=True,
)
result = resolve_spec(ref, catalog)
assert result.post_install == "sudo groupadd docker || true"
assert result.allow_sudo is True
class TestResolveSourceName:
def test_with_pm_mapping(self):
@@ -125,6 +149,19 @@ class TestResolveBinaryAsset:
assert "x64" in result
assert "linux" in result
def test_double_brace_pattern_uses_platform_map_context(self):
pkg = PackageDef(
name="nvim", type="binary", sources={},
source="github:neovim/neovim",
version="0.10.4",
asset_pattern="nvim-{{os}}-{{arch}}.tar.gz",
platform_map={"linux-x64": {"os": "linux", "arch": "x86_64"}},
extract_dir="nvim-{{os}}64", install={},
post_install=None, allow_sudo=False,
)
assert resolve_binary_asset(pkg, "linux-x64") == "nvim-linux-x86_64.tar.gz"
assert resolve_extract_dir(pkg, "linux-x64") == "nvim-linux64"
class TestResolveDownloadUrl:
def test_github_shorthand_with_version(self):
@@ -140,6 +177,18 @@ class TestResolveDownloadUrl:
assert "github.com/neovim/neovim" in url
assert "v0.10.4" in url
def test_github_shorthand_prefixes_v(self):
pkg = PackageDef(
name="nvim", type="binary", sources={},
source="github:neovim/neovim",
version="0.10.4",
asset_pattern=None, platform_map={},
extract_dir=None, install={},
post_install=None, allow_sudo=False,
)
url = resolve_download_url(pkg, "nvim.tar.gz", "linux-x64")
assert "/download/v0.10.4/" in url
def test_github_latest(self):
pkg = PackageDef(
name="nvim", type="binary", sources={},
@@ -181,7 +230,24 @@ class TestPmCommands:
cmd = pm_install_command("apt", ["fd-find"])
assert "apt-get install" in cmd
def test_brew_cask_install(self):
cmd = pm_cask_install_command("brew", ["wezterm"])
assert "--cask" in cmd
assert "wezterm" in cmd
def test_detect_package_manager_returns_something(self):
# Just verify it doesn't error
result = detect_package_manager()
assert result is None or result in ("apt", "dnf", "brew")
class TestPlanning:
def test_cask_package_is_planned(self):
pkg = PackageDef(
name="wezterm", type="cask", sources={"brew": "wezterm"},
source=None, version=None, asset_pattern=None,
platform_map={}, extract_dir=None, install={},
post_install=None, allow_sudo=False,
)
plan = plan_install([pkg], InstalledState(), "macos-arm64", "brew")
assert plan.install_ops[0].method == "cask"

View File

@@ -16,7 +16,14 @@ from flow.domain.remote.resolution import (
class TestParseTarget:
def test_valid_spec(self):
ns, plat = parse_target("personal@orb")
user, ns, plat = parse_target("personal@orb")
assert user is None
assert ns == "personal"
assert plat == "orb"
def test_valid_spec_with_user(self):
user, ns, plat = parse_target("alice@personal@orb")
assert user == "alice"
assert ns == "personal"
assert plat == "orb"
@@ -32,33 +39,40 @@ class TestParseTarget:
class TestResolveTarget:
def test_found(self):
targets = [TargetConfig(namespace="personal", platform="orb", host="personal.orb")]
result = resolve_target("personal@orb", targets)
result = resolve_target("personal@orb", targets, default_user="tomas")
assert result.host == "personal.orb"
assert result.label == "personal@orb"
assert result.user == "tomas"
def test_not_found(self):
with pytest.raises(FlowError, match="Unknown target"):
resolve_target("missing@host", [])
resolve_target("missing@host", [], default_user="tomas")
def test_falls_back_to_host_template(self):
result = resolve_target("personal@orb", [], default_user="tomas")
assert result.host == "personal.orb"
class TestBuildSSHCommand:
def test_basic(self):
target = Target(namespace="personal", platform="orb", host="personal.orb")
target = Target(namespace="personal", platform="orb", host="personal.orb", user="tomas")
cmd = build_ssh_command(target)
assert "ssh" in cmd.argv
assert "personal.orb" in cmd.argv
assert cmd.destination == "tomas@personal.orb"
assert cmd.env["DF_NAMESPACE"] == "personal"
assert "tmux" in cmd.argv
def test_with_identity(self):
target = Target(namespace="work", platform="ec2", host="work.ec2", identity="~/.ssh/id_work")
target = Target(namespace="work", platform="ec2", host="work.ec2", identity="~/.ssh/id_work", user="tomas")
cmd = build_ssh_command(target)
assert "-i" in cmd.argv
assert "~/.ssh/id_work" in cmd.argv
def test_with_remote_command(self):
target = Target(namespace="p", platform="o", host="h")
cmd = build_ssh_command(target, remote_command="ls -la")
assert cmd.argv[-1] == "ls -la"
def test_without_tmux(self):
target = Target(namespace="p", platform="o", host="h", user="tomas")
cmd = build_ssh_command(target, no_tmux=True)
assert "tmux" not in cmd.argv
assert cmd.destination == "tomas@h"
class TestListTargets:
@@ -73,7 +87,6 @@ class TestListTargets:
class TestTerminfoFix:
def test_returns_commands(self):
cmds = terminfo_fix_command()
assert len(cmds) == 2
assert "infocmp" in cmds[0]
def test_returns_command(self):
cmd = terminfo_fix_command()
assert "infocmp" in cmd

View File

@@ -1,7 +1,5 @@
"""Tests for BootstrapService."""
from pathlib import Path
import pytest
from flow.core.config import AppConfig, FlowContext
@@ -65,3 +63,61 @@ class TestBootstrapService:
svc = BootstrapService(ctx)
svc.list_profiles()
assert "No profiles" in capsys.readouterr().out
def test_run_preserves_profile_package_overrides(self, monkeypatch):
captured = {}
class StubPackageService:
def __init__(self, ctx):
pass
def install(self, packages, *, dry_run=False):
captured["packages"] = packages
monkeypatch.setattr("flow.services.packages.PackageService", StubPackageService)
monkeypatch.setattr("flow.services.dotfiles.DotfilesService.link", lambda self, profile=None: None)
manifest = {
"profiles": {
"linux-auto": {
"os": "linux",
"packages": [{
"name": "docker",
"allow-sudo": True,
"post-install": "sudo groupadd docker || true",
}],
},
},
"packages": [{"name": "docker", "type": "pkg", "sources": {"apt": "docker-ce"}}],
}
ctx = _make_ctx(manifest)
BootstrapService(ctx).run("linux-auto")
assert captured["packages"][0].allow_sudo is True
assert captured["packages"][0].post_install == "sudo groupadd docker || true"
def test_run_uses_dotfiles_profile_override(self, monkeypatch):
captured = {}
monkeypatch.setattr("flow.services.packages.PackageService.install", lambda self, packages, dry_run=False: None)
class StubDotfilesService:
def __init__(self, ctx):
pass
def link(self, profile=None):
captured["profile"] = profile
monkeypatch.setattr("flow.services.dotfiles.DotfilesService", StubDotfilesService)
manifest = {
"profiles": {
"linux-auto": {
"os": "linux",
"dotfiles-profile": "linux-work",
},
},
}
ctx = _make_ctx(manifest)
BootstrapService(ctx).run("linux-auto")
assert captured["profile"] == "linux-work"

View File

@@ -1,13 +1,11 @@
"""Tests for ContainerService."""
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
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, FileSystem, SystemRuntime
from flow.core.runtime import CommandRunner, SystemRuntime
from flow.core import paths
from flow.services.containers import ContainerService
@@ -19,6 +17,11 @@ class FakeRunner(CommandRunner):
def run(self, argv, *, cwd=None, env=None, capture_output=True, check=False, timeout=None):
self.calls.append(("run", list(argv)))
command = list(argv)
if command[:4] == ["docker", "container", "ls", "-a"]:
return subprocess.CompletedProcess(argv, 0, stdout="dev-api\n", stderr="")
if command[:3] == ["docker", "container", "ls"]:
return subprocess.CompletedProcess(argv, 0, stdout="dev-api\n", stderr="")
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
def run_shell(self, command, *, cwd=None, env=None, capture_output=True, check=False, timeout=None):
@@ -43,31 +46,35 @@ class TestContainerService:
def test_create_dry_run(self, tmp_path, capsys, monkeypatch):
monkeypatch.setattr(paths, "HOME", tmp_path)
monkeypatch.setattr(paths, "DOTFILES_DIR", tmp_path / "dotfiles")
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
ctx = _make_ctx(tmp_path)
svc = ContainerService(ctx)
svc.create("devbox", "personal", dry_run=True)
svc.create("api", "tm0/node", dry_run=True)
output = capsys.readouterr().out
assert "devbox" in output
assert "dev-api" in output
def test_list_no_docker(self, tmp_path, capsys):
def test_list_no_containers(self, tmp_path, capsys, monkeypatch):
runner = FakeRunner()
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
runner.run = lambda argv, **kwargs: subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
ctx = _make_ctx(tmp_path, runner=runner)
svc = ContainerService(ctx)
svc.list()
# FakeRunner returns empty stdout -> "No flow containers"
output = capsys.readouterr().out
assert "No flow containers" in output
def test_stop_calls_docker(self, tmp_path):
def test_stop_calls_docker(self, tmp_path, monkeypatch):
runner = FakeRunner()
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
ctx = _make_ctx(tmp_path, runner=runner)
svc = ContainerService(ctx)
svc.stop("flow-personal-devbox")
svc.stop("api")
assert any("docker" in str(c) and "stop" in str(c) for c in runner.calls)
def test_remove_calls_docker(self, tmp_path):
def test_remove_calls_docker(self, tmp_path, monkeypatch):
runner = FakeRunner()
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
ctx = _make_ctx(tmp_path, runner=runner)
svc = ContainerService(ctx)
svc.remove("flow-personal-devbox")
svc.remove("api")
assert any("docker" in str(c) and "rm" in str(c) for c in runner.calls)

View File

@@ -1,18 +1,28 @@
"""Tests for DotfilesService."""
import subprocess
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.runtime import CommandRunner, 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="")
def _make_ctx(tmp_path, console=None):
"""Build a FlowContext for testing."""
return FlowContext(
@@ -170,3 +180,59 @@ class TestDotfilesServiceLink:
svc.status()
output = capsys.readouterr().out
assert "zsh" in output
def test_relink_does_not_remove_unmanaged_file(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()
target = home / ".zshrc"
target.unlink()
target.write_text("user managed file")
svc.link()
assert target.read_text() == "user managed file"
assert not target.is_symlink()
def test_sync_modules_includes_profile_layers(self, tmp_path, monkeypatch):
home = tmp_path / "home"
home.mkdir()
dotfiles = tmp_path / "dotfiles"
profile_pkg = dotfiles / "linux-work" / "nvim" / ".config" / "nvim"
profile_pkg.mkdir(parents=True)
(profile_pkg / "_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", 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).sync_modules()
assert any("linux-work--nvim" in " ".join(call) for call in runner.calls)

View File

@@ -1,5 +1,7 @@
"""Tests for PackageService."""
import io
import tarfile
from pathlib import Path
import pytest
@@ -63,3 +65,68 @@ class TestPackageService:
svc = PackageService(ctx)
with pytest.raises(FlowError, match="Specify"):
svc.install()
def test_list_all_known_packages(self, tmp_path, monkeypatch, capsys):
monkeypatch.setattr(paths, "INSTALLED_STATE", tmp_path / "installed.json")
manifest = {"packages": [{"name": "fd", "type": "pkg"}]}
ctx = _make_ctx(tmp_path, manifest)
svc = PackageService(ctx)
svc.list_packages(show_all=True)
assert "fd" in capsys.readouterr().out
def test_install_binary_honors_declared_install_map(self, tmp_path, monkeypatch):
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
monkeypatch.setattr(paths, "DATA_DIR", tmp_path / "data")
monkeypatch.setattr(paths, "INSTALLED_STATE", tmp_path / "installed.json")
archive = io.BytesIO()
with tarfile.open(fileobj=archive, mode="w:gz") as tar:
files = {
"nvim-linux64/bin/nvim": b"#!/bin/sh\n",
"nvim-linux64/share/nvim/runtime.txt": b"runtime\n",
"nvim-linux64/share/man/man1/nvim.1": b"manpage\n",
}
for name, content in files.items():
info = tarfile.TarInfo(name=name)
info.size = len(content)
tar.addfile(info, io.BytesIO(content))
archive_bytes = archive.getvalue()
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
return archive_bytes
monkeypatch.setattr("flow.services.packages.urllib.request.urlopen", lambda *args, **kwargs: FakeResponse())
manifest = {
"packages": [{
"name": "neovim",
"type": "binary",
"source": "github:neovim/neovim",
"version": "0.10.4",
"asset-pattern": "nvim-{{os}}-{{arch}}.tar.gz",
"platform-map": {"linux-x64": {"os": "linux", "arch": "x64"}},
"extract-dir": "nvim-{{os}}64",
"install": {
"bin": ["bin/nvim"],
"share": ["share/nvim"],
"man": ["share/man/man1/nvim.1"],
},
}],
}
ctx = _make_ctx(tmp_path, manifest)
svc = PackageService(ctx)
packages = svc.resolve_install_packages(package_names=["neovim"])
svc.install(packages)
assert (home / ".local" / "bin" / "nvim").exists()
assert (home / ".local" / "share" / "nvim" / "runtime.txt").exists()
assert (home / ".local" / "share" / "man" / "man1" / "nvim.1").exists()

View File

@@ -29,6 +29,8 @@ class TestRemoteService:
output = capsys.readouterr().out
assert "personal@orb" in output
assert "ssh" in output
assert "tmux" in output
assert "DF_NAMESPACE=personal" in output
def test_enter_unknown_target(self):
ctx = _make_ctx()