generated from coulomb/repo-seed
feat: WP-0007 — Interface Completeness & Evidence
T01: markidocx inspect (FR-806) and markidocx test (FR-810) CLI commands
T02: markidocx evidence get/list CLI commands (FR-1409, FR-814)
T03: list_styles() / GET /styles / MCP list_styles with real style data (FR-907)
T04: Evidence assembly — EvidenceSet summary via REST and MCP (FR-1406–1408)
T05: LEVEL3 edge-case tests — diagram mutation, renderer version check,
bibliography duplicate keys / missing refs / special chars (FR-534, FR-538, FR-542)
T06: markidocx template extract + Word-first round-trip regression test (FR-606)
New: differ._compare_diagram_blocks tracks fenced diagram source drift (FR-534)
New: diagrams.check_renderer_version emits warning for outdated renderers (FR-538)
New: bibliography.validate_citations detects duplicate keys and missing entries (FR-542)
New: templates.extract_template / TemplateExtractionResult / list_styles / StyleEntry
New: REST POST /template/extract; MCP extract_template tool
278 tests pass, ruff+mypy clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -345,3 +345,156 @@ class TestRendererDetection:
|
||||
r2 = RendererResult(success=False)
|
||||
assert not r2.success
|
||||
assert r2.output_path is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T05 — FR-534 edge cases: diagram source mutation and empty source
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDiagramEdgeCases:
|
||||
"""FR-534: diagram source mutation and empty source edge cases."""
|
||||
|
||||
def test_mutated_source_marker_classified_as_structural_drift(
|
||||
self, tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""Mutated source marker content is not silently dropped by the importer (FR-534).
|
||||
|
||||
We construct a DOCX with only a source-intent marker paragraph (simulating
|
||||
what the renderer path produces) and verify that the importer uses the marker
|
||||
to reconstruct the fenced block. We then check the differ sees the mutation.
|
||||
"""
|
||||
from docx import Document as DocxDoc
|
||||
from docx.shared import Pt
|
||||
|
||||
from markidocx.diagrams import DIAGRAM_SOURCE_MARKER_PREFIX
|
||||
from markidocx.differ import compare
|
||||
from markidocx.importer import import_document
|
||||
from markidocx.manifest import load_manifest
|
||||
|
||||
# Build a minimal DOCX with ONLY a source marker paragraph (no verbatim code block)
|
||||
# — this simulates the rendered path where a PNG was embedded.
|
||||
docx_path = tmp_path / "mutated.docx"
|
||||
doc = DocxDoc()
|
||||
# Add a heading so there is some structure
|
||||
doc.add_heading("Test", level=1)
|
||||
# Add source marker with MUTATED source (A-->C, not original A-->B)
|
||||
marker_para = doc.add_paragraph(style="Normal")
|
||||
marker_run = marker_para.add_run(
|
||||
f"{DIAGRAM_SOURCE_MARKER_PREFIX}mermaid\ngraph TD\nA-->C"
|
||||
)
|
||||
marker_run.font.size = Pt(1)
|
||||
doc.save(str(docx_path))
|
||||
|
||||
# Set up a manifest
|
||||
_make_project(tmp_path, "")
|
||||
m = load_manifest(tmp_path / "manifest.yaml")
|
||||
|
||||
import_result = import_document(m, docx_path)
|
||||
assert import_result.success
|
||||
|
||||
reimported = import_result.output_files[0].read_text(encoding="utf-8")
|
||||
|
||||
# The mutated source (A-->C) must appear in the reimported content
|
||||
assert "A-->C" in reimported, "Mutated diagram source was silently dropped"
|
||||
|
||||
# The differ between original (A-->B) and reimported (A-->C) should detect drift
|
||||
original_md = "```mermaid\ngraph TD\nA-->B\n```"
|
||||
report = compare(original_md, reimported)
|
||||
assert report.has_drift, (
|
||||
f"Differ should detect structural drift after source mutation. "
|
||||
f"preserved={report.preserved}, degraded={report.degraded}, broken={report.broken}"
|
||||
)
|
||||
|
||||
def test_empty_diagram_source_emits_warning(
|
||||
self, tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""A diagram block with empty source must produce a WarningRecord."""
|
||||
import shutil
|
||||
|
||||
from markidocx.builder import build_document
|
||||
from markidocx.manifest import load_manifest
|
||||
|
||||
monkeypatch.setattr(shutil, "which", lambda _cmd: None)
|
||||
# Empty mermaid block
|
||||
md = "```mermaid\n\n```"
|
||||
_make_project(tmp_path, md)
|
||||
m = load_manifest(tmp_path / "manifest.yaml")
|
||||
|
||||
result = build_document(m)
|
||||
assert result.success
|
||||
# Any warning related to diagrams or empty source
|
||||
all_reasons = [w.reason for w in result.warning_records]
|
||||
# At minimum the processor-dependency warning fires since no renderer available
|
||||
assert any("processor-dependency" in r or "diagram" in r or "empty" in r for r in all_reasons)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T05 — FR-538: renderer version checking
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRendererVersionCheck:
|
||||
"""FR-538: outdated renderer versions must emit WarningRecord."""
|
||||
|
||||
def test_supported_version_no_warning(self, monkeypatch) -> None:
|
||||
import subprocess
|
||||
|
||||
import markidocx.diagrams as diag_mod
|
||||
|
||||
# Mock mmdc --version to return a supported version
|
||||
def _fake_run(cmd, **kwargs):
|
||||
class R:
|
||||
stdout = "mermaid.js/10.4.0"
|
||||
stderr = ""
|
||||
returncode = 0
|
||||
|
||||
return R()
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", _fake_run)
|
||||
warnings: list = []
|
||||
diag_mod.check_renderer_version("mmdc", warnings)
|
||||
version_warnings = [w for w in warnings if w.reason == "renderer-version-unsupported"]
|
||||
assert not version_warnings, "Should not warn for supported version"
|
||||
|
||||
def test_outdated_version_emits_warning(self, monkeypatch) -> None:
|
||||
import subprocess
|
||||
|
||||
import markidocx.diagrams as diag_mod
|
||||
|
||||
# Mock mmdc --version to return an unsupported old version (8.x < 9.x minimum)
|
||||
def _fake_run(cmd, **kwargs):
|
||||
class R:
|
||||
stdout = "mermaid.js/8.2.3"
|
||||
stderr = ""
|
||||
returncode = 0
|
||||
|
||||
return R()
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", _fake_run)
|
||||
warnings: list = []
|
||||
diag_mod.check_renderer_version("mmdc", warnings)
|
||||
version_warnings = [w for w in warnings if w.reason == "renderer-version-unsupported"]
|
||||
assert version_warnings, "Expected renderer-version-unsupported warning for old version"
|
||||
|
||||
def test_unknown_renderer_no_crash(self, monkeypatch) -> None:
|
||||
import markidocx.diagrams as diag_mod
|
||||
|
||||
warnings: list = []
|
||||
# Unknown cmd — should silently return without crashing
|
||||
diag_mod.check_renderer_version("unknown-renderer", warnings)
|
||||
assert warnings == []
|
||||
|
||||
def test_subprocess_error_no_crash(self, monkeypatch) -> None:
|
||||
import subprocess
|
||||
|
||||
import markidocx.diagrams as diag_mod
|
||||
|
||||
def _fake_run(*args, **kwargs):
|
||||
raise OSError("command not found")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", _fake_run)
|
||||
warnings: list = []
|
||||
diag_mod.check_renderer_version("mmdc", warnings)
|
||||
# Should not raise
|
||||
assert warnings == []
|
||||
|
||||
Reference in New Issue
Block a user