"""Tests for T04 — Composite workflow orchestration (FR-1300).""" from __future__ import annotations import textwrap from pathlib import Path import pytest from markidocx.evidence import EvidenceStore from markidocx.workflows import ( SUPPORTED_WORKFLOWS, WorkflowError, WorkflowResult, run_workflow, run_workflow_from_content, ) SIMPLE_MANIFEST = textwrap.dedent("""\ project: name: "Test Document" feature_level: level1 family: article sources: - path: doc.md output: dir: ./dist """) SIMPLE_MARKDOWN = textwrap.dedent("""\ # Hello World A paragraph with **bold** text. ## Section - Item one - Item two """) @pytest.fixture() def tmp_project(tmp_path: Path) -> Path: (tmp_path / "doc.md").write_text(SIMPLE_MARKDOWN, encoding="utf-8") (tmp_path / "manifest.yaml").write_text(SIMPLE_MANIFEST, encoding="utf-8") (tmp_path / "dist").mkdir() return tmp_path @pytest.fixture() def store(tmp_path: Path) -> EvidenceStore: return EvidenceStore(base_dir=tmp_path / "evidence") # --------------------------------------------------------------------------- # Known / unknown workflow names # --------------------------------------------------------------------------- def test_supported_workflows_set() -> None: assert "single-file-roundtrip" in SUPPORTED_WORKFLOWS assert "multi-file-roundtrip" in SUPPORTED_WORKFLOWS assert "release-regression" in SUPPORTED_WORKFLOWS assert "family-switch-build" in SUPPORTED_WORKFLOWS def test_unknown_workflow_raises(tmp_project: Path) -> None: with pytest.raises(WorkflowError, match="Unknown workflow"): run_workflow("no-such-workflow", tmp_project / "manifest.yaml") # --------------------------------------------------------------------------- # WorkflowResult shape (FR-1303, FR-1309) # --------------------------------------------------------------------------- def test_single_file_roundtrip_result_shape(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("single-file-roundtrip", tmp_project / "manifest.yaml", store) assert isinstance(result, WorkflowResult) assert result.run_id assert result.workflow_name == "single-file-roundtrip" assert result.timestamp assert result.classification in ("full", "with-fallback", "partial", "failed") def test_result_has_steps(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("single-file-roundtrip", tmp_project / "manifest.yaml", store) assert len(result.steps) >= 1 for step in result.steps: assert step.name assert step.status in ("executed", "skipped", "failed") def test_result_has_aggregate_output(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("single-file-roundtrip", tmp_project / "manifest.yaml", store) assert isinstance(result.aggregate_output, dict) # --------------------------------------------------------------------------- # Step visibility (FR-1304) # --------------------------------------------------------------------------- def test_single_file_roundtrip_step_names(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("single-file-roundtrip", tmp_project / "manifest.yaml", store) step_names = [s.name for s in result.steps] assert "validate" in step_names assert "build" in step_names assert "import" in step_names assert "compare" in step_names def test_failed_validate_stops_early(tmp_path: Path, store: EvidenceStore) -> None: """If validation fails, subsequent steps should not execute.""" bad_manifest = tmp_path / "manifest.yaml" bad_manifest.write_text("project:\n name: x\n", encoding="utf-8") (tmp_path / "dist").mkdir() result = run_workflow("single-file-roundtrip", bad_manifest, store) assert result.classification == "failed" step_names = [s.name for s in result.steps] assert "validate" in step_names # build should not appear after a validation failure assert "build" not in step_names # --------------------------------------------------------------------------- # Workflow classification (FR-1305) # --------------------------------------------------------------------------- def test_successful_roundtrip_classification(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("single-file-roundtrip", tmp_project / "manifest.yaml", store) assert result.classification in ("full", "with-fallback") # --------------------------------------------------------------------------- # Evidence stored (FR-1309) # --------------------------------------------------------------------------- def test_evidence_written_to_store(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("single-file-roundtrip", tmp_project / "manifest.yaml", store) runs = store.list_runs() assert result.run_id in runs def test_validation_report_in_store(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("single-file-roundtrip", tmp_project / "manifest.yaml", store) report = store.get_report(result.run_id, "validation") assert report is not None assert report.data["status"] == "ok" # --------------------------------------------------------------------------- # multi-file-roundtrip and family-switch-build # --------------------------------------------------------------------------- def test_multi_file_roundtrip_returns_result(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("multi-file-roundtrip", tmp_project / "manifest.yaml", store) assert result.workflow_name == "multi-file-roundtrip" assert result.classification in ("full", "with-fallback", "partial", "failed") def test_family_switch_build_produces_multiple_steps(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("family-switch-build", tmp_project / "manifest.yaml", store) # Should have one build step per family build_steps = [s for s in result.steps if s.name.startswith("build:")] assert len(build_steps) == 3 # article, book, website assert "builds" in result.aggregate_output def test_release_regression_returns_result(tmp_project: Path, store: EvidenceStore) -> None: result = run_workflow("release-regression", tmp_project / "manifest.yaml", store) assert result.workflow_name == "release-regression" # --------------------------------------------------------------------------- # run_workflow_from_content (REST/MCP path) # --------------------------------------------------------------------------- def test_run_from_content_single_file(store: EvidenceStore) -> None: result = run_workflow_from_content( "single-file-roundtrip", SIMPLE_MANIFEST, [{"name": "doc.md", "content": SIMPLE_MARKDOWN}], store, ) assert result.workflow_name == "single-file-roundtrip" assert result.run_id def test_run_from_content_unknown_workflow() -> None: with pytest.raises(WorkflowError): run_workflow_from_content("bad-name", SIMPLE_MANIFEST, []) # --------------------------------------------------------------------------- # CLI: markidocx workflow (FR-1308 — CLI interface) # --------------------------------------------------------------------------- def test_cli_workflow_command(tmp_project: Path) -> None: from typer.testing import CliRunner from markidocx.cli import app runner = CliRunner() result = runner.invoke(app, ["workflow", "single-file-roundtrip", str(tmp_project / "manifest.yaml"), "--json"]) assert result.exit_code in (0, 1) import json data = json.loads(result.output.strip()) assert "run_id" in data assert "classification" in data def test_cli_workflow_unknown_name(tmp_project: Path) -> None: from typer.testing import CliRunner from markidocx.cli import app runner = CliRunner() result = runner.invoke(app, ["workflow", "no-such", str(tmp_project / "manifest.yaml")]) assert result.exit_code == 1