headroom/tests/test_transforms/test_tool_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

154 lines
5.2 KiB
Python

"""Tests for tool crusher transform."""
import json
from headroom import OpenAIProvider, Tokenizer, ToolCrusherConfig
from headroom.transforms import ToolCrusher
# 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)
class TestToolCrusher:
"""Tests for ToolCrusher transform."""
def test_small_tool_output_unchanged(self):
"""Small tool outputs should not be modified."""
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": '{"status": "ok"}'},
]
crusher = ToolCrusher()
tokenizer = get_tokenizer()
result = crusher.apply(messages, tokenizer)
# Should not be modified (too small)
assert result.messages[1]["content"] == '{"status": "ok"}'
assert len(result.transforms_applied) == 0
def test_large_json_array_truncated(self):
"""Large arrays should be truncated."""
large_array = [{"id": i, "name": f"Item {i}"} for i in range(50)]
large_json = json.dumps({"results": large_array})
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": large_json},
]
config = ToolCrusherConfig(min_tokens_to_crush=50, max_array_items=5)
crusher = ToolCrusher(config)
tokenizer = get_tokenizer()
result = crusher.apply(messages, tokenizer)
# Should be modified
tool_content = result.messages[1]["content"]
parsed = json.loads(tool_content.split("\n<headroom:")[0])
# Array should be truncated
assert len(parsed["results"]) <= 6 # 5 items + truncation marker
def test_long_strings_truncated(self):
"""Long strings should be truncated."""
long_string = "x" * 2000
data = {"content": long_string}
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(data)},
]
config = ToolCrusherConfig(min_tokens_to_crush=50, max_string_length=100)
crusher = ToolCrusher(config)
tokenizer = get_tokenizer()
result = crusher.apply(messages, tokenizer)
tool_content = result.messages[1]["content"]
parsed = json.loads(tool_content.split("\n<headroom:")[0])
# String should be truncated
assert len(parsed["content"]) < 200
assert "truncated" in parsed["content"]
def test_nested_depth_limited(self):
"""Deeply nested structures should be limited."""
# Create deeply nested structure
nested = {"level": 0}
current = nested
for i in range(10):
current["nested"] = {"level": i + 1}
current = current["nested"]
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(nested)},
]
config = ToolCrusherConfig(min_tokens_to_crush=10, max_depth=3)
crusher = ToolCrusher(config)
tokenizer = get_tokenizer()
result = crusher.apply(messages, tokenizer)
tool_content = result.messages[1]["content"]
parsed = json.loads(tool_content.split("\n<headroom:")[0])
# Deep nesting should be summarized
# Navigate to depth limit
current = parsed
depth = 0
while "nested" in current and isinstance(current["nested"], dict):
current = current["nested"]
depth += 1
if depth > 5:
break
assert depth <= 4 # Should be limited
def test_digest_marker_added(self):
"""Digest marker should be added to crushed content."""
large_data = {"items": list(range(100))}
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(large_data)},
]
config = ToolCrusherConfig(min_tokens_to_crush=10, max_array_items=5)
crusher = ToolCrusher(config)
tokenizer = get_tokenizer()
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
def test_non_tool_messages_unchanged(self):
"""Non-tool messages should not be modified."""
messages = [
{"role": "system", "content": json.dumps({"large": "data" * 1000})},
{"role": "user", "content": json.dumps({"user": "data" * 1000})},
{"role": "assistant", "content": json.dumps({"assistant": "data" * 1000})},
]
crusher = ToolCrusher()
tokenizer = get_tokenizer()
result = crusher.apply(messages, tokenizer)
# All messages should be unchanged
for i, msg in enumerate(result.messages):
assert msg["content"] == messages[i]["content"]