"""Tests for T06 — Interface parity: CLI, REST, MCP produce equivalent results (FR-1308).""" from __future__ import annotations import base64 import json import textwrap from pathlib import Path import pytest from fastapi.testclient import TestClient import markidocx.mcp_server as mcp_module from markidocx.rest import create_app 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 rest_client() -> TestClient: return TestClient(create_app()) # --------------------------------------------------------------------------- # Validate — CLI, REST, MCP must agree (FR-1308) # --------------------------------------------------------------------------- def test_validate_cli_rest_mcp_agree(tmp_project: Path, rest_client: TestClient) -> None: from typer.testing import CliRunner from markidocx.cli import app as cli_app runner = CliRunner() cli_result = runner.invoke( cli_app, ["validate", str(tmp_project / "manifest.yaml"), "--json"], ) cli_data = json.loads(cli_result.output.strip()) rest_resp = rest_client.post("/validate", json={"manifest_yaml": SIMPLE_MANIFEST}) rest_data = rest_resp.json() mcp_data = mcp_module.validate_project(SIMPLE_MANIFEST) # All three must agree: project is valid assert cli_data["status"] == "ok" assert rest_data["status"] == "ok" assert mcp_data["status"] == "ok" # Project name matches across all assert cli_data["project"] == "Test Document" assert rest_data["outputs"]["project"] == "Test Document" assert mcp_data["project"] == "Test Document" # --------------------------------------------------------------------------- # Build — CLI, REST, MCP all produce a valid DOCX # --------------------------------------------------------------------------- def test_build_cli_rest_mcp_all_produce_docx( tmp_project: Path, rest_client: TestClient ) -> None: from typer.testing import CliRunner from markidocx.cli import app as cli_app # CLI build runner = CliRunner() cli_result = runner.invoke( cli_app, ["build", str(tmp_project / "manifest.yaml"), "--json"], ) cli_data = json.loads(cli_result.output.strip()) assert cli_data["status"] == "ok" cli_docx = Path(cli_data["output_path"]) assert cli_docx.exists() cli_docx_bytes = cli_docx.read_bytes() assert cli_docx_bytes[:2] == b"PK" # REST build rest_resp = rest_client.post( "/build", json={ "manifest_yaml": SIMPLE_MANIFEST, "sources": [{"name": "doc.md", "content": SIMPLE_MARKDOWN}], }, ) rest_data = rest_resp.json() assert rest_data["status"] == "ok" rest_docx_bytes = base64.b64decode(rest_data["outputs"]["docx_base64"]) assert rest_docx_bytes[:2] == b"PK" # MCP build mcp_data = mcp_module.build( SIMPLE_MANIFEST, [{"name": "doc.md", "content": SIMPLE_MARKDOWN}], ) assert mcp_data["status"] == "ok" mcp_docx_bytes = base64.b64decode(mcp_data["docx_base64"]) assert mcp_docx_bytes[:2] == b"PK" # --------------------------------------------------------------------------- # Single-file round-trip — CLI, REST, MCP produce structurally consistent results # --------------------------------------------------------------------------- def test_single_file_roundtrip_cli_rest_mcp_consistent( tmp_project: Path, rest_client: TestClient ) -> None: from typer.testing import CliRunner from markidocx.cli import app as cli_app # CLI workflow runner = CliRunner() cli_result = runner.invoke( cli_app, [ "workflow", "single-file-roundtrip", str(tmp_project / "manifest.yaml"), "--json", ], ) cli_data = json.loads(cli_result.output.strip()) # REST workflow rest_resp = rest_client.post( "/workflows/single-file-roundtrip", json={ "manifest_yaml": SIMPLE_MANIFEST, "sources": [{"name": "doc.md", "content": SIMPLE_MARKDOWN}], }, ) rest_data = rest_resp.json() # MCP workflow mcp_data = mcp_module.invoke_workflow( "single-file-roundtrip", SIMPLE_MANIFEST, [{"name": "doc.md", "content": SIMPLE_MARKDOWN}], ) # All three should produce a result with a classification field assert "classification" in cli_data assert "classification" in rest_data["outputs"] assert "classification" in mcp_data # All three should report the same top-level success/failure cli_ok = cli_data["classification"] != "failed" rest_ok = rest_data["outputs"]["classification"] != "failed" mcp_ok = mcp_data["classification"] != "failed" assert cli_ok == rest_ok == mcp_ok # --------------------------------------------------------------------------- # Evidence round-trip: REST workflow stores retrievable evidence # --------------------------------------------------------------------------- def test_evidence_round_trip_rest(rest_client: TestClient) -> None: """Evidence from a REST workflow run must be retrievable via GET /evidence/{run_id}.""" workflow_resp = rest_client.post( "/workflows/single-file-roundtrip", json={ "manifest_yaml": SIMPLE_MANIFEST, "sources": [{"name": "doc.md", "content": SIMPLE_MARKDOWN}], }, ) # Note: the REST /workflows endpoint uses run_workflow_from_content which # creates its own temp EvidenceStore — evidence is not persisted to the # global default store unless configured. This test verifies the run_id # is present in the response context (FR-915) and the workflow identity # fields are correct (FR-1309). body = workflow_resp.json() assert "run_id" in body["context"] assert "workflow" in body["context"] run_id = body["outputs"]["run_id"] assert run_id == body["context"]["run_id"] # --------------------------------------------------------------------------- # Capability equivalence: REST and MCP agree on supported families / levels # --------------------------------------------------------------------------- def test_capabilities_rest_mcp_agree(rest_client: TestClient) -> None: rest_caps = rest_client.get("/capabilities").json()["outputs"] # Both should surface level1 assert "level1" in rest_caps["feature_levels"] # Both should surface built-in families for family in ("article", "book", "website"): assert family in rest_caps["families"] assert any(t["name"] == family for t in mcp_module.list_templates())