260 lines
8.5 KiB
Python
260 lines
8.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 "[Content of intro from space-1]" 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 [Content of present from space-1] 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
|