"""Tests for Tool Output Intelligence Network (TOIN). PR-B5 retired the request-time hint API. Tests that exercised the old `get_recommendation()` / `CompressionHint` shape are skipped at module level — the new observation-only contract is covered by `tests/test_toin_observation_only.py` and `tests/test_toin_publish.py`. """ import os import tempfile import time import pytest from headroom.telemetry import ( TOINConfig, ToolIntelligenceNetwork, ToolPattern, ToolSignature, get_toin, reset_toin, ) @pytest.fixture(autouse=True) def reset_globals(monkeypatch, tmp_path): """Reset global state before each test. Also disables disk persistence by setting HEADROOM_TOIN_PATH to a temp file to avoid loading stale data from ~/.headroom/toin.json. """ # Use a unique temp file for each test to avoid cross-test contamination temp_toin_path = str(tmp_path / "toin_test.json") monkeypatch.setenv("HEADROOM_TOIN_PATH", temp_toin_path) reset_toin() yield reset_toin() class TestToolPattern: """Test ToolPattern data model.""" def test_to_dict(self): """to_dict serializes all fields.""" pattern = ToolPattern( tool_signature_hash="abc12345", total_compressions=100, total_items_seen=5000, total_items_kept=500, avg_compression_ratio=0.1, avg_token_reduction=0.8, total_retrievals=20, full_retrievals=15, search_retrievals=5, commonly_retrieved_fields=["field1", "field2"], optimal_strategy="top_n", optimal_max_items=25, sample_size=100, confidence=0.75, ) d = pattern.to_dict() assert d["tool_signature_hash"] == "abc12345" assert d["total_compressions"] == 100 assert d["total_items_seen"] == 5000 assert d["avg_compression_ratio"] == 0.1 assert d["retrieval_rate"] == 0.2 # 20/100 assert d["full_retrieval_rate"] == 0.75 # 15/20 assert d["commonly_retrieved_fields"] == ["field1", "field2"] assert d["optimal_strategy"] == "top_n" def test_from_dict(self): """from_dict deserializes correctly.""" data = { "tool_signature_hash": "xyz789", "total_compressions": 50, "total_retrievals": 10, "full_retrievals": 8, "commonly_retrieved_fields": ["field_a"], "optimal_max_items": 30, "confidence": 0.6, } pattern = ToolPattern.from_dict(data) assert pattern.tool_signature_hash == "xyz789" assert pattern.total_compressions == 50 assert pattern.total_retrievals == 10 assert pattern.full_retrievals == 8 assert pattern.commonly_retrieved_fields == ["field_a"] assert pattern.optimal_max_items == 30 assert pattern.confidence == 0.6 def test_from_dict_ignores_unknown_fields(self): """from_dict ignores unknown fields.""" data = { "tool_signature_hash": "abc123", "total_compressions": 10, "unknown_field": "should be ignored", "another_unknown": 12345, } pattern = ToolPattern.from_dict(data) assert pattern.tool_signature_hash == "abc123" assert not hasattr(pattern, "unknown_field") def test_retrieval_rate_property(self): """retrieval_rate is calculated correctly.""" pattern = ToolPattern( tool_signature_hash="test", total_compressions=100, total_retrievals=30, ) assert pattern.retrieval_rate == 0.3 def test_retrieval_rate_zero_compressions(self): """retrieval_rate is 0 when no compressions.""" pattern = ToolPattern( tool_signature_hash="test", total_compressions=0, ) assert pattern.retrieval_rate == 0.0 def test_full_retrieval_rate_property(self): """full_retrieval_rate is calculated correctly.""" pattern = ToolPattern( tool_signature_hash="test", total_retrievals=20, full_retrievals=15, ) assert pattern.full_retrieval_rate == 0.75 def test_full_retrieval_rate_zero_retrievals(self): """full_retrieval_rate is 0 when no retrievals.""" pattern = ToolPattern( tool_signature_hash="test", total_retrievals=0, ) assert pattern.full_retrieval_rate == 0.0 class TestTOINConfig: """Test TOINConfig data model.""" def test_default_values(self): """Default config values.""" config = TOINConfig() assert config.enabled is True # Storage path comes from HEADROOM_TOIN_PATH env var (set by fixture) or default # Just verify it's a non-empty string assert isinstance(config.storage_path, str) assert len(config.storage_path) > 0 assert config.auto_save_interval == 600 assert config.min_samples_for_recommendation == 10 assert config.min_users_for_network_effect == 3 assert config.high_retrieval_threshold == 0.5 assert config.medium_retrieval_threshold == 0.2 assert config.anonymize_queries is True def test_custom_values(self): """Custom config values.""" config = TOINConfig( enabled=False, storage_path="/tmp/toin.json", min_samples_for_recommendation=5, high_retrieval_threshold=0.7, ) assert config.enabled is False assert config.storage_path == "/tmp/toin.json" assert config.min_samples_for_recommendation == 5 assert config.high_retrieval_threshold == 0.7 class TestToolIntelligenceNetwork: """Test ToolIntelligenceNetwork class.""" def test_record_compression(self): """Recording compression updates pattern.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1", "name": "test"}]) toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=5000, compressed_tokens=500, strategy="top_n", ) pattern = toin.get_pattern(sig.structure_hash) assert pattern is not None assert pattern.total_compressions == 1 assert pattern.total_items_seen == 100 assert pattern.total_items_kept == 10 assert pattern.avg_compression_ratio == 0.1 def test_record_compression_disabled(self): """Disabled TOIN does not record.""" config = TOINConfig(enabled=False) toin = ToolIntelligenceNetwork(config) sig = ToolSignature.from_items([{"id": "1"}]) toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) pattern = toin.get_pattern(sig.structure_hash) assert pattern is None def test_record_compression_multiple(self): """Multiple compressions update rolling averages.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1"}]) # Record 5 compressions with varying ratios for i in range(5): toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10 + i * 5, # 10, 15, 20, 25, 30 original_tokens=1000, compressed_tokens=100 + i * 50, strategy="top_n", ) pattern = toin.get_pattern(sig.structure_hash) assert pattern.total_compressions == 5 assert pattern.sample_size == 5 assert pattern.total_items_seen == 500 # 100 * 5 # Average compression ratio: (0.1 + 0.15 + 0.2 + 0.25 + 0.3) / 5 = 0.2 assert 0.19 < pattern.avg_compression_ratio < 0.21 def test_record_retrieval(self): """Recording retrieval updates pattern.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1"}]) sig_hash = sig.structure_hash # First record compression toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) # Then record retrieval toin.record_retrieval( tool_signature_hash=sig_hash, retrieval_type="full", ) pattern = toin.get_pattern(sig_hash) assert pattern.total_retrievals == 1 assert pattern.full_retrievals == 1 assert pattern.search_retrievals == 0 assert pattern.retrieval_rate == 1.0 # 1/1 def test_record_retrieval_search(self): """Search retrievals are tracked separately.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1"}]) sig_hash = sig.structure_hash toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) # Record search retrieval with query toin.record_retrieval( tool_signature_hash=sig_hash, retrieval_type="search", query="status:error", query_fields=["status"], ) pattern = toin.get_pattern(sig_hash) assert pattern.total_retrievals == 1 assert pattern.full_retrievals == 0 assert pattern.search_retrievals == 1 def test_record_retrieval_tracks_query_fields(self): """Query fields are tracked (anonymized).""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1", "status": "ok"}]) sig_hash = sig.structure_hash toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) # Record multiple retrievals for same field for _ in range(5): toin.record_retrieval( tool_signature_hash=sig_hash, retrieval_type="search", query_fields=["status"], ) pattern = toin.get_pattern(sig_hash) # Field should be in commonly_retrieved_fields after 3+ retrievals assert len(pattern.commonly_retrieved_fields) > 0 # PR-B5: the following tests exercised the request-time hint API # that's now retired. They're skipped wholesale; the new contract # ("get_recommendation always returns None and emits a deprecation # warning") is covered by tests/test_toin_observation_only.py. @pytest.mark.skip( reason="PR-B5: get_recommendation retired — see test_toin_observation_only.py" ) def test_get_recommendation_no_data(self): pass @pytest.mark.skip( reason="PR-B5: get_recommendation retired — see test_toin_observation_only.py" ) def test_get_recommendation_insufficient_samples(self): pass @pytest.mark.skip( reason="PR-B5: get_recommendation retired — see test_toin_observation_only.py" ) def test_get_recommendation_aggressive_compression(self): pass @pytest.mark.skip( reason="PR-B5: get_recommendation retired — see test_toin_observation_only.py" ) def test_get_recommendation_conservative_compression(self): pass @pytest.mark.skip( reason="PR-B5: get_recommendation retired — see test_toin_observation_only.py" ) def test_get_recommendation_skip_compression(self): pass @pytest.mark.skip( reason="PR-B5: get_recommendation retired — see test_toin_observation_only.py" ) def test_get_recommendation_disabled(self): pass def test_get_stats(self): """get_stats returns overall statistics.""" toin = ToolIntelligenceNetwork() sig1 = ToolSignature.from_items([{"id": "1", "name": "test"}]) sig2 = ToolSignature.from_items([{"code": 200, "data": {"x": 1}}]) # Record compressions for two different tool types for _ in range(5): toin.record_compression( tool_signature=sig1, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) for _ in range(3): toin.record_compression( tool_signature=sig2, original_count=50, compressed_count=5, original_tokens=500, compressed_tokens=50, strategy="smart_sample", ) # Record some retrievals toin.record_retrieval(sig1.structure_hash, "full") toin.record_retrieval(sig2.structure_hash, "search") stats = toin.get_stats() assert stats["patterns_tracked"] == 2 assert stats["total_compressions"] == 8 # 5 + 3 assert stats["total_retrievals"] == 2 assert stats["enabled"] is True def test_clear(self): """clear() removes all patterns.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1"}]) toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) toin.clear() stats = toin.get_stats() assert stats["patterns_tracked"] == 0 assert stats["total_compressions"] == 0 class TestTOINExportImport: """Test TOIN export/import for federated learning.""" def test_export_patterns(self): """export_patterns produces complete data.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1", "name": "test"}]) toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) export = toin.export_patterns() assert "version" in export assert "export_timestamp" in export assert "instance_id" in export assert "patterns" in export assert len(export["patterns"]) == 1 # PR-B5: keys are now serialized "auth|model|hash" tuples; default # auth/model produce the "unknown|unknown|" string. assert f"unknown|unknown|{sig.structure_hash}" in export["patterns"] def test_import_patterns_new_pattern(self): """import_patterns adds new patterns.""" toin = ToolIntelligenceNetwork() # Import pattern data import_data = { "version": "1.0", "export_timestamp": time.time(), "instance_id": "other_instance", "patterns": { "abc123": { "tool_signature_hash": "abc123", "total_compressions": 50, "total_retrievals": 10, "sample_size": 50, "confidence": 0.5, }, }, } toin.import_patterns(import_data) pattern = toin.get_pattern("abc123") assert pattern is not None assert pattern.total_compressions == 50 assert pattern.user_count >= 1 def test_import_patterns_merge_existing(self): """import_patterns merges with existing patterns.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1"}]) # Record local compressions for _ in range(10): toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) # Import similar pattern from another instance import_data = { "version": "1.0", "export_timestamp": time.time(), "instance_id": "other_instance", "patterns": { sig.structure_hash: { "tool_signature_hash": sig.structure_hash, "total_compressions": 20, "total_retrievals": 5, "total_items_seen": 2000, "total_items_kept": 200, "sample_size": 20, "avg_compression_ratio": 0.15, }, }, } toin.import_patterns(import_data) pattern = toin.get_pattern(sig.structure_hash) assert pattern.total_compressions == 30 # 10 + 20 assert pattern.sample_size == 30 assert pattern.user_count >= 1 def test_import_patterns_disabled(self): """Import disabled does nothing.""" config = TOINConfig(enabled=False) toin = ToolIntelligenceNetwork(config) import_data = { "version": "1.0", "patterns": { "abc123": {"tool_signature_hash": "abc123", "total_compressions": 50}, }, } toin.import_patterns(import_data) pattern = toin.get_pattern("abc123") assert pattern is None def test_round_trip_export_import(self): """Export from one TOIN imports to another.""" toin1 = ToolIntelligenceNetwork() toin2 = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1", "score": 0.5}]) # Populate toin1 for _ in range(15): toin1.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) # Record retrievals for _ in range(3): toin1.record_retrieval( sig.structure_hash, "search", query="score>0.8", query_fields=["score"], ) # Export and import export = toin1.export_patterns() toin2.import_patterns(export) # Verify import pattern = toin2.get_pattern(sig.structure_hash) assert pattern is not None assert pattern.total_compressions == 15 assert pattern.total_retrievals == 3 class TestTOINPersistence: """Test TOIN persistence to disk.""" def test_save_and_load(self): """Save and load preserves TOIN data.""" with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f: storage_path = f.name try: # Create and populate TOIN config = TOINConfig(storage_path=storage_path) toin = ToolIntelligenceNetwork(config) sig = ToolSignature.from_items([{"id": "1", "name": "test"}]) for _ in range(5): toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) toin.save() # Verify file exists assert os.path.exists(storage_path) # Create new TOIN that loads from disk toin2 = ToolIntelligenceNetwork(config) stats = toin2.get_stats() assert stats["total_compressions"] == 5 finally: os.unlink(storage_path) def test_load_corrupted_file(self): """Corrupted file is handled gracefully.""" with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") as f: f.write("not valid json {{{") storage_path = f.name try: config = TOINConfig(storage_path=storage_path) toin = ToolIntelligenceNetwork(config) # Should not raise, starts fresh stats = toin.get_stats() assert stats["patterns_tracked"] == 0 finally: os.unlink(storage_path) def test_load_nonexistent_file(self): """Nonexistent file is handled gracefully.""" config = TOINConfig(storage_path="/nonexistent/path/toin.json") toin = ToolIntelligenceNetwork(config) # Should not raise, starts fresh stats = toin.get_stats() assert stats["patterns_tracked"] == 0 class TestGlobalTOIN: """Test global TOIN singleton.""" def test_singleton_returns_same_instance(self): """get_toin returns same instance.""" toin1 = get_toin() toin2 = get_toin() assert toin1 is toin2 def test_reset_clears_singleton(self): """reset_toin creates new instance.""" toin1 = get_toin() sig = ToolSignature.from_items([{"id": "1"}]) toin1.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) reset_toin() toin2 = get_toin() stats = toin2.get_stats() assert stats["total_compressions"] == 0 def test_get_toin_with_config(self): """First call to get_toin accepts config.""" reset_toin() config = TOINConfig(min_samples_for_recommendation=5) toin = get_toin(config) assert toin._config.min_samples_for_recommendation == 5 class TestTOINQueryAnonymization: """Test query pattern anonymization.""" def test_anonymize_query_pattern(self): """Query values are anonymized.""" toin = ToolIntelligenceNetwork() # Test internal method pattern = toin._anonymize_query_pattern("status:error AND user:john") assert pattern is not None assert "error" not in pattern.lower() assert "john" not in pattern.lower() # Should have structure preserved assert "status:*" in pattern or "*" in pattern def test_anonymize_empty_query(self): """Empty query returns None.""" toin = ToolIntelligenceNetwork() pattern = toin._anonymize_query_pattern("") assert pattern is None def test_hash_field_name(self): """Field names are hashed consistently.""" toin = ToolIntelligenceNetwork() hash1 = toin._hash_field_name("status") hash2 = toin._hash_field_name("status") hash3 = toin._hash_field_name("different") assert hash1 == hash2 # Same input = same hash assert hash1 != hash3 # Different input = different hash assert len(hash1) == 8 # SHA256[:8] class TestTOINConfidence: """Test confidence calculation.""" def test_confidence_increases_with_samples(self): """More samples increase confidence.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1"}]) confidences = [] for i in range(50): toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) if (i + 1) % 10 == 0: pattern = toin.get_pattern(sig.structure_hash) confidences.append(pattern.confidence) # Confidence should generally increase (or at least not decrease significantly) assert confidences[-1] >= confidences[0] def test_confidence_capped_at_max(self): """Confidence never exceeds maximum.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1"}]) # Record many compressions for _ in range(500): toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) pattern = toin.get_pattern(sig.structure_hash) assert pattern.confidence <= 0.95 class TestTOINRecommendationUpdates: """Test that recommendations update based on retrieval patterns.""" def test_optimal_max_items_updates(self): """optimal_max_items updates based on retrieval rate.""" config = TOINConfig( min_samples_for_recommendation=5, high_retrieval_threshold=0.5, ) toin = ToolIntelligenceNetwork(config) sig = ToolSignature.from_items([{"id": "1"}]) sig_hash = sig.structure_hash # Low retrieval rate - aggressive compression OK for _ in range(20): toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) pattern1 = toin.get_pattern(sig_hash) initial_max = pattern1.optimal_max_items # Now add many retrievals (high retrieval rate) for _ in range(15): # 15/20 = 75% retrieval rate toin.record_retrieval(sig_hash, "search") pattern2 = toin.get_pattern(sig_hash) # Should recommend more items due to high retrieval assert pattern2.optimal_max_items > initial_max def test_preserve_fields_populated(self): """preserve_fields populated from retrieval patterns.""" toin = ToolIntelligenceNetwork() sig = ToolSignature.from_items([{"id": "1", "status": "ok", "score": 0.5}]) sig_hash = sig.structure_hash # Record compression toin.record_compression( tool_signature=sig, original_count=100, compressed_count=10, original_tokens=1000, compressed_tokens=100, strategy="top_n", ) # Repeatedly retrieve by same field for _ in range(10): toin.record_retrieval( sig_hash, "search", query_fields=["status"], ) pattern = toin.get_pattern(sig_hash) # Field should be marked to preserve assert len(pattern.preserve_fields) > 0