headroom/tests/test_config.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

489 lines
17 KiB
Python
Raw Normal View History

"""Tests for the config module.
Tests all configuration dataclasses, enums, and utility classes:
- HeadroomMode enum
fix: B1 — retire ICM, RollingWindow, scoring, relevance + dependents Phase B step 1 of the live-zone-only realignment. Removes ~10K LOC of "drop messages from history" machinery that became unreachable after PR-A1 made `/v1/messages` a passthrough on the proxy. Live-zone-only compression (PR-B2..B7) operates on content blocks within messages; message-list mutation no longer happens in the pipeline. Python deletes: - headroom/transforms/intelligent_context.py (1077 LOC) - headroom/transforms/rolling_window.py (395 LOC) - headroom/transforms/progressive_summarizer.py (508 LOC) - headroom/transforms/scoring.py (459 LOC) - headroom/transforms/tool_crusher.py (338 LOC) - 5 corresponding tests/test_transforms/* and tests/test_proxy_intelligent_context.py Rust deletes: - crates/headroom-core/src/context/* (manager, config, workspace, candidate, ccr_drop, strategy/, mod) + safety.rs replaced - crates/headroom-core/src/scoring/* (mod, score, scorer, traits, weights) - MessageScorerComparator from crates/headroom-parity (PR #338/#343 becomes deletable; sunk cost stays sunk) - 13 message_scorer fixtures + record_message_scorer.py Rust adds (move + rewrite): - crates/headroom-core/src/transforms/safety.rs — `tool_pair_indices` preserves the OpenAI/Anthropic tool_use ↔ tool_result pairing rule the live-zone dispatcher (PR-B2) needs. No IcmConfig dependency. Surface refactors: - HeadroomConfig: drop `tool_crusher`, `rolling_window`, `intelligent_context` fields; hoist `output_buffer_tokens` to top level (used by client.py). - ProxyConfig: drop `intelligent_context*` fields. - `headroom wrap` proxy server: retire IntelligentContextManager and RollingWindow imports + branch; pipeline is CacheAligner → ContentRouter (smart_routing) or CacheAligner → SmartCrusher (legacy). - CLI: drop `--no-intelligent-context`, `--no-intelligent-scoring`, `--no-compress-first` flags. - LangChain memory integration: rename `_apply_rolling_window` → `_apply_compression`, drop RollingWindowConfig dep. Threshold is now advisory — B6 will rework the contract. - TransformPipeline.create_pipeline now takes only cache_aligner_config. - headroom/__init__.py + headroom/transforms/__init__.py: strip exports of deleted symbols. Bug fixes uncovered by full pytest sweep: - providers/copilot/wrap.py: `environ or os.environ` collapsed empty-dict to falsy → callers passing `environ={}` accidentally pulled from os.environ. Use `environ if environ is not None else os.environ`. Test correctness fixes: - _DummyAnthropicHandler._retry_request gains **_kwargs to match the real handler signature post-A8. - test_ws_http_fallback extracts JSON from `content=` (post-A3 byte-faithful) rather than the obsolete `json=` kwarg. - test_ccr_response_handler_extra fixture joins SSE events with `\n\n` per spec (post-A8 byte-buffer parser requirement). - test_proxy_responses_phase_preservation: capture via direct handler attached to the named logger, so the assertion is order-independent (proxy `_setup_file_logging` flips `headroom.propagate=False` once any earlier test triggers it). - conftest.py autouse fixture resets `headroom.propagate=True` before each test as a defensive measure for the same pollution. - test_wrap_copilot_translated_backend_still_requires_byok: monkeypatch.delenv every provider key so the BYOK error actually fires. - test_native_installers: skip when system bash < 4.3 (macOS ships 3.2). - TestGeminiEmbedContent / TestGeminiBatchEmbedContents: pytest.mark.skip — proxy currently has no :embedContent route; feature gap, not regression. Acceptance: - cargo build --workspace + cargo clippy + cargo fmt --check: green. - cargo test --workspace --exclude headroom-py: 777 passed. - pytest: 4892 passed, 240 skipped, 0 failed. - git grep returns only intentional comments referencing the deletion. Per-PR-B1 plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 12:23:17 -07:00
- CacheAlignerConfig
- RelevanceScorerConfig, SmartCrusherConfig
- HeadroomConfig (main config)
- Block, WasteSignals, CachePrefixMetrics
- TransformResult, RequestMetrics
"""
from dataclasses import fields
from datetime import datetime
from headroom.config import (
Block,
CacheAlignerConfig,
CachePrefixMetrics,
HeadroomConfig,
HeadroomMode,
RelevanceScorerConfig,
RequestMetrics,
SmartCrusherConfig,
TransformResult,
WasteSignals,
)
class TestHeadroomMode:
"""Tests for HeadroomMode enum."""
def test_enum_values(self):
"""All expected enum values exist with correct string values."""
assert HeadroomMode.AUDIT.value == "audit"
assert HeadroomMode.OPTIMIZE.value == "optimize"
assert HeadroomMode.SIMULATE.value == "simulate"
def test_string_conversion(self):
"""HeadroomMode inherits from str for string compatibility."""
# Enum value access works as string
assert HeadroomMode.AUDIT.value == "audit"
assert HeadroomMode.OPTIMIZE.value == "optimize"
assert HeadroomMode.SIMULATE.value == "simulate"
# Can compare directly with strings since it inherits from str
assert HeadroomMode.AUDIT == "audit"
assert HeadroomMode.OPTIMIZE == "optimize"
assert HeadroomMode.SIMULATE == "simulate"
# isinstance check confirms str inheritance
assert isinstance(HeadroomMode.AUDIT, str)
class TestCacheAlignerConfig:
"""Tests for CacheAlignerConfig dataclass."""
def test_default_values(self):
"""Default values are correctly set."""
config = CacheAlignerConfig()
assert config.enabled is False
assert config.normalize_whitespace is True
assert config.collapse_blank_lines is True
def test_date_patterns_default(self):
"""Default date_patterns contains expected regex patterns."""
config = CacheAlignerConfig()
assert isinstance(config.date_patterns, list)
assert len(config.date_patterns) == 4
# Verify specific patterns exist
assert r"Current [Dd]ate:?\s*\d{4}-\d{2}-\d{2}" in config.date_patterns
assert r"Today is \w+,?\s+\w+ \d+" in config.date_patterns
assert r"Today's date:?\s*\d{4}-\d{2}-\d{2}" in config.date_patterns
assert r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}" in config.date_patterns
def test_dynamic_tail_separator_default(self):
"""Default dynamic_tail_separator has expected value."""
config = CacheAlignerConfig()
assert config.dynamic_tail_separator == "\n\n---\n[Dynamic Context]\n"
def test_date_patterns_isolation(self):
"""Each instance gets its own date_patterns list."""
config1 = CacheAlignerConfig()
config2 = CacheAlignerConfig()
config1.date_patterns.append(r"custom pattern")
assert r"custom pattern" not in config2.date_patterns
class TestRelevanceScorerConfig:
"""Tests for RelevanceScorerConfig dataclass."""
def test_default_tier_hybrid(self):
"""Default tier is hybrid."""
config = RelevanceScorerConfig()
assert config.tier == "hybrid"
def test_bm25_params(self):
"""BM25 parameters have expected defaults."""
config = RelevanceScorerConfig()
assert config.bm25_k1 == 1.5
assert config.bm25_b == 0.75
def test_embedding_params(self):
"""Embedding parameters have expected defaults."""
config = RelevanceScorerConfig()
assert config.embedding_model == "all-MiniLM-L6-v2"
assert config.hybrid_alpha == 0.5
assert config.adaptive_alpha is True
def test_relevance_threshold_default(self):
"""Relevance threshold defaults to 0.25."""
config = RelevanceScorerConfig()
assert config.relevance_threshold == 0.25
class TestSmartCrusherConfig:
"""Tests for SmartCrusherConfig dataclass."""
def test_default_values(self):
"""Default values are correctly set."""
config = SmartCrusherConfig()
assert config.min_items_to_analyze == 5
assert config.min_tokens_to_crush == 200
assert config.variance_threshold == 2.0
assert config.uniqueness_threshold == 0.1
assert config.similarity_threshold == 0.8
assert config.max_items_after_crush == 15
assert config.preserve_change_points is True
assert config.factor_out_constants is False
assert config.include_summaries is False
def test_enabled_by_default(self):
"""SmartCrusher is enabled by default."""
config = SmartCrusherConfig()
assert config.enabled is True
def test_relevance_field_default(self):
"""Relevance field defaults to RelevanceScorerConfig instance."""
config = SmartCrusherConfig()
assert isinstance(config.relevance, RelevanceScorerConfig)
assert config.relevance.tier == "hybrid"
def test_relevance_isolation(self):
"""Each instance gets its own RelevanceScorerConfig."""
config1 = SmartCrusherConfig()
config2 = SmartCrusherConfig()
config1.relevance.tier = "bm25"
assert config2.relevance.tier == "hybrid"
class TestHeadroomConfig:
"""Tests for HeadroomConfig main configuration class."""
def test_default_values(self):
"""Default values are correctly set."""
config = HeadroomConfig()
assert config.store_url == "sqlite:///headroom.db"
assert config.default_mode == HeadroomMode.AUDIT
assert config.generate_diff_artifact is False
# Nested configs exist
assert isinstance(config.smart_crusher, SmartCrusherConfig)
assert isinstance(config.cache_aligner, CacheAlignerConfig)
def test_get_context_limit_direct_match(self):
"""get_context_limit returns limit for exact model match."""
config = HeadroomConfig(model_context_limits={"gpt-4o": 128000, "claude-3-opus": 200000})
assert config.get_context_limit("gpt-4o") == 128000
assert config.get_context_limit("claude-3-opus") == 200000
def test_get_context_limit_prefix_match(self):
"""get_context_limit returns limit for prefix match."""
config = HeadroomConfig(model_context_limits={"gpt-4": 128000, "claude-3": 200000})
# Prefix matches
assert config.get_context_limit("gpt-4-turbo") == 128000
assert config.get_context_limit("gpt-4o") == 128000
assert config.get_context_limit("claude-3-opus") == 200000
assert config.get_context_limit("claude-3-sonnet") == 200000
def test_get_context_limit_not_found(self):
"""get_context_limit returns None for unknown model."""
config = HeadroomConfig(model_context_limits={"gpt-4": 128000})
assert config.get_context_limit("unknown-model") is None
assert config.get_context_limit("llama-2") is None
def test_model_context_limits_isolation(self):
"""Each instance gets its own model_context_limits dict."""
config1 = HeadroomConfig()
config2 = HeadroomConfig()
config1.model_context_limits["custom-model"] = 50000
assert "custom-model" not in config2.model_context_limits
class TestBlock:
"""Tests for Block dataclass."""
def test_block_creation(self):
"""Block can be created with required fields."""
block = Block(
kind="user",
text="Hello, world!",
tokens_est=5,
content_hash="abc123",
source_index=0,
)
assert block.kind == "user"
assert block.text == "Hello, world!"
assert block.tokens_est == 5
assert block.content_hash == "abc123"
assert block.source_index == 0
assert block.flags == {}
def test_block_kinds(self):
"""Block accepts all valid kind values."""
valid_kinds = ["system", "user", "assistant", "tool_call", "tool_result", "rag", "unknown"]
for kind in valid_kinds:
block = Block(
kind=kind,
text="test",
tokens_est=1,
content_hash="hash",
source_index=0,
)
assert block.kind == kind
def test_block_flags_default_factory(self):
"""Each block gets its own flags dict."""
block1 = Block(kind="user", text="a", tokens_est=1, content_hash="h1", source_index=0)
block2 = Block(kind="user", text="b", tokens_est=1, content_hash="h2", source_index=1)
block1.flags["custom"] = True
assert "custom" not in block2.flags
class TestWasteSignals:
"""Tests for WasteSignals dataclass."""
def test_total_calculation(self):
"""total() correctly sums all waste token fields."""
signals = WasteSignals(
json_bloat_tokens=100,
html_noise_tokens=50,
base64_tokens=200,
whitespace_tokens=25,
dynamic_date_tokens=10,
repetition_tokens=15,
)
assert signals.total() == 400
def test_total_with_defaults(self):
"""total() returns 0 when all fields are default."""
signals = WasteSignals()
assert signals.total() == 0
def test_to_dict(self):
"""to_dict() returns correct dictionary representation."""
signals = WasteSignals(
json_bloat_tokens=100,
html_noise_tokens=50,
base64_tokens=200,
whitespace_tokens=25,
dynamic_date_tokens=10,
repetition_tokens=15,
feat: detect re-served tool results as over-compression waste signal (#854) Closes #853 ## What Adds a `reread` waste signal: identical `tool_result` content appearing at more than one message position means the agent re-fetched something already in context — the dominant failure signature of over-compression (Manus context-engineering; JetBrains "Complexity Trap", arXiv:2508.21433). Per-request savings can't see this cost; this signal makes it visible. - `WasteSignals.reread_tokens` — new field, in `total()`, exported as `"reread"` in `to_dict()`. - `parse_messages()` groups `tool_result` blocks by their **existing** `content_hash` and counts every repeat beyond the first serve. No new hashing or tokenization; one O(blocks) dict pass. - `REREAD_MIN_TOKENS = 50` guard: short outputs ("ok", empty diffs) legitimately repeat and are skipped. Duplicates within a single message (same `source_index`) are not counted. - Works across all formats the parser already normalizes to `tool_result` blocks: OpenAI `role=tool`, Anthropic `tool_result`, Strands/Bedrock `toolResult` (#813/#815). - Flows through existing generic plumbing with zero handler changes: pipeline → `RequestOutcome.waste_signals` → Prometheus `headroom_waste_signal_tokens_total{signal="reread"}` → dashboard "Waste Detected" panel. Dashboard gains label/color entries for the new key. ## Tests 7 new tests in `tests/test_parser.py::TestRereadDetection` (red before, green after): OpenAI + Anthropic format detection, repeat-counting semantics (first serve free), single-occurrence, short-duplicate guard, same-message guard, `total()`/`to_dict()` participation. Updated 2 exact-shape assertions in `tests/test_config.py`. Local runs: `tests/test_parser.py` (72 passed), `tests/test_config.py` + outcome/reporting/observability/storage/proxy-hooks suites (190 passed), `tests/test_canonical_pipeline.py` + `tests/test_proxy_pipeline_lifecycle.py` (11 passed). `ruff check` + `ruff format --check` clean. ## Real behavior proof **Setup:** macOS (Darwin 25.5), Python 3.11.9, this branch, real proxy server (`python -m headroom.proxy.server --port 18970 --anthropic-api-url http://127.0.0.1:18971`) with a local mock Anthropic upstream returning a canned `/v1/messages` response (no real key needed). **Steps:** POSTed an Anthropic-format conversation to the live proxy: agent fetches a 14 KB JSON log array via `get_logs` tool, then fetches the identical content again under a different `tool_use_id` (the re-read). **Observed result** — `curl http://127.0.0.1:18970/metrics` after the request: ``` # HELP headroom_waste_signal_tokens_total Tokens attributed to detected waste signals # TYPE headroom_waste_signal_tokens_total counter headroom_waste_signal_tokens_total{signal="json_bloat"} 9858 headroom_waste_signal_tokens_total{signal="reread"} 4935 ``` `reread` = 4935 tokens, exactly the second serve of the ~4.9k-token tool result (json_bloat counts both occurrences ≈ 2×). `/stats` shows the same: `"waste_signals": {"json_bloat": 9858, "reread": 4935, ...}` — which is what the dashboard panel renders. Also verified the negative path live: a conversation whose tool results contain non-compressible plain code text produced no waste-signal entries (the pipeline only attributes waste when compression actually engaged, unchanged behavior). **Not tested:** Gemini `functionResponse` path (parser doesn't produce `tool_result` blocks for it — pre-existing gap tracked in #819); dashboard rendering only verified via the `/stats` payload the panel binds to, not a browser screenshot. ## Out of scope (per #853) Tool-call argument matching, compression-marker attribution, tokens-per-task metric, cache hit-rate panel. --------- Co-authored-by: Ash Rhodes <ashley.rhodes@king.com>
2026-06-11 20:07:04 +02:00
reread_tokens=30,
)
expected = {
"json_bloat": 100,
"html_noise": 50,
"base64": 200,
"whitespace": 25,
"dynamic_date": 10,
"repetition": 15,
feat: detect re-served tool results as over-compression waste signal (#854) Closes #853 ## What Adds a `reread` waste signal: identical `tool_result` content appearing at more than one message position means the agent re-fetched something already in context — the dominant failure signature of over-compression (Manus context-engineering; JetBrains "Complexity Trap", arXiv:2508.21433). Per-request savings can't see this cost; this signal makes it visible. - `WasteSignals.reread_tokens` — new field, in `total()`, exported as `"reread"` in `to_dict()`. - `parse_messages()` groups `tool_result` blocks by their **existing** `content_hash` and counts every repeat beyond the first serve. No new hashing or tokenization; one O(blocks) dict pass. - `REREAD_MIN_TOKENS = 50` guard: short outputs ("ok", empty diffs) legitimately repeat and are skipped. Duplicates within a single message (same `source_index`) are not counted. - Works across all formats the parser already normalizes to `tool_result` blocks: OpenAI `role=tool`, Anthropic `tool_result`, Strands/Bedrock `toolResult` (#813/#815). - Flows through existing generic plumbing with zero handler changes: pipeline → `RequestOutcome.waste_signals` → Prometheus `headroom_waste_signal_tokens_total{signal="reread"}` → dashboard "Waste Detected" panel. Dashboard gains label/color entries for the new key. ## Tests 7 new tests in `tests/test_parser.py::TestRereadDetection` (red before, green after): OpenAI + Anthropic format detection, repeat-counting semantics (first serve free), single-occurrence, short-duplicate guard, same-message guard, `total()`/`to_dict()` participation. Updated 2 exact-shape assertions in `tests/test_config.py`. Local runs: `tests/test_parser.py` (72 passed), `tests/test_config.py` + outcome/reporting/observability/storage/proxy-hooks suites (190 passed), `tests/test_canonical_pipeline.py` + `tests/test_proxy_pipeline_lifecycle.py` (11 passed). `ruff check` + `ruff format --check` clean. ## Real behavior proof **Setup:** macOS (Darwin 25.5), Python 3.11.9, this branch, real proxy server (`python -m headroom.proxy.server --port 18970 --anthropic-api-url http://127.0.0.1:18971`) with a local mock Anthropic upstream returning a canned `/v1/messages` response (no real key needed). **Steps:** POSTed an Anthropic-format conversation to the live proxy: agent fetches a 14 KB JSON log array via `get_logs` tool, then fetches the identical content again under a different `tool_use_id` (the re-read). **Observed result** — `curl http://127.0.0.1:18970/metrics` after the request: ``` # HELP headroom_waste_signal_tokens_total Tokens attributed to detected waste signals # TYPE headroom_waste_signal_tokens_total counter headroom_waste_signal_tokens_total{signal="json_bloat"} 9858 headroom_waste_signal_tokens_total{signal="reread"} 4935 ``` `reread` = 4935 tokens, exactly the second serve of the ~4.9k-token tool result (json_bloat counts both occurrences ≈ 2×). `/stats` shows the same: `"waste_signals": {"json_bloat": 9858, "reread": 4935, ...}` — which is what the dashboard panel renders. Also verified the negative path live: a conversation whose tool results contain non-compressible plain code text produced no waste-signal entries (the pipeline only attributes waste when compression actually engaged, unchanged behavior). **Not tested:** Gemini `functionResponse` path (parser doesn't produce `tool_result` blocks for it — pre-existing gap tracked in #819); dashboard rendering only verified via the `/stats` payload the panel binds to, not a browser screenshot. ## Out of scope (per #853) Tool-call argument matching, compression-marker attribution, tokens-per-task metric, cache hit-rate panel. --------- Co-authored-by: Ash Rhodes <ashley.rhodes@king.com>
2026-06-11 20:07:04 +02:00
"reread": 30,
feat: attribute reread waste to over-compression via marker check (#901) ## Description Fixes #899. The `reread` signal (#853/#854) counts re-served tool results but cannot answer the question that motivated it: **did Headroom cause the re-read?** A re-read after an intact first serve is agent behavior; a re-read after Headroom markerized the first serve is over-compression cost. This PR splits the signal so the actionable part is visible. Request-local, no store lookups: the client resends full history each turn and the pipeline recompresses it deterministically, so the current request already holds the evidence. `TransformPipeline.apply` passes `current_messages` into `parse_messages(compressed_messages=...)`. For each counted reread group, if the transformed copy of the **first serve** carries a CCR retrieval marker and its original text is gone, the group's counted repeats go into `reread_compressed_tokens`. Lossless reshaping (no marker) is deliberately not attributed. Closes #899. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `parser.py`: `parse_messages` gains an optional `compressed_messages` param; the content-hash reread loop accumulates per-group `counted_tokens` and attributes them to `reread_compressed_tokens` when the first serve's transformed copy carries a CCR marker (`CCR_RETRIEVAL_MARKER_RE`, kept local to avoid a transforms import cycle). - `transforms/pipeline.py`: pass `current_messages` (post-transform copy) into the existing waste-detection `parse_messages` call. - `config.py`: new `reread_compressed_tokens` WasteSignals field; `dashboard.html` + `reporting/generator.py` surface it. - Tests: `tests/test_reread_attribution.py` + WasteSignals contract update. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] New tests added for new functionality - [x] New and existing unit tests pass locally with my changes ### Test Output ```text $ pytest tests/test_reread_attribution.py tests/test_parser.py tests/test_gemini_function_response_waste.py tests/test_codex_responses_waste_signals.py -q 122 passed in 1.50s $ pytest tests/ -k "waste or pipeline or reporting or config or reread" -q 348 passed, 33 skipped, 6010 deselected # (1 unrelated env-dependent failure: test_proxy_gemini_native_integration::test_generation_config — 404, reproduces on main without these changes; needs a Gemini key locally) $ ruff check headroom/parser.py headroom/transforms/pipeline.py All checks passed! ``` ## Real Behavior Proof - Environment: local macOS, repo .venv, Python 3.11.9 - Exact command / steps: rebased onto current main to resolve conflicts with #909 (merged), then ran the reread + parser + waste suites above - Observed result: a reread whose first serve is markerized attributes to `reread_compressed_tokens`; an intact first serve and a lossless (no-marker) reshape do not. #909's re-issued-call detection (same call, different bytes) continues to count and dedup correctly alongside it — all 122 targeted tests pass. - Not tested: live proxy traffic; the one gemini-native route test above (environmental 404, not introduced here). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes **Rebased onto current main after #909 merged.** #909 added a re-issued-call reread pass *after* the original content-hash loop this PR modifies — the conflict was textual/adjacent, not a re-architecture. Resolution preserves #909's `counted_results` dedup contract and leaves its new pass unchanged; #901's attribution stays scoped to the content-hash groups it was reviewed against (attributing #909's call-key pass too would be a separate follow-up). The diff differs from the prior approval only by this reshape — worth a quick re-glance.
2026-06-13 17:43:35 +02:00
"reread_compressed": 0,
}
assert signals.to_dict() == expected
def test_to_dict_defaults(self):
"""to_dict() returns zeroes for default values."""
signals = WasteSignals()
result = signals.to_dict()
assert all(v == 0 for v in result.values())
feat: attribute reread waste to over-compression via marker check (#901) ## Description Fixes #899. The `reread` signal (#853/#854) counts re-served tool results but cannot answer the question that motivated it: **did Headroom cause the re-read?** A re-read after an intact first serve is agent behavior; a re-read after Headroom markerized the first serve is over-compression cost. This PR splits the signal so the actionable part is visible. Request-local, no store lookups: the client resends full history each turn and the pipeline recompresses it deterministically, so the current request already holds the evidence. `TransformPipeline.apply` passes `current_messages` into `parse_messages(compressed_messages=...)`. For each counted reread group, if the transformed copy of the **first serve** carries a CCR retrieval marker and its original text is gone, the group's counted repeats go into `reread_compressed_tokens`. Lossless reshaping (no marker) is deliberately not attributed. Closes #899. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `parser.py`: `parse_messages` gains an optional `compressed_messages` param; the content-hash reread loop accumulates per-group `counted_tokens` and attributes them to `reread_compressed_tokens` when the first serve's transformed copy carries a CCR marker (`CCR_RETRIEVAL_MARKER_RE`, kept local to avoid a transforms import cycle). - `transforms/pipeline.py`: pass `current_messages` (post-transform copy) into the existing waste-detection `parse_messages` call. - `config.py`: new `reread_compressed_tokens` WasteSignals field; `dashboard.html` + `reporting/generator.py` surface it. - Tests: `tests/test_reread_attribution.py` + WasteSignals contract update. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] New tests added for new functionality - [x] New and existing unit tests pass locally with my changes ### Test Output ```text $ pytest tests/test_reread_attribution.py tests/test_parser.py tests/test_gemini_function_response_waste.py tests/test_codex_responses_waste_signals.py -q 122 passed in 1.50s $ pytest tests/ -k "waste or pipeline or reporting or config or reread" -q 348 passed, 33 skipped, 6010 deselected # (1 unrelated env-dependent failure: test_proxy_gemini_native_integration::test_generation_config — 404, reproduces on main without these changes; needs a Gemini key locally) $ ruff check headroom/parser.py headroom/transforms/pipeline.py All checks passed! ``` ## Real Behavior Proof - Environment: local macOS, repo .venv, Python 3.11.9 - Exact command / steps: rebased onto current main to resolve conflicts with #909 (merged), then ran the reread + parser + waste suites above - Observed result: a reread whose first serve is markerized attributes to `reread_compressed_tokens`; an intact first serve and a lossless (no-marker) reshape do not. #909's re-issued-call detection (same call, different bytes) continues to count and dedup correctly alongside it — all 122 targeted tests pass. - Not tested: live proxy traffic; the one gemini-native route test above (environmental 404, not introduced here). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes **Rebased onto current main after #909 merged.** #909 added a re-issued-call reread pass *after* the original content-hash loop this PR modifies — the conflict was textual/adjacent, not a re-architecture. Resolution preserves #909's `counted_results` dedup contract and leaves its new pass unchanged; #901's attribution stays scoped to the content-hash groups it was reviewed against (attributing #909's call-key pass too would be a separate follow-up). The diff differs from the prior approval only by this reshape — worth a quick re-glance.
2026-06-13 17:43:35 +02:00
assert len(result) == 8
class TestCachePrefixMetrics:
"""Tests for CachePrefixMetrics dataclass."""
def test_dataclass_fields(self):
"""CachePrefixMetrics has all expected fields."""
field_names = {f.name for f in fields(CachePrefixMetrics)}
expected_fields = {
"stable_prefix_bytes",
"stable_prefix_tokens_est",
"stable_prefix_hash",
"prefix_changed",
"previous_hash",
}
assert field_names == expected_fields
def test_creation(self):
"""CachePrefixMetrics can be created with required fields."""
metrics = CachePrefixMetrics(
stable_prefix_bytes=1024,
stable_prefix_tokens_est=256,
stable_prefix_hash="abc123def456",
prefix_changed=False,
)
assert metrics.stable_prefix_bytes == 1024
assert metrics.stable_prefix_tokens_est == 256
assert metrics.stable_prefix_hash == "abc123def456"
assert metrics.prefix_changed is False
assert metrics.previous_hash is None
def test_previous_hash_optional(self):
"""previous_hash defaults to None."""
metrics = CachePrefixMetrics(
stable_prefix_bytes=512,
stable_prefix_tokens_est=128,
stable_prefix_hash="hash123",
prefix_changed=True,
previous_hash="oldhash",
)
assert metrics.previous_hash == "oldhash"
class TestTransformResult:
"""Tests for TransformResult dataclass."""
def test_dataclass_fields(self):
"""TransformResult has all expected fields."""
field_names = {f.name for f in fields(TransformResult)}
expected_fields = {
"messages",
"tokens_before",
"tokens_after",
"transforms_applied",
"markers_inserted",
"warnings",
"diff_artifact",
"cache_metrics",
"timing",
"waste_signals",
}
assert field_names == expected_fields
def test_default_empty_lists(self):
"""Default factory produces empty lists for optional fields."""
result = TransformResult(
messages=[{"role": "user", "content": "test"}],
tokens_before=100,
tokens_after=80,
transforms_applied=["CacheAligner"],
)
assert result.markers_inserted == []
assert result.warnings == []
assert result.diff_artifact is None
assert result.cache_metrics is None
def test_list_isolation(self):
"""Each instance gets its own lists."""
result1 = TransformResult(
messages=[],
tokens_before=100,
tokens_after=80,
transforms_applied=["Transform1"],
)
result2 = TransformResult(
messages=[],
tokens_before=100,
tokens_after=80,
transforms_applied=["Transform2"],
)
result1.markers_inserted.append("marker")
result1.warnings.append("warning")
assert result2.markers_inserted == []
assert result2.warnings == []
class TestRequestMetrics:
"""Tests for RequestMetrics dataclass."""
def test_dataclass_fields(self):
"""RequestMetrics has all expected fields."""
field_names = {f.name for f in fields(RequestMetrics)}
expected_fields = {
"request_id",
"timestamp",
"model",
"stream",
"mode",
"tokens_input_before",
"tokens_input_after",
"tokens_output",
"block_breakdown",
"waste_signals",
"stable_prefix_hash",
"cache_alignment_score",
"cached_tokens",
feat: Add CCR architecture, TOIN telemetry, and DevEx improvements ## Core Features ### Compress-Cache-Retrieve (CCR) Architecture - Implement reversible compression with automatic retrieval support - Add CompressionStore for caching original content with TTL-based eviction - Add CompressionFeedback for learning from retrieval patterns - Implement tool injection for LLM retrieval capability - Add MCP server support for CCR operations - Track retrieval rates to dynamically adjust compression aggressiveness ### Tool Output Intelligence Network (TOIN) - Implement cross-session pattern learning for tool compression - Add ToolSignature for structural hashing of tool outputs - Track compression success rates per strategy (top_n, sample, truncate, etc.) - Implement privacy-preserving telemetry with SHA256 hashing - Add persistent storage with JSON file backend - Support network-effect learning across tool types ### SmartCrusher Enhancements - Add crushability analysis with variance/uniqueness detection - Implement statistical anomaly detection for outlier preservation - Add relevance-based item prioritization using BM25 scoring - Support multiple compression strategies with quality retention - Add change point detection for time-series data - Implement constant factoring for homogeneous datasets ## Developer Experience Improvements ### Exception Hierarchy - Add HeadroomError base class for all custom exceptions - Add specific exceptions: ConfigurationError, ProviderError, StorageError, CompressionError, TokenizationError, CacheError, ValidationError, TransformError ### Client Enhancements - Add validate_setup() for configuration verification - Add get_stats() for in-memory session metrics without DB query - Track session statistics (requests, tokens saved, cache hits) ### Logging Infrastructure - Add structured logging to TransformPipeline with token savings - Add logging to RollingWindow for dropped message tracking - Add logging to ToolCrusher for compression events - Add logging to CacheAligner for cache hit/miss detection - Add logging to SmartCrusher for strategy selection ## Bug Fixes (from deep analysis) ### Critical Fixes - Fix eviction heap memory leak with stale entry tracking - Fix hash collision detection in compression store - Fix strategy truncation desync in TOIN - Fix non-deterministic set truncation with sorted iteration - Fix race conditions in lazy initialization with proper locking - Fix user count double-counting in TOIN metrics ### High Priority Fixes - Fix unbounded strategy_success_rates growth with LRU eviction - Fix mutable pattern references with defensive copying - Fix lock held during file I/O with copy-then-write pattern - Fix state divergence on eviction with success event recording - Fix TOIN skip check order for CPU efficiency - Fix preserve_fields type mismatch (set vs list) - Fix prioritize_indices exceeding max_items limit - Fix instance ID collision risk (32-bit to 64-bit hash) ## Testing - Add comprehensive test suites for CCR, TOIN, and telemetry - Add crushability detection tests - Add quality retention tests for compression - Add integration tests for cross-component data flow - All 902 tests passing
2026-01-10 10:12:13 -08:00
# Cache optimizer metrics (provider-specific)
"cache_optimizer_used",
"cache_optimizer_strategy",
"cacheable_tokens",
"breakpoints_inserted",
"estimated_cache_hit",
"estimated_savings_percent",
"semantic_cache_hit",
# Transform details
"transforms_applied",
"tool_units_dropped",
"turns_dropped",
"messages_hash",
"error",
}
assert field_names == expected_fields
def test_default_values(self):
"""Default values are correctly set for optional fields."""
metrics = RequestMetrics(
request_id="test-123",
timestamp=datetime(2025, 1, 6),
model="gpt-4o",
stream=False,
mode="audit",
tokens_input_before=1000,
tokens_input_after=800,
)
assert metrics.tokens_output is None
assert metrics.block_breakdown == {}
assert metrics.waste_signals == {}
assert metrics.stable_prefix_hash == ""
assert metrics.cache_alignment_score == 0.0
assert metrics.cached_tokens is None
assert metrics.transforms_applied == []
assert metrics.tool_units_dropped == 0
assert metrics.turns_dropped == 0
assert metrics.messages_hash == ""
assert metrics.error is None
def test_full_creation(self):
"""RequestMetrics can be created with all fields."""
metrics = RequestMetrics(
request_id="req-456",
timestamp=datetime(2025, 1, 6, 12, 30),
model="claude-3-opus",
stream=True,
mode="optimize",
tokens_input_before=2000,
tokens_input_after=1500,
tokens_output=500,
block_breakdown={"system": 200, "user": 800},
waste_signals={"json_bloat": 100},
stable_prefix_hash="hash123",
cache_alignment_score=95.5,
cached_tokens=200,
transforms_applied=["CacheAligner", "SmartCrusher"],
tool_units_dropped=2,
turns_dropped=1,
messages_hash="msghash",
error=None,
)
assert metrics.request_id == "req-456"
assert metrics.model == "claude-3-opus"
assert metrics.stream is True
assert metrics.tokens_output == 500
assert metrics.cache_alignment_score == 95.5
def test_dict_isolation(self):
"""Each instance gets its own dicts and lists."""
metrics1 = RequestMetrics(
request_id="1",
timestamp=datetime.now(),
model="m",
stream=False,
mode="audit",
tokens_input_before=100,
tokens_input_after=100,
)
metrics2 = RequestMetrics(
request_id="2",
timestamp=datetime.now(),
model="m",
stream=False,
mode="audit",
tokens_input_before=100,
tokens_input_after=100,
)
metrics1.block_breakdown["system"] = 50
metrics1.waste_signals["json_bloat"] = 25
metrics1.transforms_applied.append("Test")
assert metrics2.block_breakdown == {}
assert metrics2.waste_signals == {}
assert metrics2.transforms_applied == []