Spaces:
Build error
Build error
File size: 16,858 Bytes
c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 751fb9e e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 e4a41fa c1feb60 751fb9e c1feb60 e4a41fa c1feb60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 | """Tests demonstrating critical fixes for TOIN/CCR implementation.
These tests verify the before/after behavior of critical bug fixes:
1. TOIN confidence math error (line 721)
2. TOIN double-count bug (lines 354-358)
3. compression_feedback.py race condition (lines 481-491)
4. Unbounded strategy dicts in compression_feedback.py
5. SmartCrusher integration with TOIN
"""
import time
from unittest.mock import patch
import pytest
class TestTOINConfidenceMathFix:
"""Test for CRITICAL: Confidence calculation math error in toin.py:721.
BUG: `user_boost = min(0.3, pattern.user_count / 10 * 0.1)`
Due to operator precedence: user_count / 10 * 0.1 = user_count * 0.01
- 3 users: 0.03 boost (too small)
- 10 users: 0.1 boost
- 30 users needed to hit 0.3 cap!
FIX: Should be `min(0.3, pattern.user_count * 0.03)` for meaningful boost
- 3 users: 0.09 boost
- 10 users: 0.3 boost (capped)
"""
def test_confidence_user_boost_at_3_users(self):
"""With 3 users (min for network effect), boost should be meaningful."""
from headroom.telemetry.toin import (
TOINConfig,
ToolIntelligenceNetwork,
ToolPattern,
reset_toin,
)
reset_toin()
config = TOINConfig(min_users_for_network_effect=3)
toin = ToolIntelligenceNetwork(config)
# Create pattern with 3 users (correct API: tool_signature_hash is first arg)
pattern = ToolPattern(
tool_signature_hash="test123",
user_count=3,
sample_size=100, # Good sample size
)
confidence = toin._calculate_confidence(pattern)
# Sample confidence = min(0.7, 100/100) = 0.7
# User boost for 3 users should be meaningful (>= 0.05)
# FIX: With user_count * 0.03: boost = 0.09, total = 0.79
# BUG: With user_count * 0.01: boost = 0.03, total = 0.73
# After fix, confidence should be at least 0.75
assert confidence >= 0.75, (
f"Confidence {confidence} too low for 3 users - user boost not meaningful"
)
def test_confidence_user_boost_at_10_users(self):
"""With 10 users, boost should hit or approach cap."""
from headroom.telemetry.toin import (
TOINConfig,
ToolIntelligenceNetwork,
ToolPattern,
reset_toin,
)
reset_toin()
config = TOINConfig(min_users_for_network_effect=3)
toin = ToolIntelligenceNetwork(config)
pattern = ToolPattern(
tool_signature_hash="test123",
user_count=10,
sample_size=100,
)
confidence = toin._calculate_confidence(pattern)
# With 10 users, should be near cap (0.95)
# Sample confidence = 0.7, user boost should be 0.3 (capped)
# Total = min(0.95, 0.7 + 0.3) = 0.95
# BUG: user_boost = 0.1, total = 0.8
assert confidence >= 0.9, f"Confidence {confidence} too low for 10 users"
class TestTOINDoubleCountFix:
"""Test for CRITICAL: Double-count bug in toin.py:354-358.
BUG: When _seen_instance_hashes hits cap (100), new instance_ids are NOT stored
but user_count IS incremented. Next call with same instance_id:
- `if self._instance_id not in pattern._seen_instance_hashes` → True (not stored!)
- user_count incremented AGAIN → Double counting!
FIX: Use a separate set to track ALL seen instances (no cap for lookup),
OR check if we already tracked overflow for this instance.
"""
def test_user_count_no_double_counting_after_cap(self):
"""Same instance shouldn't be counted twice even after cap hit."""
from headroom.telemetry.models import ToolSignature
from headroom.telemetry.toin import TOINConfig, ToolIntelligenceNetwork, reset_toin
reset_toin()
toin = ToolIntelligenceNetwork(TOINConfig())
# Create a signature using the correct factory method
items = [{"field1": "value1", "field2": 123}]
sig = ToolSignature.from_items(items)
# Simulate 101 unique instances (exceed the 100 cap)
# First, fill up the cap with 100 unique instances
original_instance_id = toin._instance_id
for i in range(100):
toin._instance_id = f"instance_{i}"
toin.record_compression(sig, 100, 10, 1000, 100, strategy="test_strategy")
# Now add one more instance (exceeds cap)
toin._instance_id = "instance_100"
toin.record_compression(sig, 100, 10, 1000, 100, strategy="test_strategy")
# Get the pattern
with toin._lock:
pattern = toin._patterns[sig.structure_hash]
user_count_after_101 = pattern.user_count
# Now call again with same instance (instance_100)
# BUG: This would increment user_count again because instance_100
# was not stored (cap hit) so the check passes again
toin.record_compression(sig, 100, 10, 1000, 100, strategy="test_strategy")
with toin._lock:
pattern = toin._patterns[sig.structure_hash]
user_count_after_102 = pattern.user_count
# Restore instance_id
toin._instance_id = original_instance_id
# User count should NOT increase for same instance
assert user_count_after_102 == user_count_after_101, (
f"Double-counting bug: user_count went from {user_count_after_101} to "
f"{user_count_after_102} for same instance after cap hit"
)
class TestCompressionFeedbackRaceCondition:
"""Test for CRITICAL: Race condition in compression_feedback.py:481-491.
BUG: _last_event_timestamp is read (line 481) and written (line 491)
WITHOUT holding the lock. Another thread calling record_retrieval()
between these could cause events to be missed or double-counted.
FIX: Move timestamp filtering and update inside the lock.
"""
def test_analyze_from_store_thread_safety(self):
"""Concurrent analyze_from_store and record_retrieval should not lose events."""
from headroom.cache.compression_feedback import (
CompressionFeedback,
reset_compression_feedback,
)
from headroom.cache.compression_store import CompressionStore, RetrievalEvent
reset_compression_feedback()
# Create store with mock events
store = CompressionStore()
feedback = CompressionFeedback(store=store, analysis_interval=0.0) # No rate limiting
# Pre-populate some events with correct API
base_time = time.time()
events_recorded = []
def add_retrieval_event(tool_name: str, timestamp: float):
event = RetrievalEvent(
hash="test_hash",
query=None,
items_retrieved=10,
total_items=100,
tool_name=tool_name,
timestamp=timestamp,
retrieval_type="full",
)
# Directly add to feedback (simulating what analyze_from_store does)
feedback.record_retrieval(event)
events_recorded.append(event)
# Record some events
for i in range(10):
add_retrieval_event(f"tool_{i % 3}", base_time + i)
with feedback._lock:
total_retrievals = feedback._total_retrievals
patterns_count = len(feedback._tool_patterns)
# All 10 events should be recorded
assert total_retrievals == 10, f"Expected 10 retrievals, got {total_retrievals}"
# Should have 3 unique tools (tool_0, tool_1, tool_2)
assert patterns_count == 3, f"Expected 3 tool patterns, got {patterns_count}"
def test_timestamp_filtering_inside_lock(self):
"""Verify that timestamp filtering happens atomically with update."""
from headroom.cache.compression_feedback import (
CompressionFeedback,
reset_compression_feedback,
)
from headroom.cache.compression_store import CompressionStore, RetrievalEvent
reset_compression_feedback()
store = CompressionStore()
feedback = CompressionFeedback(store=store, analysis_interval=0.0)
# Manually set last event timestamp
feedback._last_event_timestamp = 100.0
# Create mock store with events (correct API)
mock_events = [
RetrievalEvent(
hash="h1",
query=None,
items_retrieved=5,
total_items=50,
tool_name="tool_a",
timestamp=99.0,
retrieval_type="full",
),
RetrievalEvent(
hash="h2",
query=None,
items_retrieved=5,
total_items=50,
tool_name="tool_b",
timestamp=101.0,
retrieval_type="full",
),
RetrievalEvent(
hash="h3",
query="test",
items_retrieved=5,
total_items=50,
tool_name="tool_c",
timestamp=102.0,
retrieval_type="search",
),
]
# Mock store.get_retrieval_events
with patch.object(store, "get_retrieval_events", return_value=mock_events):
feedback.analyze_from_store()
# Only events with timestamp > 100.0 should be processed (h2, h3)
with feedback._lock:
total = feedback._total_retrievals
# The timestamp should now be 102.0 (max of processed events)
last_ts = feedback._last_event_timestamp
assert total == 2, f"Expected 2 new events processed, got {total}"
assert last_ts == 102.0, f"Expected last_event_timestamp=102.0, got {last_ts}"
class TestUnboundedStrategyDicts:
"""Test for HIGH: Unbounded strategy_compressions/strategy_retrievals dicts.
BUG: Unlike common_queries (truncated at 100) and queried_fields (truncated at 50),
the strategy dicts have no size limits and could grow unbounded.
FIX: Add truncation logic similar to other dicts.
"""
def test_strategy_dicts_have_size_limits(self):
"""Strategy dicts should be bounded to prevent memory leaks."""
from headroom.cache.compression_feedback import (
CompressionFeedback,
reset_compression_feedback,
)
from headroom.cache.compression_store import CompressionStore
reset_compression_feedback()
store = CompressionStore()
feedback = CompressionFeedback(store=store)
# Record many compressions with different strategies
for i in range(200):
feedback.record_compression(
tool_name="test_tool",
original_count=100,
compressed_count=10,
strategy=f"strategy_{i}", # 200 unique strategies
)
with feedback._lock:
pattern = feedback._tool_patterns.get("test_tool")
strategy_count = len(pattern.strategy_compressions) if pattern else 0
# Strategy dict should be bounded (e.g., to 50 like queried_fields)
assert strategy_count <= 50, (
f"strategy_compressions has {strategy_count} entries, should be <= 50"
)
class TestSmartCrusherTOINIntegration:
"""Test for CRITICAL: SmartCrusher not calling toin.record_compression().
BUG: SmartCrusher calls feedback.record_compression() but never calls
toin.record_compression(). This means TOIN only learns from retrieval events,
not from compression events - breaking the feedback loop.
FIX: Add toin.record_compression() call after compression in SmartCrusher.
"""
def test_smart_crusher_records_to_toin(self):
"""SmartCrusher should record compression events to TOIN."""
from headroom.telemetry.models import ToolSignature
from headroom.telemetry.toin import get_toin, reset_toin
from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig
reset_toin()
config = SmartCrusherConfig(
min_items_to_analyze=5,
max_items_after_crush=10,
use_feedback_hints=True,
)
crusher = SmartCrusher(config)
# Create test items that look like search results with a clear score field
# This pattern is crushable because:
# 1. Has a clear numeric score field in BOUNDED range [0,1]
# 2. Has repeated structure with some constant fields (type, language)
# 3. Score values vary within the bounded range
items = [
{
"name": f"repo_{i}",
"relevance_score": (50 - i) / 50.0, # Bounded [0,1] - descending order
"type": "repository", # Constant field
"language": "python" if i % 3 == 0 else "javascript", # Low cardinality
"description": f"Description {i % 5}", # Low cardinality
}
for i in range(50)
]
# Get TOIN instance and check initial state
toin = get_toin()
len(toin._patterns)
# Crush the array
result, info, markers, _summary = crusher._crush_array(
items, query_context="test query", tool_name="test_tool"
)
# Verify compression happened (not skipped)
assert "skip" not in info.lower(), (
f"Compression was skipped: {info}. Test needs crushable data."
)
# Get the signature that would have been created
sig = ToolSignature.from_items(items)
# Check TOIN was notified
with toin._lock:
pattern = toin._patterns.get(sig.structure_hash)
# After fix, TOIN should have a pattern for this tool's signature
assert pattern is not None, (
f"TOIN should have recorded the compression event. "
f"Info: {info}, pattern count: {len(toin._patterns)}"
)
if pattern:
assert pattern.total_compressions >= 1, (
f"Pattern should have at least 1 compression recorded, got {pattern.total_compressions}"
)
class TestAllFixesIntegrated:
"""Integration tests ensuring all fixes work together."""
def test_full_feedback_loop(self):
"""Test complete feedback loop: compress -> store -> retrieve -> learn."""
from headroom.cache.compression_feedback import (
reset_compression_feedback,
)
from headroom.cache.compression_store import reset_compression_store
from headroom.telemetry.models import ToolSignature
from headroom.telemetry.toin import get_toin, reset_toin
from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig
# Reset all singletons
reset_toin()
reset_compression_store()
reset_compression_feedback()
# Setup
config = SmartCrusherConfig(
min_items_to_analyze=5,
max_items_after_crush=10,
use_feedback_hints=True,
)
crusher = SmartCrusher(config)
# Create test items that look like API responses with scoring
# This pattern is crushable because:
# 1. Has a clear numeric score field in BOUNDED range [0,1]
# 2. Has constant fields (status, type)
# 3. Has enough items for compression (100)
items = [
{
"priority": (100 - i) / 100.0, # Bounded [0,1] - descending order
"status": "ok", # Constant field
"type": "response", # Constant field
"data": f"content_{i % 10}", # Low cardinality (only 10 unique values)
}
for i in range(100)
]
# Step 1: Compress
result, info, markers, _summary = crusher._crush_array(
items, query_context="find status", tool_name="api_response"
)
# Verify compression happened (not skipped)
assert "skip" not in info.lower(), (
f"Compression was skipped: {info}. Test needs crushable data."
)
# Step 2: Check TOIN was notified (after fix)
toin = get_toin()
sig = ToolSignature.from_items(items)
with toin._lock:
toin_pattern = toin._patterns.get(sig.structure_hash)
# After fix, TOIN should have the pattern
assert toin_pattern is not None, (
f"TOIN should have learned from the compression event. Info: {info}"
)
assert toin_pattern.total_compressions >= 1, (
f"TOIN pattern should have recorded compression, got {toin_pattern.total_compressions}"
)
# Run specific test to verify fix
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
|