Co-authored-by: Perplexity Computer <perplexity@tower.local> Co-committed-by: Perplexity Computer <perplexity@tower.local>
240 lines
8.0 KiB
Python
240 lines
8.0 KiB
Python
"""Tests for the sovereignty loop orchestrator.
|
|
|
|
Refs: #953
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
class TestSovereignPerceive:
|
|
"""Tests for sovereign_perceive (perception layer)."""
|
|
|
|
async def test_cache_hit_skips_vlm(self):
|
|
"""When cache has high-confidence match, VLM is not called."""
|
|
from timmy.sovereignty.perception_cache import CacheResult
|
|
from timmy.sovereignty.sovereignty_loop import sovereign_perceive
|
|
|
|
cache = MagicMock()
|
|
cache.match.return_value = CacheResult(
|
|
confidence=0.95, state={"template_name": "health_bar"}
|
|
)
|
|
|
|
vlm = AsyncMock()
|
|
screenshot = MagicMock()
|
|
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
|
|
new_callable=AsyncMock,
|
|
) as mock_emit:
|
|
result = await sovereign_perceive(screenshot, cache, vlm)
|
|
|
|
assert result == {"template_name": "health_bar"}
|
|
vlm.analyze.assert_not_called()
|
|
mock_emit.assert_called_once_with("perception_cache_hit", session_id="")
|
|
|
|
async def test_cache_miss_calls_vlm_and_crystallizes(self):
|
|
"""On cache miss, VLM is called and output is crystallized."""
|
|
from timmy.sovereignty.perception_cache import CacheResult
|
|
from timmy.sovereignty.sovereignty_loop import sovereign_perceive
|
|
|
|
cache = MagicMock()
|
|
cache.match.return_value = CacheResult(confidence=0.3, state=None)
|
|
|
|
vlm = AsyncMock()
|
|
vlm.analyze.return_value = {"items": []}
|
|
|
|
screenshot = MagicMock()
|
|
crystallize_fn = MagicMock(return_value=[])
|
|
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
|
|
new_callable=AsyncMock,
|
|
):
|
|
await sovereign_perceive(screenshot, cache, vlm, crystallize_fn=crystallize_fn)
|
|
|
|
vlm.analyze.assert_called_once_with(screenshot)
|
|
crystallize_fn.assert_called_once()
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
class TestSovereignDecide:
|
|
"""Tests for sovereign_decide (decision layer)."""
|
|
|
|
async def test_rule_hit_skips_llm(self, tmp_path):
|
|
"""Reliable rule match bypasses the LLM."""
|
|
from timmy.sovereignty.auto_crystallizer import Rule, RuleStore
|
|
from timmy.sovereignty.sovereignty_loop import sovereign_decide
|
|
|
|
store = RuleStore(path=tmp_path / "strategy.json")
|
|
store.add(
|
|
Rule(
|
|
id="r1",
|
|
condition="health low",
|
|
action="heal",
|
|
confidence=0.9,
|
|
times_applied=5,
|
|
times_succeeded=4,
|
|
)
|
|
)
|
|
|
|
llm = AsyncMock()
|
|
context = {"health": "low", "mana": 50}
|
|
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
|
|
new_callable=AsyncMock,
|
|
):
|
|
result = await sovereign_decide(context, llm, rule_store=store)
|
|
|
|
assert result["action"] == "heal"
|
|
assert result["source"] == "crystallized_rule"
|
|
llm.reason.assert_not_called()
|
|
|
|
async def test_no_rule_calls_llm_and_crystallizes(self, tmp_path):
|
|
"""Without matching rules, LLM is called and reasoning is crystallized."""
|
|
from timmy.sovereignty.auto_crystallizer import RuleStore
|
|
from timmy.sovereignty.sovereignty_loop import sovereign_decide
|
|
|
|
store = RuleStore(path=tmp_path / "strategy.json")
|
|
|
|
llm = AsyncMock()
|
|
llm.reason.return_value = {
|
|
"action": "attack",
|
|
"reasoning": "I chose attack because enemy_health was below 50%.",
|
|
}
|
|
|
|
context = {"enemy_health": 45}
|
|
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
|
|
new_callable=AsyncMock,
|
|
):
|
|
result = await sovereign_decide(context, llm, rule_store=store)
|
|
|
|
assert result["action"] == "attack"
|
|
llm.reason.assert_called_once_with(context)
|
|
# The reasoning should have been crystallized (threshold pattern detected)
|
|
assert len(store) > 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
class TestSovereignNarrate:
|
|
"""Tests for sovereign_narrate (narration layer)."""
|
|
|
|
async def test_template_hit_skips_llm(self):
|
|
"""Known event type uses template without LLM."""
|
|
from timmy.sovereignty.sovereignty_loop import sovereign_narrate
|
|
|
|
template_store = {
|
|
"combat_start": "Battle begins against {enemy}!",
|
|
}
|
|
|
|
llm = AsyncMock()
|
|
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
|
|
new_callable=AsyncMock,
|
|
) as mock_emit:
|
|
result = await sovereign_narrate(
|
|
{"type": "combat_start", "enemy": "Cliff Racer"},
|
|
llm=llm,
|
|
template_store=template_store,
|
|
)
|
|
|
|
assert result == "Battle begins against Cliff Racer!"
|
|
llm.narrate.assert_not_called()
|
|
mock_emit.assert_called_once_with("narration_template", session_id="")
|
|
|
|
async def test_unknown_event_calls_llm(self):
|
|
"""Unknown event type falls through to LLM and crystallizes template."""
|
|
from timmy.sovereignty.sovereignty_loop import sovereign_narrate
|
|
|
|
template_store = {}
|
|
|
|
llm = AsyncMock()
|
|
llm.narrate.return_value = "You discovered a hidden cave in the mountains."
|
|
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
|
|
new_callable=AsyncMock,
|
|
):
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop._crystallize_narration_template"
|
|
) as mock_cryst:
|
|
result = await sovereign_narrate(
|
|
{"type": "discovery", "location": "mountains"},
|
|
llm=llm,
|
|
template_store=template_store,
|
|
)
|
|
|
|
assert result == "You discovered a hidden cave in the mountains."
|
|
llm.narrate.assert_called_once()
|
|
mock_cryst.assert_called_once()
|
|
|
|
async def test_no_llm_returns_default(self):
|
|
"""Without LLM and no template, returns a default narration."""
|
|
from timmy.sovereignty.sovereignty_loop import sovereign_narrate
|
|
|
|
with patch(
|
|
"timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
|
|
new_callable=AsyncMock,
|
|
):
|
|
result = await sovereign_narrate(
|
|
{"type": "unknown_event"},
|
|
llm=None,
|
|
template_store={},
|
|
)
|
|
|
|
assert "[unknown_event]" in result
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
class TestSovereigntyEnforcedDecorator:
|
|
"""Tests for the @sovereignty_enforced decorator."""
|
|
|
|
async def test_cache_hit_skips_function(self):
|
|
"""Decorator returns cached value without calling the wrapped function."""
|
|
from timmy.sovereignty.sovereignty_loop import sovereignty_enforced
|
|
|
|
call_count = 0
|
|
|
|
@sovereignty_enforced(
|
|
layer="decision",
|
|
cache_check=lambda a, kw: "cached_result",
|
|
)
|
|
async def expensive_fn():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
return "expensive_result"
|
|
|
|
with patch("timmy.sovereignty.sovereignty_loop.get_metrics_store") as mock_store:
|
|
mock_store.return_value = MagicMock()
|
|
result = await expensive_fn()
|
|
|
|
assert result == "cached_result"
|
|
assert call_count == 0
|
|
|
|
async def test_cache_miss_runs_function(self):
|
|
"""Decorator calls function when cache returns None."""
|
|
from timmy.sovereignty.sovereignty_loop import sovereignty_enforced
|
|
|
|
@sovereignty_enforced(
|
|
layer="decision",
|
|
cache_check=lambda a, kw: None,
|
|
)
|
|
async def expensive_fn():
|
|
return "computed_result"
|
|
|
|
with patch("timmy.sovereignty.sovereignty_loop.get_metrics_store") as mock_store:
|
|
mock_store.return_value = MagicMock()
|
|
result = await expensive_fn()
|
|
|
|
assert result == "computed_result"
|