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