Files
markitect-main/tests/unit/prompts/test_resolution_strategy.py
tegwick 5f463e5b20 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>
2026-02-08 22:45:46 +01:00

183 lines
6.6 KiB
Python

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