Files
markitect-tool/tests/test_reference_resolution.py

196 lines
7.1 KiB
Python

from pathlib import Path
import pytest
from click.testing import CliRunner
from markitect_tool.cli import main
from markitect_tool.core import parse_markdown
from markitect_tool.reference import (
ReferenceContext,
ReferenceResolutionError,
load_namespaces,
parse_reference,
resolve_reference,
)
def test_parse_reference_splits_namespace_fragment_and_selector():
address = parse_reference("std:clauses/payment.md#section:fees::blocks[type=code]")
assert address.namespace == "std"
assert address.address == "clauses/payment.md"
assert address.fragment == "section:fees"
assert address.selector == "blocks[type=code]"
def test_load_namespaces_accepts_optional_colon_suffix():
namespaces = load_namespaces({"namespaces": {"std:": "./standard", "src": "../src"}})
assert namespaces == {"std": "./standard", "src": "../src"}
def test_resolve_path_reference_returns_document_unit(tmp_path: Path):
context_file = tmp_path / "context.md"
target_file = tmp_path / "target.md"
context_file.write_text("# Context\n", encoding="utf-8")
target_file.write_text("---\nid: target-doc\ntitle: Target\n---\n\n# Target\n\nBody.", encoding="utf-8")
context = ReferenceContext(root=tmp_path, current_path=context_file)
resolution = resolve_reference("target.md", context=context)
assert resolution.target_path == str(target_file.resolve())
assert len(resolution.units) == 1
assert resolution.units[0].kind == "document"
assert resolution.units[0].unit_id == "target-doc"
assert "# Target" in resolution.units[0].text
def test_resolve_namespace_reference_and_explicit_section_id(tmp_path: Path):
standard = tmp_path / "standard"
standard.mkdir()
context_file = tmp_path / "context.md"
clause_file = standard / "clauses.md"
context_file.write_text(
"---\nnamespaces:\n std: ./standard\n---\n\n# Context\n",
encoding="utf-8",
)
clause_file.write_text(
"# Clauses\n\n## Payment Terms {#payment-terms}\n\nPay within 30 days.\n",
encoding="utf-8",
)
document = parse_markdown(context_file.read_text(encoding="utf-8"), source_path=str(context_file))
context = ReferenceContext.from_document(document, root=tmp_path)
resolution = resolve_reference("std:clauses.md#section:payment-terms", context=context)
assert resolution.units[0].kind == "section"
assert resolution.units[0].unit_id == "payment-terms"
assert resolution.units[0].name == "Payment Terms"
assert "Pay within 30 days" in resolution.units[0].text
def test_resolve_selector_reference_uses_existing_query_engine(tmp_path: Path):
standard = tmp_path / "standard"
standard.mkdir()
context_file = tmp_path / "context.md"
source_file = standard / "clauses.md"
context_file.write_text(
"---\nnamespaces:\n std: ./standard\n---\n\n# Context\n",
encoding="utf-8",
)
source_file.write_text(
"# Clauses\n\n## Warranty\n\nWarranty text.\n\n## Liability\n\nLiability text.\n",
encoding="utf-8",
)
context = ReferenceContext.from_document(parse_markdown(context_file.read_text(encoding="utf-8"), str(context_file)), root=tmp_path)
resolution = resolve_reference("std:clauses.md::sections[heading=Warranty]", context=context)
assert [unit.kind for unit in resolution.units] == ["section"]
assert resolution.units[0].name == "Warranty"
assert "Liability" not in resolution.units[0].text
def test_resolve_pathless_fragment_uses_current_document(tmp_path: Path):
context_file = tmp_path / "context.md"
context_file.write_text("# Context\n\n## Overview\n\nUseful local context.\n", encoding="utf-8")
context = ReferenceContext(root=tmp_path, current_path=context_file)
resolution = resolve_reference("#overview", context=context)
assert resolution.target_path == str(context_file.resolve())
assert resolution.units[0].kind == "section"
assert resolution.units[0].unit_id == "overview"
assert "Useful local context" in resolution.units[0].text
def test_resolve_named_region_by_id_and_tag(tmp_path: Path):
context_file = tmp_path / "context.md"
context_file.write_text(
"""# Context
<!-- mkt:region id="overview" tags="reuse summary" -->
Reusable region text.
<!-- /mkt:region -->
""",
encoding="utf-8",
)
context = ReferenceContext(root=tmp_path, current_path=context_file)
by_id = resolve_reference("#region:overview", context=context)
by_tag = resolve_reference("#tag:summary", context=context)
assert by_id.units[0].kind == "region"
assert by_id.units[0].text == "Reusable region text."
assert by_tag.units[0].unit_id == "overview"
def test_resolve_fenced_block_by_id(tmp_path: Path):
context_file = tmp_path / "context.md"
context_file.write_text(
"""# Context
```python {#load-config tags="code setup" tangle="src/config.py"}
def load_config():
return {}
```
""",
encoding="utf-8",
)
context = ReferenceContext(root=tmp_path, current_path=context_file)
resolution = resolve_reference("#fence:load-config", context=context)
assert resolution.units[0].kind == "fenced_block"
assert resolution.units[0].unit_id == "load-config"
assert resolution.units[0].metadata["language"] == "python"
assert resolution.units[0].metadata["attrs"]["tangle"] == "src/config.py"
assert "def load_config" in resolution.units[0].text
def test_resolve_line_range_fragment(tmp_path: Path):
context_file = tmp_path / "context.md"
context_file.write_text("# Context\n\nLine A\nLine B\nLine C\n", encoding="utf-8")
context = ReferenceContext(root=tmp_path, current_path=context_file)
resolution = resolve_reference("#line:3-4", context=context)
assert resolution.units[0].kind == "line_range"
assert resolution.units[0].span.line_start == 3
assert resolution.units[0].text == "Line A\nLine B"
def test_resolve_rejects_unknown_namespace(tmp_path: Path):
context_file = tmp_path / "context.md"
context_file.write_text("# Context\n", encoding="utf-8")
context = ReferenceContext(root=tmp_path, current_path=context_file)
with pytest.raises(ReferenceResolutionError, match="Unknown namespace"):
resolve_reference("missing:doc.md", context=context)
def test_resolve_rejects_paths_outside_root(tmp_path: Path):
context_file = tmp_path / "context.md"
context_file.write_text("# Context\n", encoding="utf-8")
context = ReferenceContext(root=tmp_path, current_path=context_file)
with pytest.raises(ReferenceResolutionError, match="escapes root"):
resolve_reference("../outside.md", context=context)
def test_mkt_ref_resolve_outputs_text(tmp_path: Path):
context_file = tmp_path / "context.md"
target_file = tmp_path / "target.md"
context_file.write_text("# Context\n", encoding="utf-8")
target_file.write_text("# Target\n\n## Decision\n\nChosen.", encoding="utf-8")
result = CliRunner().invoke(
main,
["ref", "resolve", str(context_file), "target.md#decision", "--root", str(tmp_path)],
)
assert result.exit_code == 0
assert "1 unit(s)" in result.output
assert "section decision" in result.output
assert "Decision" in result.output