headroom/tests/test_transforms/test_smart_crusher.py
chopratejas e4a41faa33 Fix all ruff lint and format errors for CI
- Fix E402: Move module-level imports to top of file
- Fix F401: Add noqa for availability check imports
- Fix F402: Rename loop variables shadowing imports
- Fix E722: Replace bare except with except Exception
- Fix B904: Add exception chaining (from e)
- Fix F811: Remove duplicate imports
- Fix B027: Add noqa for empty close() method
- Fix E741: Rename ambiguous variable l -> label
- Fix I001: Import sorting issues
- Apply ruff format to all 106 files

All 902 tests pass.
2026-01-10 15:33:44 -08:00

1359 lines
47 KiB
Python

"""Tests for SmartCrusher transform.
Comprehensive tests covering:
- SmartAnalyzer: Statistical analysis of arrays
- SmartCrusher: Intelligent compression with Safe V1 Recipe
- RelevanceScoring: Context extraction and item matching
- Edge cases: Malformed JSON, nested arrays, different message formats
"""
import json
import pytest
from headroom import (
OpenAIProvider,
RelevanceScorerConfig,
SmartCrusherConfig,
Tokenizer,
)
from headroom.relevance import RelevanceScore, RelevanceScorer
from headroom.transforms.smart_crusher import (
CompressionStrategy,
SmartAnalyzer,
SmartCrusher,
)
# =============================================================================
# Test Fixtures
# =============================================================================
# Create a shared provider for tests
_provider = OpenAIProvider()
def get_tokenizer(model: str = "gpt-4o") -> Tokenizer:
"""Get a tokenizer for tests using OpenAI provider."""
token_counter = _provider.get_token_counter(model)
return Tokenizer(token_counter, model)
@pytest.fixture
def tokenizer():
"""Provide a tokenizer for tests."""
return get_tokenizer()
@pytest.fixture
def default_config():
"""Default SmartCrusherConfig for testing."""
return SmartCrusherConfig(
enabled=True,
min_items_to_analyze=3,
min_tokens_to_crush=0, # Always crush for tests
max_items_after_crush=10,
variance_threshold=2.0,
)
@pytest.fixture
def analyzer(default_config):
"""SmartAnalyzer instance for testing."""
return SmartAnalyzer(default_config)
@pytest.fixture
def crusher(default_config):
"""SmartCrusher instance for testing."""
return SmartCrusher(default_config)
# =============================================================================
# Test Data Generators
# =============================================================================
def generate_time_series_data(n: int = 20, with_spike: bool = False) -> list[dict]:
"""Generate time series data with optional anomaly."""
data = []
for i in range(n):
value = 100.0 + (i * 0.5) # Slight upward trend
if with_spike and i == n // 2:
value = 500.0 # Spike in the middle
data.append(
{
"timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z",
"value": value,
"metric": "cpu_usage",
}
)
return data
def generate_log_data(n: int = 20, with_errors: bool = False) -> list[dict]:
"""Generate log-style data with optional errors."""
data = []
levels = ["INFO", "DEBUG", "WARN"]
for i in range(n):
level = levels[i % len(levels)]
if with_errors and i in [5, 15]:
level = "ERROR"
message = f"Connection failed: timeout after 30s (attempt {i})"
else:
message = f"Processing request {i} successfully"
data.append(
{
"level": level,
"message": message,
"timestamp": f"2025-01-06T{12 + (i // 60):02d}:{i % 60:02d}:00Z",
}
)
return data
def generate_search_results(n: int = 20) -> list[dict]:
"""Generate search results with scores."""
return [
{
"id": f"doc_{i}",
"title": f"Document {i}",
"score": 1.0 - (i * 0.05),
"snippet": f"This is a snippet from document {i}...",
}
for i in range(n)
]
def generate_generic_data(
n: int = 20,
constant_field: bool = False,
with_signals: bool = False,
) -> list[dict]:
"""Generate generic array data.
Args:
n: Number of items to generate
constant_field: If True, type field is constant "product"
with_signals: If True, adds importance signals (errors, anomalies)
to enable crushing with new statistical detection
"""
items = []
for i in range(n):
item = {
"id": i,
"name": f"Item {i}",
"type": "product" if constant_field else f"type_{i % 3}",
"active": True if constant_field else (i % 2 == 0),
}
if with_signals:
item["value"] = 100.0
# Add some errors
if i == n // 4:
item["error"] = f"Error at {i}"
# Add some anomalies
if i == n // 2:
item["value"] = 99999.0
items.append(item)
return items
# =============================================================================
# TestSmartAnalyzer
# =============================================================================
class TestSmartAnalyzer:
"""Tests for SmartAnalyzer class."""
def test_analyze_empty_array(self, analyzer):
"""Empty array should return analysis with no field stats."""
result = analyzer.analyze_array([])
assert result.item_count == 0
assert result.field_stats == {}
assert result.detected_pattern == "generic"
assert result.recommended_strategy == CompressionStrategy.NONE
assert result.constant_fields == {}
def test_analyze_single_item(self, analyzer):
"""Single item array should return analysis but no compression."""
items = [{"id": 1, "name": "Test"}]
result = analyzer.analyze_array(items)
assert result.item_count == 1
assert "id" in result.field_stats
assert "name" in result.field_stats
# Single item means constant fields
assert result.field_stats["id"].is_constant
assert result.field_stats["name"].is_constant
def test_analyze_numeric_field_stats(self, analyzer):
"""Numeric fields should have correct statistics computed."""
items = [
{"value": 10.0},
{"value": 20.0},
{"value": 30.0},
{"value": 40.0},
{"value": 50.0},
]
result = analyzer.analyze_array(items)
stats = result.field_stats["value"]
assert stats.field_type == "numeric"
assert stats.min_val == 10.0
assert stats.max_val == 50.0
assert stats.mean_val == 30.0
assert stats.variance is not None
assert stats.variance > 0
def test_analyze_string_field_stats(self, analyzer):
"""String fields should have correct statistics computed."""
items = [
{"name": "Alice"},
{"name": "Bob"},
{"name": "Alice"}, # Duplicate
{"name": "Charlie"},
{"name": "Alice"}, # Another duplicate
]
result = analyzer.analyze_array(items)
stats = result.field_stats["name"]
assert stats.field_type == "string"
assert stats.avg_length is not None
assert stats.top_values is not None
# Alice appears 3 times, should be top
assert stats.top_values[0][0] == "Alice"
assert stats.top_values[0][1] == 3
def test_detect_time_series_pattern(self, analyzer):
"""Time series data should be detected correctly."""
# Create data with timestamp and numeric variance
# Include anomaly to provide an importance signal for crushing
items = []
for i in range(40):
# Create variance-inducing data
value = 100.0 + (i * 2.0) # Steady increase with variance
if i == 20:
value = 999.0 # Anomaly provides importance signal
items.append(
{
"timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z",
"value": value,
"metric": "cpu_usage",
}
)
result = analyzer.analyze_array(items)
# Pattern should be detected as time_series (timestamp + numeric variance)
assert result.detected_pattern == "time_series"
# With anomaly signal, strategy should allow crushing
assert result.recommended_strategy in [
CompressionStrategy.TIME_SERIES,
CompressionStrategy.SMART_SAMPLE,
]
def test_detect_time_series_pattern_with_change_points(self):
"""Time series with clear change points should use TIME_SERIES strategy."""
# The change point detection threshold is variance_threshold * std
# To detect a change point, the before/after mean difference must exceed this
# With bimodal data, std is very high. We need a lower variance_threshold
# to reliably detect change points, OR the test should use a config
# with lower variance threshold.
config = SmartCrusherConfig(
min_items_to_analyze=3,
variance_threshold=1.0, # Lower threshold to detect changes
)
analyzer = SmartAnalyzer(config)
# Create data with clear step change
items = []
for i in range(40):
if i < 20:
value = 100.0 + (i * 0.5) # Values around 100-110
else:
value = 300.0 + ((i - 20) * 0.5) # Values around 300-310 (jump)
items.append(
{
"timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z",
"value": value,
"metric": "cpu_usage",
}
)
result = analyzer.analyze_array(items)
assert result.detected_pattern == "time_series"
# With lower variance_threshold, change points should be detected
value_stats = result.field_stats.get("value")
assert value_stats is not None
# Even with low threshold, bimodal data has high std
# The test verifies the strategy selection logic
if len(value_stats.change_points) > 0:
assert result.recommended_strategy == CompressionStrategy.TIME_SERIES
else:
# If change points still not detected, strategy falls back
assert result.recommended_strategy in [
CompressionStrategy.TIME_SERIES,
CompressionStrategy.SMART_SAMPLE,
]
def test_detect_logs_pattern(self, analyzer):
"""Log data should be detected correctly."""
# Use logs WITH errors to provide importance signal
items = generate_log_data(20, with_errors=True)
result = analyzer.analyze_array(items)
# With structural detection, logs are detected as logs pattern
# but strategy depends on crushability analysis
assert result.detected_pattern in ["logs", "generic"]
# With error items providing signal, crushing can proceed
assert result.recommended_strategy in [
CompressionStrategy.CLUSTER_SAMPLE,
CompressionStrategy.SMART_SAMPLE,
CompressionStrategy.SKIP, # May still skip if other conditions met
]
def test_detect_search_results_pattern(self, analyzer):
"""Search results with scores should be detected correctly."""
items = generate_search_results(20)
result = analyzer.analyze_array(items)
assert result.detected_pattern == "search_results"
assert result.recommended_strategy == CompressionStrategy.TOP_N
def test_detect_generic_pattern(self, analyzer):
"""Generic data without special patterns should be detected."""
items = generate_generic_data(20)
result = analyzer.analyze_array(items)
assert result.detected_pattern == "generic"
# With new crushability analysis: unique IDs + no importance signal = SKIP
# This is the safe behavior to avoid dropping important unique entities
assert result.recommended_strategy in [
CompressionStrategy.SMART_SAMPLE,
CompressionStrategy.SKIP, # More conservative when no signal present
]
def test_detect_change_points(self, analyzer):
"""Change points should be detected in numeric data with variance."""
# Create data with clear change point
items = []
for i in range(30):
if i < 15:
value = 100.0 + (i * 0.1) # Low values
else:
value = 200.0 + ((i - 15) * 0.1) # High values after change
items.append({"timestamp": f"2025-01-{(i % 28) + 1:02d}", "metric": value})
result = analyzer.analyze_array(items)
# Should detect change point around index 15
metric_stats = result.field_stats.get("metric")
assert metric_stats is not None
assert metric_stats.change_points is not None
# Change points should be near the transition
if metric_stats.change_points:
assert any(10 <= cp <= 20 for cp in metric_stats.change_points)
def test_constant_field_detection(self, analyzer):
"""Constant fields should be identified."""
items = generate_generic_data(20, constant_field=True)
result = analyzer.analyze_array(items)
# type field should be constant ("product")
type_stats = result.field_stats.get("type")
assert type_stats is not None
assert type_stats.is_constant
assert type_stats.constant_value == "product"
# Constant fields should be in constant_fields dict
assert "type" in result.constant_fields
assert result.constant_fields["type"] == "product"
# =============================================================================
# TestSmartCrusher
# =============================================================================
class TestSmartCrusher:
"""Tests for SmartCrusher transform."""
def test_should_apply_below_threshold(self, tokenizer):
"""Should not apply when tokens below min_tokens_to_crush."""
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": '{"status": "ok"}'},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=1000, # High threshold
)
crusher = SmartCrusher(config)
assert not crusher.should_apply(messages, tokenizer)
def test_should_apply_no_arrays(self, tokenizer):
"""Should not apply when no crushable arrays present."""
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": '{"status": "ok", "value": 123}'},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
)
crusher = SmartCrusher(config)
assert not crusher.should_apply(messages, tokenizer)
def test_should_apply_small_array(self, tokenizer):
"""Should not apply when array is below min_items_to_analyze."""
small_array = [{"id": i} for i in range(3)]
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(small_array)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=10, # Array too small
)
crusher = SmartCrusher(config)
assert not crusher.should_apply(messages, tokenizer)
def test_crush_time_series_keeps_change_points(self, tokenizer, default_config):
"""Time series crushing should preserve items around change points."""
# Create data with clear change point AND an anomaly signal
items = []
for i in range(30):
if i < 15:
value = 100.0
else:
value = 200.0 # Jump at index 15
# Add anomaly to provide importance signal for crushing
if i == 25:
value = 999.0 # Extreme anomaly
items.append(
{
"timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z",
"value": value,
}
)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=15,
preserve_change_points=True,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
# Remove digest marker
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Should have fewer items than original
assert len(crushed) < len(items)
# Should have items around change point (index ~15)
values = [item["value"] for item in crushed]
# Should have both low and high values (around change point)
assert 100.0 in values
assert 200.0 in values
def test_crush_keeps_first_k_items(self, tokenizer):
"""Crushing should always keep first K items (Safe V1 Recipe)."""
items = generate_generic_data(30)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# First 3 items should be preserved
crushed_ids = [item["id"] for item in crushed]
assert 0 in crushed_ids # First item
assert 1 in crushed_ids # Second item
assert 2 in crushed_ids # Third item
def test_crush_keeps_last_k_items(self, tokenizer):
"""Crushing should always keep last K items (Safe V1 Recipe)."""
items = generate_generic_data(30)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Last 2 items should be preserved
crushed_ids = [item["id"] for item in crushed]
assert 28 in crushed_ids # Second to last
assert 29 in crushed_ids # Last item
def test_crush_keeps_error_items(self, tokenizer):
"""Crushing should always preserve error items (Safe V1 Recipe)."""
items = generate_generic_data(30)
# Add error items in the middle
items[10] = {"id": 10, "status": "error", "message": "Connection failed"}
items[20] = {"id": 20, "status": "failed", "exception": "TimeoutError"}
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Error items should be preserved
crushed_ids = [item["id"] for item in crushed]
assert 10 in crushed_ids # Error item
assert 20 in crushed_ids # Failed item
def test_crush_keeps_anomalies(self, tokenizer):
"""Crushing should preserve anomalous numeric items (> 2 std from mean)."""
items = []
for i in range(30):
value = 100.0 + (i * 0.1) # Normal range ~100-103
items.append({"id": i, "metric": value})
# Add anomaly in the middle
items[15]["metric"] = 500.0 # Way above mean
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
variance_threshold=2.0,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Anomaly should be preserved
crushed_ids = [item["id"] for item in crushed]
assert 15 in crushed_ids # Anomaly
def test_crush_top_n_by_score(self, tokenizer):
"""Search results should be crushed keeping top N by score."""
items = generate_search_results(30)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Should have reduced items
assert len(crushed) < len(items)
# Top scored items should be present
crushed_ids = [item["id"] for item in crushed]
assert "doc_0" in crushed_ids # Highest score
assert "doc_1" in crushed_ids # Second highest
def test_schema_preserved(self, tokenizer):
"""Output items should have same schema as input items."""
items = [
{
"id": i,
"name": f"Item {i}",
"nested": {"key": f"value_{i}"},
"tags": ["a", "b"],
}
for i in range(20)
]
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Each item should have original schema
original_keys = set(items[0].keys())
for item in crushed:
assert set(item.keys()) == original_keys
assert "nested" in item
assert "key" in item["nested"]
assert "tags" in item
assert isinstance(item["tags"], list)
def test_respects_max_items_after_crush(self, tokenizer):
"""Output should respect max_items_after_crush limit."""
# Create data with importance signals (errors, anomalies) so crushing happens
items = []
for i in range(100):
item = {
"id": i,
"name": f"Item {i}",
"type": f"type_{i % 3}",
"value": 100.0,
}
# Add some errors to provide importance signal
if i in [10, 30, 50, 70, 90]:
item["error"] = f"Error at {i}"
# Add some anomalies
if i in [15, 45, 75]:
item["value"] = 99999.0
items.append(item)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=15,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# With errors and anomalies, crushing should happen
# But critical items override max, so we may have more than 15
# The test verifies that crushing happened (fewer than original)
assert len(crushed) < 100, "Should compress the data"
# Errors must be preserved
error_count = sum(1 for x in crushed if x.get("error"))
assert error_count == 5, "All errors must be preserved"
# =============================================================================
# TestRelevanceScoring
# =============================================================================
class TestRelevanceScoring:
"""Tests for relevance scoring in SmartCrusher."""
def test_context_extraction_from_user_messages(self, tokenizer):
"""Context should be extracted from user messages."""
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Find user Alice in the database"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "search_users", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "[]"},
]
config = SmartCrusherConfig(enabled=True, min_tokens_to_crush=0)
crusher = SmartCrusher(config)
context = crusher._extract_context_from_messages(messages)
assert "Alice" in context
assert "database" in context
def test_context_extraction_from_tool_calls(self, tokenizer):
"""Context should be extracted from tool call arguments."""
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Search for it"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "search_users",
"arguments": '{"query": "user_id=12345"}',
},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "[]"},
]
config = SmartCrusherConfig(enabled=True, min_tokens_to_crush=0)
crusher = SmartCrusher(config)
context = crusher._extract_context_from_messages(messages)
assert "12345" in context
def test_relevance_keeps_matching_items(self, tokenizer):
"""Items matching user query should be preserved."""
# Create items where one matches the user query
items = [{"id": i, "name": f"User {i}", "email": f"user{i}@example.com"} for i in range(30)]
# Add special user Alice
items[15] = {"id": 15, "name": "Alice", "email": "alice@example.com"}
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Find user Alice"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "list_users", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
# Use BM25 scorer for deterministic testing
relevance_config = RelevanceScorerConfig(
tier="bm25",
relevance_threshold=0.1, # Lower threshold to catch matches
)
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config, relevance_config=relevance_config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[-1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Alice should be preserved due to relevance matching
crushed_names = [item["name"] for item in crushed]
assert "Alice" in crushed_names
def test_custom_scorer_injection(self, tokenizer):
"""Custom scorer should be used when provided."""
class MockScorer(RelevanceScorer):
"""Mock scorer that always returns high score for items with id=5."""
def score(self, item: str, context: str) -> RelevanceScore:
if '"id": 5' in item or '"id":5' in item:
return RelevanceScore(score=0.9, reason="mock high score")
return RelevanceScore(score=0.0, reason="mock low score")
def score_batch(self, items: list[str], context: str) -> list[RelevanceScore]:
return [self.score(item, context) for item in items]
items = generate_generic_data(30)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Find the special item"},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config, scorer=MockScorer())
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[-1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Item with id=5 should be preserved due to mock scorer
crushed_ids = [item["id"] for item in crushed]
assert 5 in crushed_ids
# =============================================================================
# TestEdgeCases
# =============================================================================
class TestEdgeCases:
"""Edge case tests for SmartCrusher."""
def test_malformed_json_passthrough(self, tokenizer):
"""Malformed JSON should pass through unchanged."""
malformed = "{ this is not valid JSON ["
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": malformed},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Content should be unchanged
assert result.messages[1]["content"] == malformed
def test_nested_arrays(self, tokenizer):
"""Nested arrays should be handled correctly."""
# Use with_signals=True to enable crushing with new statistical detection
nested_data = {
"results": generate_generic_data(20, with_signals=True),
"metadata": {
"inner_array": [{"x": i, "value": 100.0 if i != 7 else 99999.0} for i in range(15)],
},
}
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(nested_data)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Should be modified
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Results array should be crushed (with signals, crushing can happen)
assert len(crushed["results"]) < 20, "Results should be crushed"
# Nested array should also be crushed if large enough
assert "metadata" in crushed
assert "inner_array" in crushed["metadata"]
def test_anthropic_style_tool_results(self, tokenizer):
"""Anthropic-style tool_result blocks should be handled."""
# Use with_signals=True to enable crushing with new statistical detection
items = generate_generic_data(20, with_signals=True)
messages = [
{"role": "system", "content": "You are helpful."},
{
"role": "user",
"content": [
{"type": "text", "text": "What are the results?"},
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": json.dumps(items),
},
],
},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
# Check should_apply works
assert crusher.should_apply(messages, tokenizer)
result = crusher.apply(messages, tokenizer)
# Tool result content should be crushed (with signals present)
tool_result_block = result.messages[1]["content"][1]
content = tool_result_block["content"]
json_part = content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
assert len(crushed) < len(items), "With signals, crushing should happen"
def test_openai_style_tool_results(self, tokenizer):
"""OpenAI-style tool messages should be handled."""
# Use with_signals=True to enable crushing with new statistical detection
items = generate_generic_data(20, with_signals=True)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Search for items"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {"name": "search_items", "arguments": "{}"},
}
],
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": json.dumps(items),
},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
# Check should_apply works
assert crusher.should_apply(messages, tokenizer)
result = crusher.apply(messages, tokenizer)
# Tool content should be crushed
tool_content = result.messages[3]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
assert len(crushed) < len(items)
assert len(crushed) <= 10
def test_empty_tool_content(self, tokenizer):
"""Empty tool content should be handled gracefully."""
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": ""},
]
config = SmartCrusherConfig(enabled=True, min_tokens_to_crush=0)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Should not crash, content unchanged
assert result.messages[1]["content"] == ""
def test_non_dict_array_items(self, tokenizer):
"""Arrays of non-dict items should be handled gracefully."""
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
)
crusher = SmartCrusher(config)
# Should not crash, but won't crush non-dict arrays
result = crusher.apply(messages, tokenizer)
# Primitive arrays are not crushed by SmartCrusher (it requires dict items)
# Content should pass through
assert result.messages is not None
def test_mixed_null_values(self, tokenizer):
"""Items with null values should be handled without crashing.
With statistical detection, this data has unique IDs and no importance
signals, so it will be SKIPPED (not crushed) - which is correct behavior.
The test verifies that null values don't crash the analyzer.
"""
items = [{"id": i, "value": None if i % 2 == 0 else i * 10} for i in range(20)]
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Should not crash - parsing should succeed
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# With statistical detection, unique entities with no signals are SKIPPED
# This is correct conservative behavior - all 20 items preserved
assert len(crushed) == 20
def test_unicode_content(self, tokenizer):
"""Unicode content should be preserved."""
items = [
{"id": i, "name": f"Item {i} - \u4e2d\u6587 \u65e5\u672c\u8a9e \ud83d\ude80"}
for i in range(20)
]
messages = [
{"role": "system", "content": "You are helpful."},
{
"role": "tool",
"tool_call_id": "call_1",
"content": json.dumps(items, ensure_ascii=False),
},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
tool_content = result.messages[1]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Unicode should be preserved
for item in crushed:
assert "\u4e2d\u6587" in item["name"]
assert "\u65e5\u672c\u8a9e" in item["name"]
def test_digest_marker_added(self, tokenizer):
"""Digest marker should be added to crushed content."""
items = generate_generic_data(30)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
tool_content = result.messages[1]["content"]
# Should have digest marker
assert "<headroom:tool_digest" in tool_content
assert "sha256=" in tool_content
assert len(result.markers_inserted) > 0
def test_transforms_applied_tracking(self, tokenizer):
"""Transforms applied should be tracked."""
items = generate_generic_data(30)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Should track transforms
assert len(result.transforms_applied) > 0
assert any("smart" in t.lower() for t in result.transforms_applied)
def test_token_reduction(self, tokenizer):
"""Token count should be reduced after crushing."""
items = generate_generic_data(100)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Tokens should be reduced
assert result.tokens_after < result.tokens_before
# =============================================================================
# Integration Tests
# =============================================================================
class TestSmartCrusherIntegration:
"""Integration tests for SmartCrusher with realistic scenarios."""
def test_database_query_results(self, tokenizer):
"""Simulate crushing database query results."""
# Simulate a database query returning many rows
items = [
{
"user_id": f"usr_{i:05d}",
"email": f"user{i}@example.com",
"created_at": f"2025-01-{(i % 28) + 1:02d}T00:00:00Z",
"status": "active" if i % 10 != 0 else "inactive",
"login_count": i * 5,
}
for i in range(100)
]
messages = [
{"role": "system", "content": "You are a database assistant."},
{"role": "user", "content": "Show me users with email containing 'user50'"},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "query_users", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
relevance_config = RelevanceScorerConfig(tier="bm25", relevance_threshold=0.1)
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=15,
)
crusher = SmartCrusher(config, relevance_config=relevance_config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[3]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Should be significantly reduced
assert len(crushed) <= 15
assert len(crushed) < len(items)
# User 50 should be preserved due to relevance
user_ids = [item["user_id"] for item in crushed]
assert "usr_00050" in user_ids
def test_api_search_results(self, tokenizer):
"""Simulate crushing API search results."""
items = [
{
"id": f"result_{i}",
"title": f"Result {i}: {'Important Finding' if i < 5 else 'Regular Result'}",
"relevance_score": 0.95 - (i * 0.02),
"snippet": f"This is the snippet for result {i}...",
"url": f"https://example.com/doc/{i}",
}
for i in range(50)
]
messages = [
{"role": "system", "content": "You are a search assistant."},
{"role": "user", "content": "Search for important findings"},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=10,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[2]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Should use TOP_N strategy
assert len(crushed) <= 12 # max + some buffer for first/last
# Top results should be preserved
ids = [item["id"] for item in crushed]
assert "result_0" in ids
assert "result_1" in ids
def test_monitoring_metrics(self, tokenizer):
"""Simulate crushing monitoring/metrics data."""
items = []
for i in range(60):
# Normal CPU usage around 50%
cpu = 50.0 + (i * 0.1)
# Spike at index 30
if i == 30:
cpu = 95.0
items.append(
{
"timestamp": f"2025-01-06T{10 + (i // 60):02d}:{i % 60:02d}:00Z",
"cpu_percent": cpu,
"memory_percent": 60.0,
"host": "server-01",
}
)
messages = [
{"role": "system", "content": "You are a monitoring assistant."},
{"role": "user", "content": "Show me CPU metrics"},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items)},
]
config = SmartCrusherConfig(
enabled=True,
min_tokens_to_crush=0,
min_items_to_analyze=3,
max_items_after_crush=15,
preserve_change_points=True,
)
crusher = SmartCrusher(config)
result = crusher.apply(messages, tokenizer)
# Parse result
tool_content = result.messages[2]["content"]
json_part = tool_content.split("\n<headroom:")[0]
crushed = json.loads(json_part)
# Should preserve the spike
cpu_values = [item["cpu_percent"] for item in crushed]
assert 95.0 in cpu_values # Spike should be preserved