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:
0
tests/regression/__init__.py
Normal file
0
tests/regression/__init__.py
Normal file
151
tests/regression/test_roundtrip.py
Normal file
151
tests/regression/test_roundtrip.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""End-to-end round-trip regression tests (FR-1100).
|
||||
|
||||
Tests the full build → import → compare cycle using the SIMPLE_MARKDOWN
|
||||
fixture and per-family smoke tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from markidocx.builder import build_document
|
||||
from markidocx.differ import compare
|
||||
from markidocx.importer import import_document
|
||||
from markidocx.manifest import load_manifest
|
||||
|
||||
LEVEL1_MARKDOWN = textwrap.dedent("""\
|
||||
# Document Title
|
||||
|
||||
This is the introduction paragraph.
|
||||
|
||||
## Section One
|
||||
|
||||
- Item alpha
|
||||
- Item beta
|
||||
- Item gamma
|
||||
|
||||
## Section Two
|
||||
|
||||
| Column A | Column B |
|
||||
|----------|----------|
|
||||
| row1a | row1b |
|
||||
| row2a | row2b |
|
||||
|
||||
1. First ordered item
|
||||
2. Second ordered item
|
||||
""")
|
||||
|
||||
|
||||
def _make_project(tmp_path: Path, family: str, content: str) -> Path:
|
||||
"""Create a minimal project directory and return the manifest path."""
|
||||
(tmp_path / "doc.md").write_text(content, encoding="utf-8")
|
||||
manifest_path = tmp_path / "manifest.yaml"
|
||||
manifest_path.write_text(
|
||||
yaml.dump(
|
||||
{
|
||||
"project": {"name": f"test-{family}", "feature_level": "level1", "family": family},
|
||||
"sources": [{"path": "doc.md"}],
|
||||
"output": {"dir": "./dist"},
|
||||
}
|
||||
)
|
||||
)
|
||||
(tmp_path / "dist").mkdir()
|
||||
return manifest_path
|
||||
|
||||
|
||||
class TestRoundtripArticle:
|
||||
def test_build_import_cycle(self, tmp_path: Path) -> None:
|
||||
manifest_path = _make_project(tmp_path, "article", LEVEL1_MARKDOWN)
|
||||
manifest = load_manifest(manifest_path)
|
||||
|
||||
build_result = build_document(manifest)
|
||||
assert build_result.success, f"Build failed: {build_result.errors}"
|
||||
|
||||
import_result = import_document(manifest, build_result.output_path)
|
||||
assert import_result.success, f"Import failed: {import_result.warnings}"
|
||||
assert import_result.mapping_status == "redistributed"
|
||||
|
||||
def test_heading_structure_preserved(self, tmp_path: Path) -> None:
|
||||
manifest_path = _make_project(tmp_path, "article", LEVEL1_MARKDOWN)
|
||||
manifest = load_manifest(manifest_path)
|
||||
build_result = build_document(manifest)
|
||||
import_result = import_document(manifest, build_result.output_path)
|
||||
assert import_result.success
|
||||
|
||||
md = import_result.output_files[0].read_text(encoding="utf-8")
|
||||
report = compare(LEVEL1_MARKDOWN, md)
|
||||
broken_headings = [b for b in report.broken if b.startswith("heading:")]
|
||||
assert not broken_headings, f"Headings lost in round-trip: {broken_headings}"
|
||||
|
||||
def test_no_errors_on_clean_roundtrip(self, tmp_path: Path) -> None:
|
||||
manifest_path = _make_project(tmp_path, "article", LEVEL1_MARKDOWN)
|
||||
manifest = load_manifest(manifest_path)
|
||||
build_result = build_document(manifest)
|
||||
assert not build_result.errors
|
||||
|
||||
|
||||
class TestRoundtripBook:
|
||||
def test_build_import_cycle(self, tmp_path: Path) -> None:
|
||||
manifest_path = _make_project(tmp_path, "book", LEVEL1_MARKDOWN)
|
||||
manifest = load_manifest(manifest_path)
|
||||
build_result = build_document(manifest)
|
||||
assert build_result.success
|
||||
|
||||
import_result = import_document(manifest, build_result.output_path)
|
||||
assert import_result.success
|
||||
|
||||
|
||||
class TestRoundtripWebsite:
|
||||
def test_build_import_cycle(self, tmp_path: Path) -> None:
|
||||
manifest_path = _make_project(tmp_path, "website", LEVEL1_MARKDOWN)
|
||||
manifest = load_manifest(manifest_path)
|
||||
build_result = build_document(manifest)
|
||||
assert build_result.success
|
||||
|
||||
import_result = import_document(manifest, build_result.output_path)
|
||||
assert import_result.success
|
||||
|
||||
|
||||
class TestMultiFileRoundtrip:
|
||||
def test_two_source_files(self, tmp_path: Path) -> None:
|
||||
ch1 = textwrap.dedent("""\
|
||||
# Chapter One
|
||||
|
||||
Introduction text.
|
||||
|
||||
- Point one
|
||||
- Point two
|
||||
""")
|
||||
ch2 = textwrap.dedent("""\
|
||||
# Chapter Two
|
||||
|
||||
Conclusion text.
|
||||
|
||||
## Subsection
|
||||
|
||||
Final paragraph.
|
||||
""")
|
||||
(tmp_path / "ch1.md").write_text(ch1, encoding="utf-8")
|
||||
(tmp_path / "ch2.md").write_text(ch2, encoding="utf-8")
|
||||
manifest_path = tmp_path / "manifest.yaml"
|
||||
manifest_path.write_text(
|
||||
yaml.dump(
|
||||
{
|
||||
"project": {"name": "two-chap", "feature_level": "level1", "family": "book"},
|
||||
"sources": [{"path": "ch1.md"}, {"path": "ch2.md"}],
|
||||
"output": {"dir": "./dist"},
|
||||
}
|
||||
)
|
||||
)
|
||||
(tmp_path / "dist").mkdir()
|
||||
manifest = load_manifest(manifest_path)
|
||||
build_result = build_document(manifest)
|
||||
assert build_result.success
|
||||
|
||||
import_result = import_document(manifest, build_result.output_path)
|
||||
assert import_result.success
|
||||
# Should have 2 output files (redistributed) or 1 merged
|
||||
assert len(import_result.output_files) >= 1
|
||||
Reference in New Issue
Block a user