Clean action runtime project state

This commit is contained in:
2026-05-14 13:58:45 +03:00
parent b05d3589b7
commit 4ce98d0ff1
27 changed files with 711 additions and 4772 deletions

View File

@@ -7,12 +7,15 @@ import sys
import pytest
from flow.actions import ActionExecutor, ActionPlan, PrimitiveAction, RollbackPolicy
from flow.actions import ActionExecutor, ActionPlan, DomainAction, PrimitiveAction, RollbackPolicy
from flow.adapters.containers import ContainerRuntime
from flow.adapters.tmux import TmuxClient
from flow.core.config import AppConfig, FlowContext
from flow.core.console import Console
from flow.core.errors import FlowError
from flow.core.platform import PlatformInfo
from flow.core.runtime import SystemRuntime
from tests.fakes import FakeRunner
def _ctx() -> FlowContext:
@@ -138,3 +141,72 @@ def test_barrier_prevents_rollback_across_external_boundary(tmp_path):
assert target.is_symlink()
def test_domain_action_expands_embedded_primitives(tmp_path):
target = tmp_path / "completion"
primitive = PrimitiveAction(
id="completion.write",
type="file.write",
description="Write completion",
payload={"path": target, "content": "complete"},
)
plan = ActionPlan(
name="completion.install",
domain_actions=(
DomainAction(
id="completion.install",
kind="completion",
action="install-zsh",
description="Install completion",
payload={"primitive_actions": (primitive,)},
),
),
)
ActionExecutor(_ctx(), audit_path=tmp_path / "actions.jsonl").execute(plan)
assert target.read_text() == "complete"
def test_executor_dispatches_container_and_tmux_primitives(tmp_path):
runner = FakeRunner()
ctx = _ctx()
ctx.runtime.runner = runner
ctx.runtime.containers = ContainerRuntime(runner, binary="docker")
ctx.runtime.tmux = TmuxClient(runner)
plan = ActionPlan(
name="runtime-dispatch",
primitive_actions=(
PrimitiveAction(
id="container.exec",
type="container.exec",
description="Run command in container",
payload={"name": "dev-api", "argv": ("echo", "hello")},
),
PrimitiveAction(
id="tmux.session",
type="tmux.new_session",
description="Create session",
payload={"name": "dev-api", "detached": True, "command": "flow dev exec api"},
),
PrimitiveAction(
id="tmux.option",
type="tmux.set_option",
description="Set option",
payload={
"session": "dev-api",
"option": "default-command",
"value": "flow dev exec api",
},
),
),
)
summary = ActionExecutor(ctx, audit_path=tmp_path / "actions.jsonl").execute(plan)
assert summary.results[0].returncode == 0
assert ["docker", "exec", "dev-api", "echo", "hello"] in runner.calls
assert ["tmux", "new-session", "-ds", "dev-api", "flow dev exec api"] in runner.calls
assert [
"tmux", "set-option", "-t", "dev-api", "default-command", "flow dev exec api",
] in runner.calls

View File

@@ -2,7 +2,8 @@
import subprocess
from flow.app.completion import complete
from flow.app.completion import complete, install_zsh_completion
from flow.core import paths
from flow.core.config import AppConfig, FlowContext
from flow.core.console import Console
from flow.core.platform import PlatformInfo
@@ -96,3 +97,20 @@ def test_complete_dev_attach_returns_empty_on_timeout():
ctx.runtime.containers.ps = fake_ps # type: ignore[method-assign]
result = complete(ctx, ["flow", "dev", "attach", ""], 3)
assert result == []
def test_install_zsh_completion_uses_action_runtime(tmp_path, monkeypatch):
monkeypatch.setattr(paths, "HOME", tmp_path)
monkeypatch.setattr(paths, "STATE_DIR", tmp_path / "state")
ctx = _make_ctx()
completion_file = install_zsh_completion(
ctx,
directory=str(tmp_path / "completions"),
rc=str(tmp_path / ".zshrc"),
)
assert completion_file == tmp_path / "completions" / "_flow"
assert "compdef _flow flow" in completion_file.read_text()
assert "# >>> flow completion >>>" in (tmp_path / ".zshrc").read_text()
assert (tmp_path / "state" / "actions.jsonl").exists()

View File

@@ -2,7 +2,8 @@
import subprocess
from flow.adapters.tmux import TmuxClient, build_new_session_argv
from flow.adapters.tmux import TmuxClient
from flow.domain.remote.resolution import build_remote_tmux_argv
from tests.fakes import FakeRunner
@@ -78,11 +79,11 @@ class TestTmuxClient:
class TestBuildNewSessionArgv:
def test_basic(self):
argv = build_new_session_argv("default")
argv = build_remote_tmux_argv("default")
assert argv == ["tmux", "new-session", "-As", "default"]
def test_with_env(self):
argv = build_new_session_argv(
argv = build_remote_tmux_argv(
"main",
env={"DF_NAMESPACE": "personal", "DF_PLATFORM": "orb"},
)
@@ -93,5 +94,5 @@ class TestBuildNewSessionArgv:
]
def test_empty_env(self):
argv = build_new_session_argv("sess", env={})
argv = build_remote_tmux_argv("sess", env={})
assert argv == ["tmux", "new-session", "-As", "sess"]

View File

@@ -1,12 +1,14 @@
"""Tests for packages catalog and resolution."""
from flow.adapters.packages import (
detect_package_manager,
pm_cask_install_argv,
pm_install_argv,
pm_update_argv,
)
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 (
detect_package_manager,
pm_cask_install_command,
pm_install_command,
pm_update_command,
resolve_binary_asset,
resolve_download_url,
resolve_extract_dir,
@@ -210,24 +212,23 @@ class TestResolveDownloadUrl:
class TestPmCommands:
def test_apt_update(self):
assert "apt-get update" in pm_update_command("apt")
assert pm_update_argv("apt") == ["sudo", "apt-get", "update", "-qq"]
def test_dnf_update(self):
assert "dnf" in pm_update_command("dnf")
assert pm_update_argv("dnf") == ["sudo", "dnf", "check-update", "-q"]
def test_brew_install(self):
cmd = pm_install_command("brew", ["fd", "rg"])
assert "brew install" in cmd
assert "fd" in cmd
assert pm_install_argv("brew", ["fd", "rg"]) == ["brew", "install", "fd", "rg"]
def test_apt_install(self):
cmd = pm_install_command("apt", ["fd-find"])
assert "apt-get install" in cmd
assert pm_install_argv("apt", ["fd-find"]) == [
"sudo", "apt-get", "install", "-y", "-qq", "fd-find",
]
def test_brew_cask_install(self):
cmd = pm_cask_install_command("brew", ["wezterm"])
assert "--cask" in cmd
assert "wezterm" in cmd
assert pm_cask_install_argv("brew", ["wezterm"]) == [
"brew", "install", "--cask", "wezterm",
]
def test_detect_package_manager_returns_something(self):
# Just verify it doesn't error

View File

@@ -66,3 +66,17 @@ class TestContainerService:
svc = ContainerService(ctx)
svc.remove("api")
assert any("docker" in str(c) and "rm" in str(c) for c in runner.calls)
def test_exec_command_uses_container_exec_action(self, tmp_path):
runner = FakeRunner(responses={
("ps", "{{.Names}}"): subprocess.CompletedProcess([], 0, stdout="dev-api\n"),
})
ctx = _make_ctx(tmp_path, runner=runner)
svc = ContainerService(ctx)
try:
svc.exec("api", ["echo", "hello"])
except SystemExit as e:
assert e.code == 0
assert ["docker", "exec", "dev-api", "echo", "hello"] in runner.calls

View File

@@ -7,6 +7,7 @@ from pathlib import Path
import pytest
from flow.actions import ActionExecutor, ActionPlan, PrimitiveAction, RollbackPolicy
from flow.core.config import AppConfig, FlowContext
from flow.core.console import Console
from flow.core.errors import FlowError
@@ -135,26 +136,7 @@ class TestPackageService:
def test_post_install_with_sudo_runs_unchecked(self, tmp_path, monkeypatch):
"""No allow_sudo gate -- post-install scripts run as written."""
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr(paths, "HOME", home)
monkeypatch.setattr(paths, "INSTALLED_STATE", tmp_path / "installed.json")
calls: list[str] = []
class _Runner:
def run_shell(self, command, **kwargs):
calls.append(command)
class _Result:
returncode = 0
stdout = ""
stderr = ""
return _Result()
ctx = _make_ctx(tmp_path)
ctx.runtime.runner = _Runner()
svc = PackageService(ctx)
pkg = PackageDef(
name="docker", type="pkg", sources={},
@@ -162,8 +144,11 @@ class TestPackageService:
platform_map={}, extract_dir=None, install={},
post_install="sudo groupadd docker || true",
)
svc._run_post_install(pkg)
assert calls == ["sudo groupadd docker || true"]
primitive = svc._post_install_primitive(pkg)
assert primitive is not None
assert primitive.type == "process.shell_user_hook"
assert primitive.payload["command"] == "sudo groupadd docker || true"
assert primitive.rollback_policy == RollbackPolicy.BARRIER
def test_install_binary_url_failure_raises_flow_error(self, tmp_path, monkeypatch):
home = tmp_path / "home"
@@ -226,11 +211,21 @@ class TestPackageService:
link = extract_root / "evil"
link.symlink_to(sibling)
with pytest.raises(FlowError, match="escapes extract-dir"):
svc._copy_install_item(
"pkg",
extract_root,
extract_root.resolve(),
"bin",
"evil/escape",
with pytest.raises(FlowError, match="escapes allowed root"):
ActionExecutor(ctx, audit_path=tmp_path / "actions.jsonl").execute(
ActionPlan(
name="copy-escape",
primitive_actions=(
PrimitiveAction(
id="copy",
type="file.copy",
description="Copy escaped source",
payload={
"source": link / "escape",
"target": tmp_path / "target",
"source_root": extract_root.resolve(),
},
),
),
)
)

View File

@@ -15,9 +15,9 @@ MUTATING_PATTERNS = (
COMMAND_PATTERNS = (
re.compile(r"runtime\.runner\.(run|run_shell)\("),
re.compile(r"runtime\.git\.run\("),
re.compile(r"runtime\.containers\.(run_container|start|stop|kill|rm)\("),
re.compile(r"runtime\.containers\.(run_container|start|stop|kill|rm|exec_in)\("),
re.compile(r"runtime\.tmux\.(new_session|set_option|respawn_pane)\("),
re.compile(r"self\.(rt|tmux)\.(run_container|start|stop|kill|rm|new_session|set_option|respawn_pane)\("),
re.compile(r"self\.(rt|tmux)\.(run_container|start|stop|kill|rm|exec_in|new_session|set_option|respawn_pane)\("),
)
ALLOWED_PREFIXES = (