feat(prompts): implement Phase 3 - Resolver Engine (FR-3)
Implement deterministic multi-space resolution with configurable search order. Core Features: - ResolutionContext and ResolutionResult for tracking resolution state - MultiSpaceResolutionStrategy implementing FR-3.1 search order: 1. Local InformationSpace 2. Explicitly included InformationSpaces 3. Default InformationSpace 4. Team/Shared InformationSpace - PromptResolver with macro resolution logic - ContextCompiler for assembling resolved prompts - ResolutionConfig for configurable resolution behavior Resolution Behavior: - Required macros fail if not found (FR-3.2) - Optional macros resolve to empty (FR-3.3) - Generate macros detected for deferred execution (FR-3.4) - Deterministic search order with duplicate removal - Partial compilation support for debugging Tests (31 passing): - 14 strategy tests (search order, duplicates, priority) - 9 resolver tests (required, optional, generate, multi-space) - 8 compiler tests (substitution, dependencies, digests) Implements: - FR-3.1: Deterministic resolution order - FR-3.2: Required macro validation - FR-3.3: Optional macro fallback - FR-3.4: Generate macro detection - FR-3.5: Max generation depth configuration Files Created: - markitect/prompts/resolver/models.py - markitect/prompts/resolver/strategy.py - markitect/prompts/resolver/resolver.py - markitect/prompts/resolver/compiler.py - migrations/prompts/002_create_resolution_config.sql - tests/unit/prompts/test_resolution_strategy.py - tests/unit/prompts/test_prompt_resolver.py - tests/unit/prompts/test_context_compiler.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
259
tests/unit/prompts/test_context_compiler.py
Normal file
259
tests/unit/prompts/test_context_compiler.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""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
|
||||
248
tests/unit/prompts/test_prompt_resolver.py
Normal file
248
tests/unit/prompts/test_prompt_resolver.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""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
|
||||
182
tests/unit/prompts/test_resolution_strategy.py
Normal file
182
tests/unit/prompts/test_resolution_strategy.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Unit tests for resolution strategies."""
|
||||
|
||||
import pytest
|
||||
from markitect.prompts.resolver.strategy import (
|
||||
ResolutionConfig,
|
||||
MultiSpaceResolutionStrategy,
|
||||
SingleSpaceResolutionStrategy,
|
||||
)
|
||||
|
||||
|
||||
class TestResolutionConfig:
|
||||
"""Tests for ResolutionConfig."""
|
||||
|
||||
def test_create_minimal_config(self):
|
||||
"""Test creating config with only required fields."""
|
||||
config = ResolutionConfig(space_id="space-1")
|
||||
assert config.space_id == "space-1"
|
||||
assert config.included_spaces == []
|
||||
assert config.default_space_id is None
|
||||
assert config.shared_space_id is None
|
||||
assert config.max_generation_depth == 3
|
||||
|
||||
def test_create_full_config(self):
|
||||
"""Test creating config with all fields."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
included_spaces=["space-2", "space-3"],
|
||||
default_space_id="default-space",
|
||||
shared_space_id="shared-space",
|
||||
max_generation_depth=5,
|
||||
)
|
||||
assert config.space_id == "space-1"
|
||||
assert config.included_spaces == ["space-2", "space-3"]
|
||||
assert config.default_space_id == "default-space"
|
||||
assert config.shared_space_id == "shared-space"
|
||||
assert config.max_generation_depth == 5
|
||||
|
||||
def test_config_to_dict(self):
|
||||
"""Test serialization."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
included_spaces=["space-2"],
|
||||
)
|
||||
data = config.to_dict()
|
||||
assert data["space_id"] == "space-1"
|
||||
assert data["included_spaces"] == ["space-2"]
|
||||
assert "max_generation_depth" in data
|
||||
|
||||
def test_config_from_dict(self):
|
||||
"""Test deserialization."""
|
||||
data = {
|
||||
"space_id": "space-1",
|
||||
"included_spaces": ["space-2", "space-3"],
|
||||
"default_space_id": "default",
|
||||
"shared_space_id": "shared",
|
||||
"max_generation_depth": 4,
|
||||
}
|
||||
config = ResolutionConfig.from_dict(data)
|
||||
assert config.space_id == "space-1"
|
||||
assert config.included_spaces == ["space-2", "space-3"]
|
||||
assert config.default_space_id == "default"
|
||||
assert config.shared_space_id == "shared"
|
||||
assert config.max_generation_depth == 4
|
||||
|
||||
|
||||
class TestMultiSpaceResolutionStrategy:
|
||||
"""Tests for MultiSpaceResolutionStrategy."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup strategy for each test."""
|
||||
self.strategy = MultiSpaceResolutionStrategy()
|
||||
|
||||
def test_search_order_local_only(self):
|
||||
"""Test search order with only local space."""
|
||||
config = ResolutionConfig(space_id="space-1")
|
||||
order = self.strategy.get_search_order(config)
|
||||
assert order == ["space-1"]
|
||||
|
||||
def test_search_order_with_included(self):
|
||||
"""Test search order with included spaces."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
included_spaces=["space-2", "space-3"],
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
assert order == ["space-1", "space-2", "space-3"]
|
||||
|
||||
def test_search_order_with_default(self):
|
||||
"""Test search order with default space."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
default_space_id="default-space",
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
assert order == ["space-1", "default-space"]
|
||||
|
||||
def test_search_order_with_shared(self):
|
||||
"""Test search order with shared space."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
shared_space_id="shared-space",
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
assert order == ["space-1", "shared-space"]
|
||||
|
||||
def test_search_order_full_config(self):
|
||||
"""Test full resolution order (FR-3.1)."""
|
||||
config = ResolutionConfig(
|
||||
space_id="local",
|
||||
included_spaces=["included-1", "included-2"],
|
||||
default_space_id="default",
|
||||
shared_space_id="shared",
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
# FR-3.1: Local, Included, Default, Shared
|
||||
assert order == ["local", "included-1", "included-2", "default", "shared"]
|
||||
|
||||
def test_search_order_removes_duplicates(self):
|
||||
"""Test that duplicates are removed."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
included_spaces=["space-2", "space-1", "space-3"], # space-1 duplicate
|
||||
default_space_id="space-2", # space-2 duplicate
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
# Should have each space only once, preserving first occurrence
|
||||
assert order == ["space-1", "space-2", "space-3"]
|
||||
assert len(order) == 3
|
||||
|
||||
def test_search_order_preserves_included_order(self):
|
||||
"""Test that included spaces order is preserved."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
included_spaces=["space-a", "space-b", "space-c"],
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
# Included spaces should appear in specified order
|
||||
assert order.index("space-a") < order.index("space-b")
|
||||
assert order.index("space-b") < order.index("space-c")
|
||||
|
||||
def test_search_order_priority(self):
|
||||
"""Test search order priority."""
|
||||
config = ResolutionConfig(
|
||||
space_id="local",
|
||||
included_spaces=["included"],
|
||||
default_space_id="default",
|
||||
shared_space_id="shared",
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
# Local has highest priority (index 0)
|
||||
assert order[0] == "local"
|
||||
# Shared has lowest priority (last index)
|
||||
assert order[-1] == "shared"
|
||||
|
||||
|
||||
class TestSingleSpaceResolutionStrategy:
|
||||
"""Tests for SingleSpaceResolutionStrategy."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup strategy for each test."""
|
||||
self.strategy = SingleSpaceResolutionStrategy()
|
||||
|
||||
def test_search_order_only_local(self):
|
||||
"""Test that only local space is returned."""
|
||||
config = ResolutionConfig(
|
||||
space_id="space-1",
|
||||
included_spaces=["space-2", "space-3"],
|
||||
default_space_id="default",
|
||||
shared_space_id="shared",
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
assert order == ["space-1"]
|
||||
|
||||
def test_search_order_ignores_other_spaces(self):
|
||||
"""Test that other configured spaces are ignored."""
|
||||
config = ResolutionConfig(
|
||||
space_id="my-space",
|
||||
included_spaces=["ignored-1", "ignored-2"],
|
||||
)
|
||||
order = self.strategy.get_search_order(config)
|
||||
assert len(order) == 1
|
||||
assert order[0] == "my-space"
|
||||
Reference in New Issue
Block a user