249 lines
8.8 KiB
Python
249 lines
8.8 KiB
Python
|
|
"""Unit tests for PromptResolver."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from markitect.prompts.templates.models import PromptTemplate, ContentMacro, MacroKind
|
||
|
|
from markitect.prompts.templates.analyzer import TemplateAnalyzer
|
||
|
|
from markitect.prompts.resolver.resolver import PromptResolver
|
||
|
|
from markitect.prompts.resolver.strategy import MultiSpaceResolutionStrategy, ResolutionConfig
|
||
|
|
from markitect.prompts.resolver.models import ResolutionStatus
|
||
|
|
from markitect.prompts.services.artifact_service import ArtifactService
|
||
|
|
from markitect.prompts.repositories.sqlite import SQLiteArtifactRepository
|
||
|
|
from markitect.prompts.models import Artifact, ArtifactType
|
||
|
|
|
||
|
|
|
||
|
|
@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 with temp database."""
|
||
|
|
repository = SQLiteArtifactRepository(temp_db)
|
||
|
|
return ArtifactService(repository)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def resolver(artifact_service):
|
||
|
|
"""Create resolver with multi-space strategy."""
|
||
|
|
strategy = MultiSpaceResolutionStrategy()
|
||
|
|
return PromptResolver(artifact_service, strategy)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def analyzer():
|
||
|
|
"""Create template analyzer."""
|
||
|
|
return TemplateAnalyzer()
|
||
|
|
|
||
|
|
|
||
|
|
class TestPromptResolver:
|
||
|
|
"""Tests for PromptResolver."""
|
||
|
|
|
||
|
|
def test_resolve_template_not_analyzed_raises_error(self, resolver):
|
||
|
|
"""Test resolving unanalyzed template raises error."""
|
||
|
|
template = PromptTemplate.create(
|
||
|
|
space_id="space-1",
|
||
|
|
name="test",
|
||
|
|
content="{{require:dep}}",
|
||
|
|
)
|
||
|
|
config = ResolutionConfig(space_id="space-1")
|
||
|
|
|
||
|
|
with pytest.raises(ValueError, match="must be analyzed"):
|
||
|
|
resolver.resolve_template(template, config)
|
||
|
|
|
||
|
|
def test_resolve_template_no_macros(self, resolver, analyzer):
|
||
|
|
"""Test resolving template with no 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)
|
||
|
|
|
||
|
|
assert result.success is True
|
||
|
|
assert result.status == ResolutionStatus.SUCCESS
|
||
|
|
assert len(result.context.resolved_macros) == 0
|
||
|
|
assert len(result.context.unresolved_required) == 0
|
||
|
|
|
||
|
|
def test_resolve_required_macro_found(self, resolver, analyzer, artifact_service):
|
||
|
|
"""Test resolving required macro when artifact exists."""
|
||
|
|
# Create dependency artifact
|
||
|
|
artifact_service.create_artifact(
|
||
|
|
space_id="space-1",
|
||
|
|
name="glossary",
|
||
|
|
content="Glossary content here",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Create template with required macro
|
||
|
|
content = "# Template\n{{require:glossary}}"
|
||
|
|
template = PromptTemplate.create(
|
||
|
|
space_id="space-1",
|
||
|
|
name="test-template",
|
||
|
|
content=content,
|
||
|
|
)
|
||
|
|
analyzer.analyze(template, content)
|
||
|
|
|
||
|
|
config = ResolutionConfig(space_id="space-1")
|
||
|
|
result = resolver.resolve_template(template, config)
|
||
|
|
|
||
|
|
assert result.success is True
|
||
|
|
assert result.status == ResolutionStatus.SUCCESS
|
||
|
|
assert len(result.context.resolved_macros) == 1
|
||
|
|
assert result.context.resolved_macros[0].resolved is True
|
||
|
|
assert result.context.resolved_macros[0].artifact.name == "glossary"
|
||
|
|
|
||
|
|
def test_resolve_required_macro_not_found_fails(self, resolver, analyzer):
|
||
|
|
"""Test resolving required macro when artifact missing (FR-3.2)."""
|
||
|
|
content = "# Template\n{{require:missing-artifact}}"
|
||
|
|
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)
|
||
|
|
|
||
|
|
assert result.success is False
|
||
|
|
assert result.status == ResolutionStatus.FAILED
|
||
|
|
assert len(result.context.unresolved_required) == 1
|
||
|
|
assert result.context.unresolved_required[0].target == "missing-artifact"
|
||
|
|
assert len(result.context.errors) > 0
|
||
|
|
|
||
|
|
def test_resolve_optional_macro_not_found_succeeds(self, resolver, analyzer):
|
||
|
|
"""Test resolving optional macro when missing succeeds (FR-3.3)."""
|
||
|
|
content = "# Template\n{{optional:missing-optional}}"
|
||
|
|
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)
|
||
|
|
|
||
|
|
assert result.success is True # Still succeeds
|
||
|
|
assert result.status == ResolutionStatus.PARTIAL # But partial
|
||
|
|
assert len(result.context.unresolved_optional) == 1
|
||
|
|
assert result.context.unresolved_optional[0].target == "missing-optional"
|
||
|
|
|
||
|
|
def test_resolve_generate_macro_deferred(self, resolver, analyzer):
|
||
|
|
"""Test generate macro is detected and deferred (FR-3.4)."""
|
||
|
|
content = "# Template\n{{generate:examples}}"
|
||
|
|
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)
|
||
|
|
|
||
|
|
assert result.success is True
|
||
|
|
assert result.needs_generation is True
|
||
|
|
assert len(result.context.generator_macros) == 1
|
||
|
|
assert result.context.generator_macros[0].target == "examples"
|
||
|
|
|
||
|
|
def test_resolve_multi_space_search_order(self, resolver, analyzer, artifact_service):
|
||
|
|
"""Test multi-space resolution follows search order (FR-3.1)."""
|
||
|
|
# Create same-named artifact in multiple spaces
|
||
|
|
artifact_service.create_artifact(
|
||
|
|
space_id="space-1",
|
||
|
|
name="common",
|
||
|
|
content="From space-1",
|
||
|
|
)
|
||
|
|
artifact_service.create_artifact(
|
||
|
|
space_id="space-2",
|
||
|
|
name="common",
|
||
|
|
content="From space-2",
|
||
|
|
)
|
||
|
|
|
||
|
|
content = "{{require:common}}"
|
||
|
|
template = PromptTemplate.create(
|
||
|
|
space_id="space-1",
|
||
|
|
name="test",
|
||
|
|
content=content,
|
||
|
|
)
|
||
|
|
analyzer.analyze(template, content)
|
||
|
|
|
||
|
|
# space-1 has higher priority in search order
|
||
|
|
config = ResolutionConfig(
|
||
|
|
space_id="space-1",
|
||
|
|
included_spaces=["space-2"],
|
||
|
|
)
|
||
|
|
result = resolver.resolve_template(template, config)
|
||
|
|
|
||
|
|
# Should resolve from space-1 (higher priority)
|
||
|
|
assert result.context.resolved_macros[0].space_id == "space-1"
|
||
|
|
|
||
|
|
def test_resolve_falls_back_to_included_space(self, resolver, analyzer, artifact_service):
|
||
|
|
"""Test resolution falls back to included spaces."""
|
||
|
|
# Create artifact only in included space
|
||
|
|
artifact_service.create_artifact(
|
||
|
|
space_id="space-2",
|
||
|
|
name="shared-artifact",
|
||
|
|
content="Shared content",
|
||
|
|
)
|
||
|
|
|
||
|
|
content = "{{require:shared-artifact}}"
|
||
|
|
template = PromptTemplate.create(
|
||
|
|
space_id="space-1",
|
||
|
|
name="test",
|
||
|
|
content=content,
|
||
|
|
)
|
||
|
|
analyzer.analyze(template, content)
|
||
|
|
|
||
|
|
config = ResolutionConfig(
|
||
|
|
space_id="space-1",
|
||
|
|
included_spaces=["space-2"],
|
||
|
|
)
|
||
|
|
result = resolver.resolve_template(template, config)
|
||
|
|
|
||
|
|
assert result.success is True
|
||
|
|
assert result.context.resolved_macros[0].space_id == "space-2"
|
||
|
|
|
||
|
|
def test_resolution_summary(self, resolver, analyzer, artifact_service):
|
||
|
|
"""Test getting resolution summary."""
|
||
|
|
artifact_service.create_artifact(
|
||
|
|
space_id="space-1",
|
||
|
|
name="found",
|
||
|
|
content="Content",
|
||
|
|
)
|
||
|
|
|
||
|
|
content = """
|
||
|
|
{{require:found}}
|
||
|
|
{{require:missing}}
|
||
|
|
{{optional:optional-missing}}
|
||
|
|
{{generate:gen}}
|
||
|
|
"""
|
||
|
|
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)
|
||
|
|
|
||
|
|
summary = resolver.get_resolution_summary(result)
|
||
|
|
assert summary["resolved_count"] == 1
|
||
|
|
assert summary["unresolved_required"] == ["missing"]
|
||
|
|
assert summary["unresolved_optional"] == ["optional-missing"]
|
||
|
|
assert summary["needs_generation"] is True
|
||
|
|
assert summary["generator_count"] == 1
|