generated from coulomb/repo-seed
feat: WP-0001 + WP-0002 complete — LEVEL1 core + service interfaces
WP-0001 (Foundation & LEVEL1 Core):
- manifest model (FR-100), MD→DOCX builder (FR-200), DOCX→MD importer
(FR-300/400), template family registry (FR-600), drift detector (FR-700),
CLI wiring, pre-commit config, CI skeleton, regression harness
WP-0002 (Service Interfaces & Workflow Orchestration):
- REST service via FastAPI (FR-900): /health, /version, /capabilities,
/templates, /styles, /validate, /build, /import, /compare,
/templates/register, /workflows/{name}, /evidence/{run_id}
- Evidence & report store (FR-1400): JSON-backed, per-run, retrievable
through all interfaces, classification (pass/warnings/failed)
- Composite workflow orchestration (FR-1300): single-file-roundtrip,
multi-file-roundtrip, release-regression, family-switch-build
- MCP server via FastMCP (FR-1000): all tools + resources
- CLI additions: `markidocx serve`, `markidocx workflow`, `markidocx mcp`
- Interface parity tests: CLI / REST / MCP produce equivalent results
135 tests passing, ruff + mypy clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
248
tests/test_mcp.py
Normal file
248
tests/test_mcp.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Tests for T05 — MCP server (FR-1000).
|
||||
|
||||
MCP tool functions are tested by calling them directly (the MCP registration
|
||||
is decorative — the logic lives in the function bodies).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import textwrap
|
||||
|
||||
import markidocx.mcp_server as mcp_module
|
||||
from markidocx import __version__
|
||||
|
||||
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
|
||||
""")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_version (FR-1010)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_version() -> None:
|
||||
result = mcp_module.get_version()
|
||||
assert result["version"] == __version__
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# list_templates (FR-1002)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_list_templates_returns_built_ins() -> None:
|
||||
families = mcp_module.list_templates()
|
||||
names = {f["name"] for f in families}
|
||||
assert "article" in names
|
||||
assert "book" in names
|
||||
assert "website" in names
|
||||
|
||||
|
||||
def test_list_templates_have_name_and_description() -> None:
|
||||
for f in mcp_module.list_templates():
|
||||
assert "name" in f
|
||||
assert "description" in f
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# list_styles (FR-1003)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_list_styles_returns_list() -> None:
|
||||
assert isinstance(mcp_module.list_styles(), list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate_project (FR-1004)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_validate_project_ok() -> None:
|
||||
result = mcp_module.validate_project(SIMPLE_MANIFEST)
|
||||
assert result["status"] == "ok"
|
||||
assert result["project"] == "Test Document"
|
||||
assert result["family"] == "article"
|
||||
assert result["feature_level"] == "level1"
|
||||
|
||||
|
||||
def test_validate_project_error() -> None:
|
||||
bad = "project:\n name: x\n"
|
||||
result = mcp_module.validate_project(bad)
|
||||
assert result["status"] == "error"
|
||||
assert result["errors"]
|
||||
|
||||
|
||||
def test_validate_project_context_has_capabilities() -> None:
|
||||
result = mcp_module.validate_project(SIMPLE_MANIFEST)
|
||||
assert "supported_families" in result["context"]
|
||||
assert "supported_feature_levels" in result["context"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# inspect_project (FR-1005)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_inspect_project_same_as_validate() -> None:
|
||||
v = mcp_module.validate_project(SIMPLE_MANIFEST)
|
||||
i = mcp_module.inspect_project(SIMPLE_MANIFEST)
|
||||
assert v["status"] == i["status"]
|
||||
assert v["project"] == i["project"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build (FR-1006)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_build_returns_docx_base64() -> None:
|
||||
result = mcp_module.build(
|
||||
SIMPLE_MANIFEST,
|
||||
[{"name": "doc.md", "content": SIMPLE_MARKDOWN}],
|
||||
)
|
||||
assert result["status"] == "ok"
|
||||
docx_bytes = base64.b64decode(result["docx_base64"])
|
||||
assert docx_bytes[:2] == b"PK" # ZIP/DOCX magic
|
||||
|
||||
|
||||
def test_build_returns_family_and_level() -> None:
|
||||
result = mcp_module.build(
|
||||
SIMPLE_MANIFEST,
|
||||
[{"name": "doc.md", "content": SIMPLE_MARKDOWN}],
|
||||
)
|
||||
assert result["family"] == "article"
|
||||
assert result["feature_level"] == "level1"
|
||||
|
||||
|
||||
def test_build_invalid_manifest_error() -> None:
|
||||
result = mcp_module.build("project:\n name: x\n", [])
|
||||
assert result["status"] == "error"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# import_docx (FR-1007)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_docx_b64() -> str:
|
||||
result = mcp_module.build(
|
||||
SIMPLE_MANIFEST,
|
||||
[{"name": "doc.md", "content": SIMPLE_MARKDOWN}],
|
||||
)
|
||||
return result["docx_base64"]
|
||||
|
||||
|
||||
def test_import_docx_returns_files() -> None:
|
||||
docx_b64 = _build_docx_b64()
|
||||
result = mcp_module.import_docx(SIMPLE_MANIFEST, docx_b64)
|
||||
assert result["status"] == "ok"
|
||||
assert isinstance(result["files"], dict)
|
||||
assert result["mapping_status"] in ("redistributed", "merged")
|
||||
|
||||
|
||||
def test_import_bad_docx_error() -> None:
|
||||
bad_b64 = base64.b64encode(b"not a docx").decode()
|
||||
result = mcp_module.import_docx(SIMPLE_MANIFEST, bad_b64)
|
||||
assert result["status"] == "error"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# compare (FR-1008)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_compare_returns_drift_report() -> None:
|
||||
docx_b64 = _build_docx_b64()
|
||||
result = mcp_module.compare(
|
||||
SIMPLE_MANIFEST,
|
||||
docx_b64,
|
||||
[{"name": "doc.md", "content": SIMPLE_MARKDOWN}],
|
||||
)
|
||||
assert result["status"] == "ok"
|
||||
assert "has_drift" in result
|
||||
assert "preserved" in result
|
||||
assert "degraded" in result
|
||||
assert "broken" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# run_tests (FR-1009)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_run_tests_invokes_roundtrip() -> None:
|
||||
result = mcp_module.run_tests(
|
||||
SIMPLE_MANIFEST,
|
||||
[{"name": "doc.md", "content": SIMPLE_MARKDOWN}],
|
||||
)
|
||||
assert result["status"] in ("ok", "error")
|
||||
assert "run_id" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# invoke_workflow (FR-1012)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_invoke_workflow_single_file_roundtrip() -> None:
|
||||
result = mcp_module.invoke_workflow(
|
||||
"single-file-roundtrip",
|
||||
SIMPLE_MANIFEST,
|
||||
[{"name": "doc.md", "content": SIMPLE_MARKDOWN}],
|
||||
)
|
||||
assert result["status"] in ("ok", "error")
|
||||
assert "run_id" in result
|
||||
assert "classification" in result
|
||||
assert "steps" in result
|
||||
|
||||
|
||||
def test_invoke_workflow_unknown_name() -> None:
|
||||
result = mcp_module.invoke_workflow("no-such", SIMPLE_MANIFEST, [])
|
||||
assert result["status"] == "error"
|
||||
assert result["errors"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_evidence (FR-1013)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_evidence_not_found() -> None:
|
||||
result = mcp_module.get_evidence("no-such-run-id")
|
||||
assert result["status"] == "not_found"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCP server object exists and has the right name (FR-1001)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_mcp_object_exists() -> None:
|
||||
assert mcp_module.mcp is not None
|
||||
|
||||
|
||||
def test_mcp_name() -> None:
|
||||
assert mcp_module.mcp.name == "markidocx"
|
||||
Reference in New Issue
Block a user