141 lines
4.1 KiB
Python
141 lines
4.1 KiB
Python
"""Tests for the canonical action executor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
|
|
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
|
|
from flow.core.platform import PlatformInfo
|
|
from flow.core.runtime import SystemRuntime
|
|
|
|
|
|
def _ctx() -> FlowContext:
|
|
return FlowContext(
|
|
config=AppConfig(),
|
|
manifest={},
|
|
platform=PlatformInfo(),
|
|
console=Console(color=False),
|
|
runtime=SystemRuntime(),
|
|
)
|
|
|
|
|
|
def test_dry_run_does_not_mutate(tmp_path):
|
|
target = tmp_path / "out.txt"
|
|
plan = ActionPlan(
|
|
name="dry-run",
|
|
primitive_actions=(
|
|
PrimitiveAction(
|
|
id="write",
|
|
type="file.write",
|
|
description="Write a file",
|
|
payload={"path": target, "content": "hello"},
|
|
),
|
|
),
|
|
)
|
|
|
|
summary = ActionExecutor(_ctx(), audit_path=tmp_path / "actions.jsonl").execute(
|
|
plan,
|
|
dry_run=True,
|
|
)
|
|
|
|
assert not target.exists()
|
|
assert summary.results[0].status == "dry-run"
|
|
|
|
|
|
def test_audit_jsonl_records_success(tmp_path):
|
|
target = tmp_path / "out.txt"
|
|
audit_path = tmp_path / "actions.jsonl"
|
|
plan = ActionPlan(
|
|
name="write-plan",
|
|
primitive_actions=(
|
|
PrimitiveAction(
|
|
id="write",
|
|
type="file.write",
|
|
description="Write a file",
|
|
payload={"path": target, "content": "hello"},
|
|
),
|
|
),
|
|
)
|
|
|
|
ActionExecutor(_ctx(), audit_path=audit_path).execute(plan)
|
|
|
|
records = [json.loads(line) for line in audit_path.read_text().splitlines()]
|
|
assert [record["event"] for record in records] == [
|
|
"plan_start",
|
|
"action_start",
|
|
"action_success",
|
|
"plan_success",
|
|
]
|
|
assert target.read_text() == "hello"
|
|
|
|
|
|
def test_rollback_removes_created_symlink_after_failure(tmp_path):
|
|
source = tmp_path / "source"
|
|
target = tmp_path / "target"
|
|
source.write_text("managed")
|
|
plan = ActionPlan(
|
|
name="rollback",
|
|
primitive_actions=(
|
|
PrimitiveAction(
|
|
id="link",
|
|
type="file.create_symlink",
|
|
description="Create managed link",
|
|
payload={"source": source, "target": target},
|
|
),
|
|
PrimitiveAction(
|
|
id="copy-missing",
|
|
type="file.copy",
|
|
description="Fail on missing source",
|
|
payload={"source": tmp_path / "missing", "target": tmp_path / "dest"},
|
|
),
|
|
),
|
|
)
|
|
|
|
with pytest.raises(FlowError, match="missing"):
|
|
ActionExecutor(_ctx(), audit_path=tmp_path / "actions.jsonl").execute(plan)
|
|
|
|
assert not target.exists()
|
|
assert not target.is_symlink()
|
|
|
|
|
|
def test_barrier_prevents_rollback_across_external_boundary(tmp_path):
|
|
source = tmp_path / "source"
|
|
target = tmp_path / "target"
|
|
source.write_text("managed")
|
|
plan = ActionPlan(
|
|
name="barrier",
|
|
primitive_actions=(
|
|
PrimitiveAction(
|
|
id="link",
|
|
type="file.create_symlink",
|
|
description="Create managed link",
|
|
payload={"source": source, "target": target},
|
|
),
|
|
PrimitiveAction(
|
|
id="barrier",
|
|
type="process.argv",
|
|
description="External boundary",
|
|
payload={"argv": (sys.executable, "-c", "")},
|
|
rollback_policy=RollbackPolicy.BARRIER,
|
|
),
|
|
PrimitiveAction(
|
|
id="copy-missing",
|
|
type="file.copy",
|
|
description="Fail on missing source",
|
|
payload={"source": tmp_path / "missing", "target": tmp_path / "dest"},
|
|
),
|
|
),
|
|
)
|
|
|
|
with pytest.raises(FlowError, match="missing"):
|
|
ActionExecutor(_ctx(), audit_path=tmp_path / "actions.jsonl").execute(plan)
|
|
|
|
assert target.is_symlink()
|
|
|