272 lines
9.3 KiB
Python
272 lines
9.3 KiB
Python
|
|
"""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
|