generated from coulomb/repo-seed
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>
This commit is contained in:
271
tests/test_level3_plumbing.py
Normal file
271
tests/test_level3_plumbing.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""Tests for LEVEL3 plumbing — feature-level gating & disclosure (FR-537–539)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
from markidocx.level3 import (
|
||||
Level3Support,
|
||||
ProcessorDependency,
|
||||
capabilities_entry,
|
||||
check_level3_support,
|
||||
)
|
||||
from markidocx.manifest import FeatureLevel, load_manifest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Level3 support detection (FR-537, FR-538)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckLevel3Support:
|
||||
def test_returns_level3_support(self) -> None:
|
||||
support = check_level3_support()
|
||||
assert isinstance(support, Level3Support)
|
||||
|
||||
def test_always_available(self) -> None:
|
||||
support = check_level3_support()
|
||||
assert support.available is True
|
||||
|
||||
def test_dependencies_are_processor_dependency_instances(self) -> None:
|
||||
support = check_level3_support()
|
||||
for dep in support.dependencies:
|
||||
assert isinstance(dep, ProcessorDependency)
|
||||
assert dep.name in ("mmdc", "dot", "plantuml")
|
||||
assert isinstance(dep.available, bool)
|
||||
assert dep.description
|
||||
|
||||
def test_partial_when_no_diagram_tools(self, monkeypatch) -> None:
|
||||
"""When no diagram tool is found, partial=True and missing_coverage is populated."""
|
||||
import shutil
|
||||
|
||||
monkeypatch.setattr(shutil, "which", lambda _cmd: None)
|
||||
support = check_level3_support()
|
||||
assert support.partial is True
|
||||
assert len(support.missing_coverage) > 0
|
||||
assert any("diagram" in m for m in support.missing_coverage)
|
||||
|
||||
def test_not_partial_when_diagram_tool_present(self, monkeypatch) -> None:
|
||||
"""When at least one diagram tool is found, partial=False."""
|
||||
import shutil
|
||||
|
||||
def fake_which(cmd: str) -> str | None:
|
||||
return "/usr/bin/mmdc" if cmd == "mmdc" else None
|
||||
|
||||
monkeypatch.setattr(shutil, "which", fake_which)
|
||||
support = check_level3_support()
|
||||
assert support.partial is False
|
||||
assert support.missing_coverage == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# capabilities_entry (FR-537)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCapabilitiesEntry:
|
||||
def test_returns_dict_with_level(self) -> None:
|
||||
entry = capabilities_entry()
|
||||
assert entry["level"] == "level3"
|
||||
|
||||
def test_available_is_true(self) -> None:
|
||||
entry = capabilities_entry()
|
||||
assert entry["available"] is True
|
||||
|
||||
def test_has_dependencies_list(self) -> None:
|
||||
entry = capabilities_entry()
|
||||
assert isinstance(entry["dependencies"], list)
|
||||
for dep in entry["dependencies"]:
|
||||
assert "name" in dep
|
||||
assert "available" in dep
|
||||
assert "description" in dep
|
||||
|
||||
def test_has_partial_and_missing_coverage(self) -> None:
|
||||
entry = capabilities_entry()
|
||||
assert "partial" in entry
|
||||
assert "missing_coverage" in entry
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manifest accepts feature_level: level3 (FR-537)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestManifestLevel3:
|
||||
def test_level3_accepted(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "doc.md").write_text("# Hello", encoding="utf-8")
|
||||
(tmp_path / "manifest.yaml").write_text(
|
||||
textwrap.dedent("""\
|
||||
project:
|
||||
name: test
|
||||
feature_level: level3
|
||||
family: article
|
||||
sources:
|
||||
- path: doc.md
|
||||
output:
|
||||
dir: ./dist
|
||||
"""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
m = load_manifest(tmp_path / "manifest.yaml")
|
||||
assert m.project.feature_level == FeatureLevel.LEVEL3
|
||||
|
||||
def test_level3_routes_to_level3_processing(self, tmp_path: Path) -> None:
|
||||
"""Building with feature_level: level3 succeeds (processing path reached)."""
|
||||
from markidocx.builder import build_document
|
||||
|
||||
(tmp_path / "doc.md").write_text("# Hello\n\nContent.", encoding="utf-8")
|
||||
(tmp_path / "manifest.yaml").write_text(
|
||||
textwrap.dedent("""\
|
||||
project:
|
||||
name: test-l3
|
||||
feature_level: level3
|
||||
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 result.feature_level == "level3"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# partial_level3 flag and processor-dependency disclosure (FR-538, FR-539)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPartialLevel3Flag:
|
||||
def test_partial_level3_set_when_no_diagram_tools(
|
||||
self, tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
import shutil
|
||||
|
||||
from markidocx.builder import build_document
|
||||
|
||||
monkeypatch.setattr(shutil, "which", lambda _cmd: None)
|
||||
(tmp_path / "doc.md").write_text("# Hello\n\nContent.", encoding="utf-8")
|
||||
(tmp_path / "manifest.yaml").write_text(
|
||||
textwrap.dedent("""\
|
||||
project:
|
||||
name: test-partial
|
||||
feature_level: level3
|
||||
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 result.partial_level3 is True
|
||||
assert len(result.missing_coverage) > 0
|
||||
|
||||
def test_partial_level3_false_for_level1(self, tmp_path: Path) -> None:
|
||||
from markidocx.builder import build_document
|
||||
|
||||
(tmp_path / "doc.md").write_text("# Hello\n\nContent.", encoding="utf-8")
|
||||
(tmp_path / "manifest.yaml").write_text(
|
||||
textwrap.dedent("""\
|
||||
project:
|
||||
name: test-l1
|
||||
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.partial_level3 is False
|
||||
assert result.missing_coverage == []
|
||||
|
||||
def test_dependency_warning_emitted_for_unavailable_tool(
|
||||
self, tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
import shutil
|
||||
|
||||
from markidocx.builder import build_document
|
||||
from markidocx.errors import Severity
|
||||
|
||||
monkeypatch.setattr(shutil, "which", lambda _cmd: None)
|
||||
(tmp_path / "doc.md").write_text("# Hello", encoding="utf-8")
|
||||
(tmp_path / "manifest.yaml").write_text(
|
||||
textwrap.dedent("""\
|
||||
project:
|
||||
name: t
|
||||
feature_level: level3
|
||||
family: article
|
||||
sources:
|
||||
- path: doc.md
|
||||
output:
|
||||
dir: ./dist
|
||||
"""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
m = load_manifest(tmp_path / "manifest.yaml")
|
||||
result = build_document(m)
|
||||
dep_warnings = [
|
||||
w for w in result.warning_records
|
||||
if w.reason == "processor-dependency-unavailable"
|
||||
]
|
||||
assert dep_warnings, "Expected processor-dependency-unavailable warning"
|
||||
assert all(w.severity == Severity.WARNING for w in dep_warnings)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REST capabilities includes level3 (FR-537)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRestCapabilitiesLevel3:
|
||||
def test_capabilities_includes_level3(self) -> None:
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from markidocx.rest import create_app
|
||||
|
||||
client = TestClient(create_app())
|
||||
resp = client.get("/capabilities")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
outputs = body["outputs"]
|
||||
assert "level3" in outputs
|
||||
assert outputs["level3"]["level"] == "level3"
|
||||
assert outputs["level3"]["available"] is True
|
||||
assert "dependencies" in outputs["level3"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCP validate_project includes level3 in context (FR-537)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMcpLevel3:
|
||||
def test_validate_project_includes_level3(self) -> None:
|
||||
from markidocx.mcp_server import validate_project
|
||||
|
||||
manifest_yaml = textwrap.dedent("""\
|
||||
project:
|
||||
name: test
|
||||
feature_level: level3
|
||||
family: article
|
||||
sources:
|
||||
- path: doc.md
|
||||
output:
|
||||
dir: ./dist
|
||||
""")
|
||||
result = validate_project(manifest_yaml)
|
||||
assert result["status"] == "ok"
|
||||
assert result["feature_level"] == "level3"
|
||||
assert "level3" in result["context"]
|
||||
assert result["context"]["level3"]["available"] is True
|
||||
Reference in New Issue
Block a user