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