Files
markitect-main/tests/unit/prompts/test_context_compiler.py
tegwick 706981c39f fix(prompts): fix three infrastructure bugs in prompt dependency resolution
- ContentMacro: add __post_init__ to auto-derive raw_text when built
  programmatically, preventing str.replace("", X) corruption
- MacroParser: add @{target} shorthand syntax support mapped to REQUIRED kind,
  updating parse, has_macros, count_macros, and find_macro_positions
- Artifact: store content in model and SQLite DB, replace resolver placeholder
  with actual artifact content, add migration for existing databases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:53:02 +01:00

289 lines
9.5 KiB
Python

"""Unit tests for ContextCompiler."""
import pytest
import tempfile
from pathlib import Path
from markitect.prompts.templates.models import PromptTemplate
from markitect.prompts.templates.analyzer import TemplateAnalyzer
from markitect.prompts.resolver.resolver import PromptResolver
from markitect.prompts.resolver.compiler import ContextCompiler
from markitect.prompts.resolver.strategy import MultiSpaceResolutionStrategy, ResolutionConfig
from markitect.prompts.services.artifact_service import ArtifactService
from markitect.prompts.repositories.sqlite import SQLiteArtifactRepository
@pytest.fixture
def temp_db():
"""Create temporary database."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
yield db_path
Path(db_path).unlink(missing_ok=True)
@pytest.fixture
def artifact_service(temp_db):
"""Create artifact service."""
repository = SQLiteArtifactRepository(temp_db)
return ArtifactService(repository)
@pytest.fixture
def resolver(artifact_service):
"""Create resolver."""
strategy = MultiSpaceResolutionStrategy()
return PromptResolver(artifact_service, strategy)
@pytest.fixture
def compiler():
"""Create compiler."""
return ContextCompiler()
@pytest.fixture
def analyzer():
"""Create analyzer."""
return TemplateAnalyzer()
class TestContextCompiler:
"""Tests for ContextCompiler."""
def test_compile_template_no_macros(self, compiler, analyzer, resolver):
"""Test compiling template without macros."""
content = "# Simple Template\nNo macros here."
template = PromptTemplate.create(
space_id="space-1",
name="simple",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile(template, content, result)
assert compiled.content == content
assert compiled.template_id == template.id
assert compiled.template_name == template.name
assert len(compiled.dependency_digests) == 0
def test_compile_with_resolved_macros(
self, compiler, analyzer, resolver, artifact_service
):
"""Test compiling with resolved macros substitutes content."""
# Create dependency
artifact_service.create_artifact(
space_id="space-1",
name="intro",
content="Introduction text",
)
content = "# Document\n{{require:intro}}\nMore content"
template = PromptTemplate.create(
space_id="space-1",
name="doc",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile(template, content, result)
# Macro should be replaced with resolved content
assert "{{require:intro}}" not in compiled.content
assert "Introduction text" in compiled.content
assert "intro" in compiled.dependency_digests
def test_compile_with_optional_macros_substitutes_empty(
self, compiler, analyzer, resolver, artifact_service
):
"""Test optional macros are replaced with empty string."""
# Create one artifact, leave another missing
artifact_service.create_artifact(
space_id="space-1",
name="present",
content="Present content",
)
content = "Start {{require:present}} middle {{optional:missing}} end"
template = PromptTemplate.create(
space_id="space-1",
name="test",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile(template, content, result)
# Optional missing macro should be removed
assert "{{optional:missing}}" not in compiled.content
assert compiled.content == "Start Present content middle end"
def test_compile_failed_resolution_raises_error(
self, compiler, analyzer, resolver
):
"""Test compiling failed resolution raises error."""
content = "{{require:missing}}"
template = PromptTemplate.create(
space_id="space-1",
name="test",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
with pytest.raises(ValueError, match="Resolution failed"):
compiler.compile(template, content, result)
def test_compile_partial_with_placeholder(
self, compiler, analyzer, resolver
):
"""Test partial compilation with placeholder for unresolved."""
content = "{{require:missing}} text {{optional:also-missing}}"
template = PromptTemplate.create(
space_id="space-1",
name="test",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile_partial(
template, content, result, placeholder="[MISSING]"
)
assert "[MISSING]:missing" in compiled.content
assert "{{optional:also-missing}}" not in compiled.content
assert compiled.metadata.get("partial") == "true"
def test_compiled_prompt_has_content_digest(
self, compiler, analyzer, resolver, artifact_service
):
"""Test compiled prompt has content digest."""
artifact_service.create_artifact(
space_id="space-1",
name="dep",
content="Dependency",
)
content = "{{require:dep}}"
template = PromptTemplate.create(
space_id="space-1",
name="test",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile(template, content, result)
assert compiled.content_digest
assert len(compiled.content_digest) == 64 # SHA-256 hex
def test_compiled_prompt_tracks_dependencies(
self, compiler, analyzer, resolver, artifact_service
):
"""Test compiled prompt tracks dependency digests."""
art1 = artifact_service.create_artifact(
space_id="space-1",
name="dep1",
content="Dep 1",
)
art2 = artifact_service.create_artifact(
space_id="space-1",
name="dep2",
content="Dep 2",
)
content = "{{require:dep1}} and {{require:dep2}}"
template = PromptTemplate.create(
space_id="space-1",
name="test",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile(template, content, result)
assert len(compiled.dependency_digests) == 2
assert "dep1" in compiled.dependency_digests
assert "dep2" in compiled.dependency_digests
assert compiled.dependency_digests["dep1"] == art1.content_digest
assert compiled.dependency_digests["dep2"] == art2.content_digest
def test_get_compilation_info(
self, compiler, analyzer, resolver, artifact_service
):
"""Test getting compilation info."""
artifact_service.create_artifact(
space_id="space-1",
name="dep",
content="Dependency",
)
content = "{{require:dep}}"
template = PromptTemplate.create(
space_id="space-1",
name="test",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile(template, content, result)
info = compiler.get_compilation_info(compiled)
assert info["template_id"] == template.id
assert info["dependency_count"] == 1
assert "dep" in info["dependencies"]
assert info["is_partial"] is False
def test_compile_with_programmatic_macros_no_corruption(
self, compiler, analyzer, resolver, artifact_service
):
"""Test that compilation with programmatic macros doesn't corrupt content."""
artifact_service.create_artifact(
space_id="space-1",
name="dep",
content="Dependency content",
)
# Use @{target} shorthand syntax (parsed into programmatic macros)
content = "Before @{dep} after"
template = PromptTemplate.create(
space_id="space-1",
name="test",
content=content,
)
analyzer.analyze(template, content)
config = ResolutionConfig(space_id="space-1")
result = resolver.resolve_template(template, config)
compiled = compiler.compile(template, content, result)
# Content should be cleanly substituted, not corrupted
assert "@{dep}" not in compiled.content
assert "Dependency content" in compiled.content
assert compiled.content == "Before Dependency content after"