refactor: fail loud, tighten types, remove speculative abstraction

Fail loud at the boundary:
- substitute_template raises ConfigError on unresolved {{...}}; no more
  silent literal placeholders in download URLs.
- parse_profile raises ConfigError when 'os' is missing -- no
  raw.get("os", "linux") default that silently masks typos.
- urllib download failures wrapped to FlowError.
- bootstrap _execute_action dispatches phases explicitly and raises
  on unhandled phase; no more "anything else runs as shell".

Direct access over defensive wrapping:
- plan_bootstrap requires env; plan_install requires pm. Drop the
  dead `or os.environ` / `or detect_package_manager()` fallbacks.
- InstalledState.from_dict raises ConfigError on missing fields
  rather than .get(..., default).
- Replace `x or {}` chains with explicit `x if x is not None else {}`
  in package resolution; catalog validates type/platform-map/install
  shapes at parse.

One canonical form / direct access:
- Path.home() replaced with paths.HOME in services/packages.py and
  commands/completion.py. paths.HOME is the single source now.
- Use Path.is_relative_to for install-path containment instead of
  str.startswith.

Domain purity:
- domain/containers/resolution.resolve_mounts takes a filesystem_check
  predicate; service passes the probe in. Domain no longer touches
  the filesystem directly.

No speculative abstraction:
- Drop the `allow_sudo` field entirely. The _script_uses_sudo check
  it gated was bypassable (substring match) and gave false confidence;
  the manifest is fully user-trusted anyway.
- Delete dead terminfo_fix_command + RemoteService.fix_terminfo
  (no command surface exposes them).
- FileSystem.remove_tree no longer swallows errors via ignore_errors;
  callers opt into missing_ok if needed.

Typed enums:
- PackageDef.type, AppConfig.container_runtime as Literal[...].
  container_runtime values validated at config parse.

Completion bypasses runtime no longer:
- complete(ctx, ...) threads context; ContainerRuntime and state-file
  reads go through ctx.runtime instead of constructing primitives.

Tests added for: template raise, missing os raise, env/pm required,
unknown phase raise, no allow_sudo gate, URL download failure, install
path escape, corrupt installed.json, container_runtime Literal,
filesystem_check controls mounts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 00:02:06 +03:00
parent c0e2758057
commit a71742afee
26 changed files with 429 additions and 214 deletions

View File

@@ -1,5 +1,7 @@
"""Tests for bootstrap planning."""
import inspect
import pytest
from flow.core.errors import ConfigError
@@ -22,33 +24,45 @@ class TestParseProfile:
assert profile.shell == "zsh"
assert len(profile.packages) == 2
def test_defaults(self):
profile = parse_profile("minimal", {})
def test_missing_os_raises(self):
with pytest.raises(ConfigError, match=r"Profile 'minimal': required field 'os' is missing"):
parse_profile("minimal", {})
def test_optional_fields_default(self):
profile = parse_profile("minimal", {"os": "linux"})
assert profile.os == "linux"
assert profile.hostname is None
assert profile.packages == ()
def test_ssh_keys(self):
raw = {"ssh-keys": [{"path": "~/.ssh/id_ed25519", "type": "ed25519"}]}
raw = {"os": "linux", "ssh-keys": [{"path": "~/.ssh/id_ed25519", "type": "ed25519"}]}
profile = parse_profile("test", raw)
assert len(profile.ssh_keys) == 1
def test_ssh_keys_with_filename(self):
raw = {"ssh-keys": [{"filename": "id_work", "type": "ed25519"}]}
raw = {"os": "linux", "ssh-keys": [{"filename": "id_work", "type": "ed25519"}]}
profile = parse_profile("test", raw)
assert profile.ssh_keys[0]["path"] == "~/.ssh/id_work"
def test_env_required(self):
profile = parse_profile("test", {"env-required": ["USER_EMAIL"]})
profile = parse_profile("test", {"os": "linux", "env-required": ["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"})
profile = parse_profile(
"test",
{"os": "linux", "dotfiles-profile": "linux-work", "post-link": "echo done"},
)
assert profile.dotfiles_profile == "linux-work"
assert profile.post_link == "echo done"
class TestPlanBootstrap:
def test_env_is_required_keyword(self):
sig = inspect.signature(plan_bootstrap)
param = sig.parameters["env"]
assert param.default is inspect.Parameter.empty
def test_basic_plan(self):
profile = Profile(
name="test", os="linux", arch=None,
@@ -57,7 +71,7 @@ class TestPlanBootstrap:
packages=["fd"], env_required=[],
)
manifest = {"packages": [{"name": "fd", "type": "pkg"}]}
plan = plan_bootstrap(profile, manifest)
plan = plan_bootstrap(profile, manifest, env={})
assert plan.profile == "test"
assert plan.total_steps > 0
phases = [a.phase for a in plan.actions]
@@ -65,8 +79,7 @@ class TestPlanBootstrap:
assert "packages" in phases
assert "dotfiles" in phases
def test_missing_env_raises(self, monkeypatch):
monkeypatch.delenv("REQUIRED_VAR", raising=False)
def test_missing_env_raises(self):
profile = Profile(
name="test", os="linux", arch=None,
hostname=None, locale=None, shell=None,
@@ -74,7 +87,7 @@ class TestPlanBootstrap:
env_required=["REQUIRED_VAR"],
)
with pytest.raises(ConfigError, match="REQUIRED_VAR"):
plan_bootstrap(profile, {})
plan_bootstrap(profile, {}, env={})
def test_runcmd_produces_action(self):
profile = Profile(
@@ -83,7 +96,7 @@ class TestPlanBootstrap:
ssh_keys=[], runcmd=["echo hello", "echo world"],
packages=[], env_required=[],
)
plan = plan_bootstrap(profile, {})
plan = plan_bootstrap(profile, {}, env={})
runcmd_actions = [a for a in plan.actions if "custom command" in a.description.lower()]
assert len(runcmd_actions) == 1
@@ -94,7 +107,7 @@ class TestPlanBootstrap:
ssh_keys=[], runcmd=[], packages=[], env_required=[],
post_link="echo done",
)
plan = plan_bootstrap(profile, {})
plan = plan_bootstrap(profile, {}, env={})
assert any(action.phase == "post-link" for action in plan.actions)
def test_ssh_keys_action(self):
@@ -104,6 +117,6 @@ class TestPlanBootstrap:
ssh_keys=[{"path": "~/.ssh/id", "type": "ed25519"}],
runcmd=[], packages=[], env_required=[],
)
plan = plan_bootstrap(profile, {})
plan = plan_bootstrap(profile, {}, env={})
ssh_actions = [a for a in plan.actions if "SSH" in a.description]
assert len(ssh_actions) == 1