generated from coulomb/repo-seed
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>
128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""Tests for manifest model (FR-100)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
from markidocx.manifest import (
|
|
FeatureLevel,
|
|
ManifestError,
|
|
load_manifest,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def minimal_manifest_yaml(tmp_path: Path) -> Path:
|
|
src = tmp_path / "doc.md"
|
|
src.write_text("# Hello\n\nContent here.")
|
|
manifest = tmp_path / "manifest.yaml"
|
|
manifest.write_text(
|
|
yaml.dump(
|
|
{
|
|
"project": {"name": "Test Doc", "feature_level": "level1", "family": "article"},
|
|
"sources": [{"path": "doc.md"}],
|
|
"output": {"dir": "./dist"},
|
|
}
|
|
)
|
|
)
|
|
return manifest
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_source_manifest(tmp_path: Path) -> Path:
|
|
for name in ("ch1.md", "ch2.md"):
|
|
(tmp_path / name).write_text(f"# {name}\n\nContent.")
|
|
manifest = tmp_path / "manifest.yaml"
|
|
manifest.write_text(
|
|
yaml.dump(
|
|
{
|
|
"project": {"name": "Book", "feature_level": "level1", "family": "book"},
|
|
"sources": [{"path": "ch1.md"}, {"path": "ch2.md"}],
|
|
"output": {"dir": "./dist"},
|
|
"metadata": {"title": "My Book", "author": "Author", "date": "2026-01-01"},
|
|
}
|
|
)
|
|
)
|
|
return manifest
|
|
|
|
|
|
class TestLoadManifest:
|
|
def test_loads_minimal_manifest(self, minimal_manifest_yaml: Path) -> None:
|
|
m = load_manifest(minimal_manifest_yaml)
|
|
assert m.project.name == "Test Doc"
|
|
assert m.project.feature_level == FeatureLevel.LEVEL1
|
|
assert m.project.family == "article"
|
|
assert len(m.sources) == 1
|
|
|
|
def test_loads_multi_source(self, multi_source_manifest: Path) -> None:
|
|
m = load_manifest(multi_source_manifest)
|
|
assert len(m.sources) == 2
|
|
assert m.metadata["title"] == "My Book"
|
|
|
|
def test_missing_file_raises(self, tmp_path: Path) -> None:
|
|
manifest = tmp_path / "manifest.yaml"
|
|
manifest.write_text(
|
|
yaml.dump(
|
|
{
|
|
"project": {"name": "X", "feature_level": "level1", "family": "article"},
|
|
"sources": [{"path": "missing.md"}],
|
|
"output": {"dir": "./dist"},
|
|
}
|
|
)
|
|
)
|
|
with pytest.raises(ManifestError, match="missing.md"):
|
|
load_manifest(manifest)
|
|
|
|
def test_missing_project_key_raises(self, tmp_path: Path) -> None:
|
|
manifest = tmp_path / "manifest.yaml"
|
|
manifest.write_text(yaml.dump({"sources": []}))
|
|
with pytest.raises(ManifestError, match="project"):
|
|
load_manifest(manifest)
|
|
|
|
def test_invalid_feature_level_raises(self, tmp_path: Path) -> None:
|
|
src = tmp_path / "doc.md"
|
|
src.write_text("# x")
|
|
manifest = tmp_path / "manifest.yaml"
|
|
manifest.write_text(
|
|
yaml.dump(
|
|
{
|
|
"project": {"name": "X", "feature_level": "level9", "family": "article"},
|
|
"sources": [{"path": "doc.md"}],
|
|
"output": {"dir": "./dist"},
|
|
}
|
|
)
|
|
)
|
|
with pytest.raises(ManifestError, match="feature_level"):
|
|
load_manifest(manifest)
|
|
|
|
def test_invalid_family_raises(self, tmp_path: Path) -> None:
|
|
src = tmp_path / "doc.md"
|
|
src.write_text("# x")
|
|
manifest = tmp_path / "manifest.yaml"
|
|
manifest.write_text(
|
|
yaml.dump(
|
|
{
|
|
"project": {"name": "X", "feature_level": "level1", "family": "unknown"},
|
|
"sources": [{"path": "doc.md"}],
|
|
"output": {"dir": "./dist"},
|
|
}
|
|
)
|
|
)
|
|
with pytest.raises(ManifestError, match="family"):
|
|
load_manifest(manifest)
|
|
|
|
def test_sources_resolved_relative_to_manifest(self, minimal_manifest_yaml: Path) -> None:
|
|
m = load_manifest(minimal_manifest_yaml)
|
|
assert m.sources[0].path.is_absolute()
|
|
assert m.sources[0].path.exists()
|
|
|
|
def test_output_dir_resolved(self, minimal_manifest_yaml: Path) -> None:
|
|
m = load_manifest(minimal_manifest_yaml)
|
|
assert m.output_dir.is_absolute()
|
|
|
|
def test_manifest_not_found_raises(self, tmp_path: Path) -> None:
|
|
with pytest.raises(ManifestError, match="not found"):
|
|
load_manifest(tmp_path / "nonexistent.yaml")
|