Files
marki-docx/tests/test_error_framework.py
Bernd Worsch ac442ea41f feat: WP-0003 complete — LEVEL3 advanced features + error framework
Implements full LEVEL3 feature set: cross-references (xref.py), numbered
figures (figures.py), auto-diagrams (diagrams.py), bibliography/citations
(bibliography.py), LEVEL3 capability detection (level3.py), and structured
error/warning records (errors.py). Builder, importer, and differ updated for
LEVEL3 round-trip support. REST and MCP interfaces updated with structured
warning records. 259 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 10:51:38 +00:00

381 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for structured error & warning framework (FR-12011210)."""
from __future__ import annotations
import textwrap
from pathlib import Path
# ---------------------------------------------------------------------------
# WarningRecord / FailureRecord / OutputState types (FR-12081210)
# ---------------------------------------------------------------------------
class TestWarningRecord:
def test_to_dict(self) -> None:
from markidocx.errors import Severity, WarningRecord
w = WarningRecord(severity=Severity.WARNING, reason="unsupported-construct", construct="html_block")
d = w.to_dict()
assert d["severity"] == "warning"
assert d["reason"] == "unsupported-construct"
assert d["construct"] == "html_block"
def test_str_with_construct(self) -> None:
from markidocx.errors import WarningRecord
w = WarningRecord(severity="warning", reason="test-reason", construct="my-token")
assert "warning" in str(w)
assert "test-reason" in str(w)
assert "my-token" in str(w)
def test_str_without_construct(self) -> None:
from markidocx.errors import WarningRecord
w = WarningRecord(severity="info", reason="test-reason")
s = str(w)
assert "info" in s
assert "test-reason" in s
class TestFailureRecord:
def test_to_dict(self) -> None:
from markidocx.errors import FailureRecord, Severity
f = FailureRecord(severity=Severity.ERROR, reason="docx-not-found", construct="some.docx")
d = f.to_dict()
assert d["severity"] == "error"
assert d["reason"] == "docx-not-found"
class TestOutputState:
def test_all_states_defined(self) -> None:
from markidocx.errors import OutputState
assert OutputState.FINAL == "final"
assert OutputState.PARTIAL == "partial"
assert OutputState.FALLBACK == "fallback"
assert OutputState.DEGRADED == "degraded"
assert OutputState.UNRESOLVED == "unresolved"
# ---------------------------------------------------------------------------
# Builder emits WarningRecord for unsupported constructs (FR-1203, FR-1205)
# ---------------------------------------------------------------------------
class TestBuilderWarningRecords:
def test_unsupported_html_emits_warning_record(self, tmp_path: Path) -> None:
from markidocx.builder import build_document
from markidocx.errors import Severity
from markidocx.manifest import load_manifest
(tmp_path / "doc.md").write_text(
"# Hello\n\n<div>raw html</div>\n\nNormal paragraph.",
encoding="utf-8",
)
(tmp_path / "manifest.yaml").write_text(
textwrap.dedent("""\
project:
name: test
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m = load_manifest(tmp_path / "manifest.yaml")
result = build_document(m)
assert result.success
assert len(result.warning_records) > 0
html_warnings = [w for w in result.warning_records if "html" in w.construct]
assert html_warnings, "Expected warning for html construct"
assert all(w.severity == Severity.WARNING for w in html_warnings)
def test_warning_records_have_reason(self, tmp_path: Path) -> None:
from markidocx.builder import build_document
from markidocx.manifest import load_manifest
(tmp_path / "doc.md").write_text(
"# Hello\n\n<div>raw html</div>",
encoding="utf-8",
)
(tmp_path / "manifest.yaml").write_text(
textwrap.dedent("""\
project:
name: test
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m = load_manifest(tmp_path / "manifest.yaml")
result = build_document(m)
for w in result.warning_records:
assert w.reason, "WarningRecord must have a non-empty reason"
def test_warnings_property_returns_strings(self, tmp_path: Path) -> None:
from markidocx.builder import build_document
from markidocx.manifest import load_manifest
(tmp_path / "doc.md").write_text("# Hello\n\n<div>html</div>", encoding="utf-8")
(tmp_path / "manifest.yaml").write_text(
textwrap.dedent("""\
project:
name: test
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m = load_manifest(tmp_path / "manifest.yaml")
result = build_document(m)
assert all(isinstance(w, str) for w in result.warnings)
def test_output_state_on_clean_build(self, tmp_path: Path) -> None:
from markidocx.builder import build_document
from markidocx.errors import OutputState
from markidocx.manifest import load_manifest
(tmp_path / "doc.md").write_text("# Hello\n\nContent.", encoding="utf-8")
(tmp_path / "manifest.yaml").write_text(
textwrap.dedent("""\
project:
name: clean
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m = load_manifest(tmp_path / "manifest.yaml")
result = build_document(m)
assert result.output_state == OutputState.FINAL
# ---------------------------------------------------------------------------
# Importer emits WarningRecord for errors and fallback paths (FR-1206, FR-1207)
# ---------------------------------------------------------------------------
class TestImporterWarningRecords:
def test_not_found_emits_error_warning_record(self, tmp_path: Path) -> None:
from markidocx.errors import OutputState, Severity
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
(tmp_path / "doc.md").write_text("# Hello", encoding="utf-8")
(tmp_path / "manifest.yaml").write_text(
textwrap.dedent("""\
project:
name: test
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m = load_manifest(tmp_path / "manifest.yaml")
result = import_document(m, tmp_path / "missing.docx")
assert not result.success
assert result.output_state == OutputState.UNRESOLVED
assert len(result.warning_records) > 0
assert result.warning_records[0].severity == Severity.ERROR
assert result.warning_records[0].reason == "docx-not-found"
def test_warnings_property_returns_strings(self, tmp_path: Path) -> None:
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
(tmp_path / "doc.md").write_text("# Hello", encoding="utf-8")
(tmp_path / "manifest.yaml").write_text(
textwrap.dedent("""\
project:
name: test
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m = load_manifest(tmp_path / "manifest.yaml")
result = import_document(m, tmp_path / "missing.docx")
assert all(isinstance(w, str) for w in result.warnings)
def test_fallback_emits_fallback_warning(self, tmp_path: Path) -> None:
"""Multi-source import that can't redistribute produces fallback WarningRecord."""
from markidocx.builder import build_document
from markidocx.errors import OutputState
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
# Create two source files — the DOCX will have a single H1 so redistribution fails
(tmp_path / "a.md").write_text("# Alpha\n\nContent.", encoding="utf-8")
(tmp_path / "b.md").write_text("# Beta\n\nContent.", encoding="utf-8")
(tmp_path / "manifest.yaml").write_text(
textwrap.dedent("""\
project:
name: multi
feature_level: level1
family: article
sources:
- path: a.md
- path: b.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m = load_manifest(tmp_path / "manifest.yaml")
# Build first to get a DOCX
build_result = build_document(m)
assert build_result.success
# Now import with a manifest that has 3 sources (mismatch)
(tmp_path / "c.md").write_text("# Gamma\n\nContent.", encoding="utf-8")
(tmp_path / "manifest3.yaml").write_text(
textwrap.dedent("""\
project:
name: multi
feature_level: level1
family: article
sources:
- path: a.md
- path: b.md
- path: c.md
output:
dir: ./dist
"""),
encoding="utf-8",
)
m3 = load_manifest(tmp_path / "manifest3.yaml")
result = import_document(m3, build_result.output_path)
assert result.success
assert result.mapping_status == "merged"
assert result.output_state == OutputState.FALLBACK
fallback_warnings = [w for w in result.warning_records if w.reason == "fallback"]
assert fallback_warnings, "Expected fallback WarningRecord"
# ---------------------------------------------------------------------------
# Differ output_state (FR-1204)
# ---------------------------------------------------------------------------
class TestDifferOutputState:
def test_final_state_on_clean_diff(self) -> None:
from markidocx.differ import compare
from markidocx.errors import OutputState
text = "# Hello\n\nSome paragraph.\n\n- item one\n- item two"
report = compare(text, text)
assert not report.has_drift
assert report.output_state == OutputState.FINAL
def test_degraded_state_on_degraded_diff(self) -> None:
from markidocx.differ import compare
from markidocx.errors import OutputState
original = "# Hello\n\n- item one\n- item two\n- item three"
reimported = "# Hello\n\n- item one"
report = compare(original, reimported)
assert report.has_drift
assert report.output_state in (OutputState.DEGRADED, OutputState.PARTIAL)
def test_partial_state_on_broken_diff(self) -> None:
from markidocx.differ import compare
from markidocx.errors import OutputState
original = "# Section A\n\n## Sub\n\nParagraph."
reimported = ""
report = compare(original, reimported)
assert report.has_drift
assert report.output_state == OutputState.PARTIAL
# ---------------------------------------------------------------------------
# REST response envelope warnings are WarningRecord dicts (FR-1208)
# ---------------------------------------------------------------------------
class TestRestWarningRecords:
def test_build_warnings_are_dicts(self, tmp_path: Path) -> None:
"""When build produces warnings, REST response warnings are dicts, not bare strings."""
from fastapi.testclient import TestClient
from markidocx.rest import create_app
manifest_yaml = textwrap.dedent("""\
project:
name: test
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
""")
# HTML in source will produce warnings
sources = [{"name": "doc.md", "content": "# Hello\n\n<div>html</div>"}]
client = TestClient(create_app())
resp = client.post("/build", json={"manifest_yaml": manifest_yaml, "sources": sources})
assert resp.status_code == 200
body = resp.json()
warnings = body.get("warnings", [])
# Each warning should be a dict with severity/reason/construct keys
for w in warnings:
assert isinstance(w, dict), f"Expected dict warning, got {type(w)}: {w}"
assert "severity" in w
assert "reason" in w
def test_import_warnings_are_dicts_on_failure(self) -> None:
"""Import failure warns with WarningRecord dict, not bare string."""
import base64
from fastapi.testclient import TestClient
from markidocx.rest import create_app
manifest_yaml = textwrap.dedent("""\
project:
name: test
feature_level: level1
family: article
sources:
- path: doc.md
output:
dir: ./dist
""")
# Send an invalid (empty) DOCX
empty_docx = base64.b64encode(b"not-a-docx").decode()
client = TestClient(create_app())
resp = client.post(
"/import",
json={"manifest_yaml": manifest_yaml, "docx_base64": empty_docx},
)
body = resp.json()
warnings = body.get("warnings", [])
for w in warnings:
assert isinstance(w, dict), f"Expected dict warning, got {type(w)}: {w}"