Clean action runtime project state
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user