Files
markitect-tool/tests/test_workflow_engine.py

262 lines
7.7 KiB
Python

from pathlib import Path
from click.testing import CliRunner
from markitect_tool.cli import main
from markitect_tool.generation import GenerationHookRequest, GenerationHookResult
from markitect_tool.workflow import (
WorkflowRunner,
load_workflow_file,
resolve_workflow_bindings,
)
WORKFLOW = """# ADR Release Workflow
```yaml workflow
metadata:
id: adr-release
title: ADR Release Notes
intent:
summary: Collect accepted ADR decisions.
permissions:
filesystem:
read: [adrs, templates]
write: [out]
responsibilities:
agent:
may_run_deterministic_steps: true
inputs:
adrs:
glob: adrs/*.md
where:
frontmatter.status: accepted
extract:
decisions:
selector: sections[heading=Decision]
steps:
render:
kind: template
template: templates/release.md
data:
decisions: ${sources.adrs.extracts.decisions}
outputs:
release_notes:
path: out/release-notes.md
content: ${steps.render.markdown}
observability:
events: [workflow.started, workflow.completed]
```
"""
def _write_workflow_fixture(tmp_path: Path) -> Path:
(tmp_path / "adrs").mkdir()
(tmp_path / "templates").mkdir()
(tmp_path / "adrs" / "one.md").write_text(
"---\nstatus: accepted\n---\n# One\n\n## Decision\n\nUse the workflow engine.\n",
encoding="utf-8",
)
(tmp_path / "adrs" / "two.md").write_text(
"---\nstatus: proposed\n---\n# Two\n\n## Decision\n\nIgnore this one.\n",
encoding="utf-8",
)
(tmp_path / "templates" / "release.md").write_text(
"# Release Notes\n\n{{decisions}}\n",
encoding="utf-8",
)
workflow = tmp_path / "workflow.md"
workflow.write_text(WORKFLOW, encoding="utf-8")
return workflow
def test_load_workflow_file_preserves_standard_sections(tmp_path: Path):
workflow = _write_workflow_fixture(tmp_path)
plan = load_workflow_file(workflow)
assert plan.id == "adr-release"
assert plan.intent["summary"] == "Collect accepted ADR decisions."
assert plan.permissions["filesystem"]["read"] == ["adrs", "templates"]
assert plan.responsibilities["agent"]["may_run_deterministic_steps"] is True
assert plan.observability["events"] == ["workflow.started", "workflow.completed"]
assert plan.steps[0]["id"] == "render"
def test_load_workflow_file_preserves_policy_identity_permissions(tmp_path: Path):
workflow = tmp_path / "policy.workflow.md"
workflow.write_text(
"""# Policy Workflow
```yaml workflow
metadata:
id: policy-aware
inputs:
static:
value: ok
permissions:
policy:
subject_from_token: examples/policy/netkingdom-claims.yaml
policy_map: examples/policy/enterprise-policy-map.yaml
required_assurance:
mfa: true
emergency_justification: INC-123
decision_log: .markitect/policy-decisions.jsonl
flex_auth:
resource_manifest: examples/policy/flex-auth-resource-manifest.yaml
```
""",
encoding="utf-8",
)
plan = load_workflow_file(workflow)
assert plan.permissions["policy"]["subject_from_token"] == "examples/policy/netkingdom-claims.yaml"
assert plan.permissions["policy"]["required_assurance"]["mfa"] is True
assert plan.permissions["flex_auth"]["resource_manifest"].endswith("flex-auth-resource-manifest.yaml")
def test_workflow_runner_collects_sources_and_renders_output(tmp_path: Path):
workflow = _write_workflow_fixture(tmp_path)
plan = load_workflow_file(workflow)
result = WorkflowRunner(plan).run(dry_run=True)
assert result.valid
assert result.sources["adrs"]["count"] == 1
assert "Use the workflow engine." in result.sources["adrs"]["extracts"]["decisions"][0]
assert result.steps["render"]["complete"]
assert result.outputs[0].path.endswith("out/release-notes.md")
assert not result.outputs[0].written
assert "Ignore this one" not in result.outputs[0].content
def test_workflow_runner_writes_outputs_under_output_dir(tmp_path: Path):
workflow = _write_workflow_fixture(tmp_path)
plan = load_workflow_file(workflow)
output_dir = tmp_path / "build"
result = WorkflowRunner(plan, output_dir=output_dir).run()
assert result.valid
output = output_dir / "out" / "release-notes.md"
assert output.exists()
assert "Use the workflow engine" in output.read_text(encoding="utf-8")
assert result.outputs[0].written
def test_resolve_workflow_bindings_preserves_native_types_and_projects_lists():
context = {
"sources": {
"docs": {
"items": [
{"path": "a.md", "frontmatter": {"status": "accepted"}},
{"path": "b.md", "frontmatter": {"status": "proposed"}},
],
}
}
}
value = resolve_workflow_bindings(
{
"paths": "${sources.docs.items.path}",
"sentence": "Files: ${sources.docs.items.path}",
},
context,
)
assert value["paths"] == ["a.md", "b.md"]
assert value["sentence"] == "Files: - a.md\n- b.md"
def test_optional_assisted_step_skips_without_hook(tmp_path: Path):
workflow = tmp_path / "workflow.yaml"
workflow.write_text(
"""
metadata:
id: assisted-demo
steps:
review:
kind: assisted
optional: true
prompt_text: Review this.
outputs:
review:
content: ${steps.review.skipped}
""",
encoding="utf-8",
)
result = WorkflowRunner(load_workflow_file(workflow)).run(dry_run=True)
assert result.valid
assert result.steps["review"]["skipped"] is True
assert result.steps["review"]["diagnostics"][0]["code"] == "workflow.assisted_unavailable"
class _FakeHook:
def generate(self, request: GenerationHookRequest) -> GenerationHookResult:
return GenerationHookResult(
markdown=f"Reviewed: {request.prompt}",
provider="fake",
)
def test_assisted_step_uses_injected_hook(tmp_path: Path):
workflow = tmp_path / "workflow.yaml"
workflow.write_text(
"""
metadata:
id: assisted-demo
steps:
review:
kind: assisted
optional: false
prompt_text: Review this.
outputs:
review:
content: ${steps.review.markdown}
""",
encoding="utf-8",
)
result = WorkflowRunner(load_workflow_file(workflow), assisted_hook=_FakeHook()).run(dry_run=True)
assert result.valid
assert result.steps["review"]["provider"] == "fake"
assert result.outputs[0].content == "Reviewed: Review this."
def test_mkt_workflow_inspect_plan_and_run(tmp_path: Path):
workflow = _write_workflow_fixture(tmp_path)
runner = CliRunner()
inspected = runner.invoke(main, ["workflow", "inspect", str(workflow)])
planned = runner.invoke(main, ["workflow", "plan", str(workflow)])
run = runner.invoke(main, ["workflow", "run", str(workflow), "--output-dir", str(tmp_path / "build")])
assert inspected.exit_code == 0
assert "valid" in inspected.output
assert planned.exit_code == 0
assert "outputs: 1" in planned.output
assert run.exit_code == 0
assert "written" in run.output
def test_example_adr_release_notes_workflow_runs(tmp_path: Path):
workflow = Path("examples/workflows/adr-release-notes.workflow.md")
result = WorkflowRunner(load_workflow_file(workflow), output_dir=tmp_path).run()
assert result.valid
assert result.sources["adrs"]["count"] == 2
assert (tmp_path / "out" / "release-notes.md").exists()
assert "local SQLite backend" in (tmp_path / "out" / "release-notes.md").read_text(encoding="utf-8")
def test_example_assisted_review_workflow_is_deterministic_without_hook(tmp_path: Path):
workflow = Path("examples/workflows/assisted-review.workflow.md")
result = WorkflowRunner(load_workflow_file(workflow), output_dir=tmp_path).run(dry_run=True)
assert result.valid
assert result.steps["review"]["skipped"] is True