"""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