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