"""Tests for the sovereignty perception cache (template matching). Refs: #1261 """ import json from unittest.mock import patch import numpy as np class TestTemplate: """Tests for the Template dataclass.""" def test_template_default_values(self): """Template dataclass has correct defaults.""" from timmy.sovereignty.perception_cache import Template image = np.array([[1, 2], [3, 4]]) template = Template(name="test_template", image=image) assert template.name == "test_template" assert np.array_equal(template.image, image) assert template.threshold == 0.85 def test_template_custom_threshold(self): """Template can have custom threshold.""" from timmy.sovereignty.perception_cache import Template image = np.array([[1, 2], [3, 4]]) template = Template(name="test_template", image=image, threshold=0.95) assert template.threshold == 0.95 class TestCacheResult: """Tests for the CacheResult dataclass.""" def test_cache_result_with_state(self): """CacheResult stores confidence and state.""" from timmy.sovereignty.perception_cache import CacheResult result = CacheResult(confidence=0.92, state={"template_name": "test"}) assert result.confidence == 0.92 assert result.state == {"template_name": "test"} def test_cache_result_no_state(self): """CacheResult can have None state.""" from timmy.sovereignty.perception_cache import CacheResult result = CacheResult(confidence=0.5, state=None) assert result.confidence == 0.5 assert result.state is None class TestPerceptionCacheInit: """Tests for PerceptionCache initialization.""" def test_init_creates_empty_cache_when_no_file(self, tmp_path): """Cache initializes empty when templates file doesn't exist.""" from timmy.sovereignty.perception_cache import PerceptionCache templates_path = tmp_path / "nonexistent_templates.json" cache = PerceptionCache(templates_path=templates_path) assert cache.templates_path == templates_path assert cache.templates == [] def test_init_loads_existing_templates(self, tmp_path): """Cache loads templates from existing JSON file.""" from timmy.sovereignty.perception_cache import PerceptionCache templates_path = tmp_path / "templates.json" templates_data = [ {"name": "template1", "threshold": 0.85}, {"name": "template2", "threshold": 0.90}, ] with open(templates_path, "w") as f: json.dump(templates_data, f) cache = PerceptionCache(templates_path=templates_path) assert len(cache.templates) == 2 assert cache.templates[0].name == "template1" assert cache.templates[0].threshold == 0.85 assert cache.templates[1].name == "template2" assert cache.templates[1].threshold == 0.90 def test_init_with_string_path(self, tmp_path): """Cache accepts string path for templates.""" from timmy.sovereignty.perception_cache import PerceptionCache templates_path = str(tmp_path / "templates.json") cache = PerceptionCache(templates_path=templates_path) assert str(cache.templates_path) == templates_path class TestPerceptionCacheMatch: """Tests for PerceptionCache.match() template matching.""" def test_match_no_templates_returns_low_confidence(self, tmp_path): """Matching with no templates returns low confidence and None state.""" from timmy.sovereignty.perception_cache import PerceptionCache cache = PerceptionCache(templates_path=tmp_path / "templates.json") screenshot = np.array([[1, 2], [3, 4]]) result = cache.match(screenshot) assert result.confidence == 0.0 assert result.state is None @patch("timmy.sovereignty.perception_cache.cv2") def test_match_finds_best_template(self, mock_cv2, tmp_path): """Match returns the best matching template above threshold.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template # Setup mock cv2 behavior mock_cv2.matchTemplate.return_value = np.array([[0.5, 0.6], [0.7, 0.8]]) mock_cv2.TM_CCOEFF_NORMED = "TM_CCOEFF_NORMED" mock_cv2.minMaxLoc.return_value = (None, 0.92, None, None) cache = PerceptionCache(templates_path=tmp_path / "templates.json") template = Template(name="best_match", image=np.array([[1, 2], [3, 4]])) cache.add([template]) screenshot = np.array([[5, 6], [7, 8]]) result = cache.match(screenshot) assert result.confidence == 0.92 assert result.state == {"template_name": "best_match"} @patch("timmy.sovereignty.perception_cache.cv2") def test_match_respects_global_threshold(self, mock_cv2, tmp_path): """Match returns None state when confidence is below threshold.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template # Setup mock cv2 to return confidence below 0.85 threshold mock_cv2.matchTemplate.return_value = np.array([[0.1, 0.2], [0.3, 0.4]]) mock_cv2.TM_CCOEFF_NORMED = "TM_CCOEFF_NORMED" mock_cv2.minMaxLoc.return_value = (None, 0.75, None, None) cache = PerceptionCache(templates_path=tmp_path / "templates.json") template = Template(name="low_match", image=np.array([[1, 2], [3, 4]])) cache.add([template]) screenshot = np.array([[5, 6], [7, 8]]) result = cache.match(screenshot) # Confidence is recorded but state is None (below threshold) assert result.confidence == 0.75 assert result.state is None @patch("timmy.sovereignty.perception_cache.cv2") def test_match_selects_highest_confidence(self, mock_cv2, tmp_path): """Match selects template with highest confidence across all templates.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template mock_cv2.TM_CCOEFF_NORMED = "TM_CCOEFF_NORMED" # Each template will return a different confidence mock_cv2.minMaxLoc.side_effect = [ (None, 0.70, None, None), # template1 (None, 0.95, None, None), # template2 (best) (None, 0.80, None, None), # template3 ] cache = PerceptionCache(templates_path=tmp_path / "templates.json") templates = [ Template(name="template1", image=np.array([[1, 2], [3, 4]])), Template(name="template2", image=np.array([[5, 6], [7, 8]])), Template(name="template3", image=np.array([[9, 10], [11, 12]])), ] cache.add(templates) screenshot = np.array([[13, 14], [15, 16]]) result = cache.match(screenshot) assert result.confidence == 0.95 assert result.state == {"template_name": "template2"} @patch("timmy.sovereignty.perception_cache.cv2") def test_match_exactly_at_threshold(self, mock_cv2, tmp_path): """Match returns state when confidence is exactly at threshold boundary.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template mock_cv2.matchTemplate.return_value = np.array([[0.1]]) mock_cv2.TM_CCOEFF_NORMED = "TM_CCOEFF_NORMED" mock_cv2.minMaxLoc.return_value = (None, 0.85, None, None) # Exactly at threshold cache = PerceptionCache(templates_path=tmp_path / "templates.json") template = Template(name="threshold_match", image=np.array([[1, 2], [3, 4]])) cache.add([template]) screenshot = np.array([[5, 6], [7, 8]]) result = cache.match(screenshot) # Implementation uses >= 0.85 (inclusive threshold) assert result.confidence == 0.85 assert result.state is not None assert result.state["template_name"] == "threshold_match" @patch("timmy.sovereignty.perception_cache.cv2") def test_match_just_above_threshold(self, mock_cv2, tmp_path): """Match returns state when confidence is just above threshold.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template mock_cv2.matchTemplate.return_value = np.array([[0.1]]) mock_cv2.TM_CCOEFF_NORMED = "TM_CCOEFF_NORMED" mock_cv2.minMaxLoc.return_value = (None, 0.851, None, None) # Just above threshold cache = PerceptionCache(templates_path=tmp_path / "templates.json") template = Template(name="above_threshold", image=np.array([[1, 2], [3, 4]])) cache.add([template]) screenshot = np.array([[5, 6], [7, 8]]) result = cache.match(screenshot) assert result.confidence == 0.851 assert result.state == {"template_name": "above_threshold"} class TestPerceptionCacheAdd: """Tests for PerceptionCache.add() method.""" def test_add_single_template(self, tmp_path): """Can add a single template to the cache.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template cache = PerceptionCache(templates_path=tmp_path / "templates.json") template = Template(name="new_template", image=np.array([[1, 2], [3, 4]])) cache.add([template]) assert len(cache.templates) == 1 assert cache.templates[0].name == "new_template" def test_add_multiple_templates(self, tmp_path): """Can add multiple templates at once.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template cache = PerceptionCache(templates_path=tmp_path / "templates.json") templates = [ Template(name="template1", image=np.array([[1, 2], [3, 4]])), Template(name="template2", image=np.array([[5, 6], [7, 8]])), ] cache.add(templates) assert len(cache.templates) == 2 assert cache.templates[0].name == "template1" assert cache.templates[1].name == "template2" def test_add_templates_accumulate(self, tmp_path): """Adding templates multiple times accumulates them.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template cache = PerceptionCache(templates_path=tmp_path / "templates.json") cache.add([Template(name="first", image=np.array([[1]]))]) cache.add([Template(name="second", image=np.array([[2]]))]) assert len(cache.templates) == 2 class TestPerceptionCachePersist: """Tests for PerceptionCache.persist() method.""" def test_persist_creates_file(self, tmp_path): """Persist creates templates JSON file.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template templates_path = tmp_path / "subdir" / "templates.json" cache = PerceptionCache(templates_path=templates_path) cache.add([Template(name="persisted", image=np.array([[1, 2], [3, 4]]))]) cache.persist() assert templates_path.exists() def test_persist_stores_template_names(self, tmp_path): """Persist stores template names and thresholds.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template templates_path = tmp_path / "templates.json" cache = PerceptionCache(templates_path=templates_path) cache.add( [ Template(name="template1", image=np.array([[1]]), threshold=0.85), Template(name="template2", image=np.array([[2]]), threshold=0.90), ] ) cache.persist() with open(templates_path) as f: data = json.load(f) assert len(data) == 2 assert data[0]["name"] == "template1" assert data[0]["threshold"] == 0.85 assert data[1]["name"] == "template2" assert data[1]["threshold"] == 0.90 def test_persist_does_not_store_image_data(self, tmp_path): """Persist only stores metadata, not actual image arrays.""" from timmy.sovereignty.perception_cache import PerceptionCache, Template templates_path = tmp_path / "templates.json" cache = PerceptionCache(templates_path=templates_path) cache.add([Template(name="no_image", image=np.array([[1, 2, 3], [4, 5, 6]]))]) cache.persist() with open(templates_path) as f: data = json.load(f) assert "image" not in data[0] # raw image array is NOT in JSON # image_path is stored for .npy file reference assert "name" in data[0] assert "threshold" in data[0] class TestPerceptionCacheLoad: """Tests for PerceptionCache.load() method.""" def test_load_from_existing_file(self, tmp_path): """Load restores templates from persisted file.""" from timmy.sovereignty.perception_cache import PerceptionCache templates_path = tmp_path / "templates.json" # Create initial cache with templates and persist cache1 = PerceptionCache(templates_path=templates_path) from timmy.sovereignty.perception_cache import Template cache1.add([Template(name="loaded", image=np.array([[1]]), threshold=0.88)]) cache1.persist() # Create new cache instance that loads from same file cache2 = PerceptionCache(templates_path=templates_path) assert len(cache2.templates) == 1 assert cache2.templates[0].name == "loaded" assert cache2.templates[0].threshold == 0.88 # Images are now persisted as .npy files and loaded back assert cache2.templates[0].image.size > 0 def test_load_empty_file(self, tmp_path): """Load handles empty template list in file.""" from timmy.sovereignty.perception_cache import PerceptionCache templates_path = tmp_path / "templates.json" with open(templates_path, "w") as f: json.dump([], f) cache = PerceptionCache(templates_path=templates_path) assert cache.templates == [] class TestCrystallizePerception: """Tests for crystallize_perception function.""" def test_crystallize_returns_empty_list(self, tmp_path): """crystallize_perception currently returns empty list (placeholder).""" from timmy.sovereignty.perception_cache import crystallize_perception screenshot = np.array([[1, 2], [3, 4]]) result = crystallize_perception(screenshot, {"some": "response"}) assert result == [] def test_crystallize_accepts_any_vlm_response(self, tmp_path): """crystallize_perception accepts any vlm_response format.""" from timmy.sovereignty.perception_cache import crystallize_perception screenshot = np.array([[1, 2], [3, 4]]) # Test with various response types assert crystallize_perception(screenshot, None) == [] assert crystallize_perception(screenshot, {}) == [] assert crystallize_perception(screenshot, {"items": []}) == [] assert crystallize_perception(screenshot, "string response") == []