headroom/tests/test_transforms/test_smart_crusher_bugs.py
Nadia Ujovich 7c93c50c2c
Marker-free lossless_only mode + gate opaque-blob CCR markers behind enable_ccr_marker (#1129)
## Description

`enable_ccr_marker` only gated the **row-drop sentinel** path. The
**opaque-blob** path still emitted `<<ccr:HASH,kind,size>>` markers
unconditionally whenever a string cell exceeded `opaque_min_bytes`
(256), so **no configuration could produce a fully marker-free prompt**.
Any `<<ccr:>>` marker is a promise that the full payload lives in the
CCR store and must be fetched back via a retrieval tool call — there was
no way to get compression without that round-trip dependency.

**Rebased on #1130 (merged).** That PR fixed the opaque-blob gate at the
classifier (`ClassifyConfig.emit_opaque_markers`, driven by
`enable_ccr_marker`) and closed #1091. This branch originally carried
its own equivalent gating commit; that commit is now **redundant and has
been dropped** — `classifier.rs` here is identical to upstream. What
remains is the **net-new** work that is **not** in #1130:

- **Strict `lossless_only` mode** — keeps lossless tabular compaction,
but routes every path that would need a CCR marker (row-drop sentinel
**and** opaque-blob offload) to leave content uncompacted instead, so
output is always marker-free **and** byte-recoverable.
- **Python parity** — `lossless_only` exposed across both config
dataclasses, a `SmartCrusher` kwarg, and a per-call `crush(...,
lossless_only=)` override.
- **`HEADROOM_LOSSLESS_ONLY` env var** — wires the mode through to the
proxy runtime so real agents can use it.

The #1130 opaque gate is consumed here through a single centralized
helper (`opaque_markers_enabled() = enable_ccr_marker &&
!lossless_only`) used by **all four** `ClassifyConfig` construction
sites.

## Type of Change

- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [x] Code refactoring (no functional changes)

## Changes Made

- **`feat(smart_crusher)`** — Add `lossless_only` (default `false`):
keeps lossless tabular compaction but routes every marker-requiring path
(row-drop sentinel + opaque-blob offload) to leave content uncompacted
instead. Exposed across the Rust core, PyO3 bridge, both Python config
dataclasses, a `SmartCrusher` kwarg, a per-call `crush(...,
lossless_only=)` override, and `smart_crush_tool_output`. Includes a
`debug_assert` documenting the load-bearing invariant (a `lossless_only`
crusher must never reach the CCR store write).
- **`refactor(smart_crusher)`** — Extract
`SmartCrusherConfig::opaque_markers_enabled()` as the single source of
truth for `enable_ccr_marker && !lossless_only`, consumed by **all
four** `ClassifyConfig` sites: the compaction-stage builder,
`with_compaction_format`, the top-level `process_string` path (Rust
core), and the PyO3 `compact_document_json` document-compactor path. No
site derives the gate inline anymore, so they cannot drift.
- **`feat(proxy)`** — Expose the mode via `HEADROOM_LOSSLESS_ONLY`:
`ContentRouterConfig.smart_crusher_lossless_only` →
`_get_smart_crusher`; the proxy reads the env var and sets it on the
live router config. Previously reachable only via the Python API, never
through the proxy.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check` on changed files)
- [ ] Type checking passes (`mypy headroom`) — not run (see Additional
Notes)
- [x] New tests added for new functionality
- [x] Manual testing performed (proxy env-var seam, end-to-end — see
Real Behavior Proof)

### Test Output

```text
### RUST  (cargo test -p headroom-core --lib smart_crusher)
test result: ok. 323 passed; 0 failed; 0 ignored; 0 measured; 516 filtered out

### PYTEST  (test_smart_crusher_bugs.py + test_agent_savings.py + test_smart_crusher_toin_attachment.py)
45 passed

### RUFF  (changed files)
All checks passed!

### FMT + CLIPPY  (cargo fmt --check && cargo clippy --workspace --lib)
clean — no warnings
```

New/affected tests: `enable_ccr_marker_false_suppresses_opaque_markers`,
`lossless_only_leaves_array_uncompacted_instead_of_dropping`,
`lossless_only_inlines_opaque_blobs_when_table_ships`,
`lossless_only_never_writes_to_ccr_store` (Rust);
`TestLosslessOnlyMode`,
`test_router_lossless_only_flag_reaches_crusher`,
`test_router_lossless_only_defaults_off` (Python). Coexists green with
#1130's `long_string_stays_scalar_when_opaque_markers_disabled` (Rust)
and `test_smart_crusher_toin_attachment.py` (Python). The Python
`TestOpaqueMarkerGate` from the dropped gating commit was removed as
redundant with #1130's coverage.

## Real Behavior Proof

### Proxy env-var seam — end-to-end (this revision)

The one path with no automated coverage was `server.py` reading
`HEADROOM_LOSSLESS_ONLY` from the environment and threading it into the
live router config. Verified end-to-end by instantiating the **real**
`HeadroomProxy`, pulling the `ContentRouter` out of its pipeline, and
crushing a 50-row array with >256B opaque cells through the real Rust
crusher:

| | `HEADROOM_LOSSLESS_ONLY=1` | env unset (default) |
|---|---|---|
| `crusher._lossless_only` | **True** | **False** |
| output contains `<<ccr:` | **No** (marker-free) | Yes (normal lossy) |
| byte-recoverable (round-trips to original JSON) | **Yes** | No (rows
offloaded) |

This confirms the full chain `os.environ["HEADROOM_LOSSLESS_ONLY"]` →
`server.py` → `ContentRouterConfig.smart_crusher_lossless_only` →
`content_router.py` → `crusher_config.lossless_only` → Rust crusher. The
default column proves strict mode genuinely changes behavior (not a
no-op) and that the default path is unchanged.

### Prior live-traffic run

- Environment: Headroom proxy in front of a real agent (Hermes) routed
to NVIDIA NIM (OpenAI-compatible upstream). Isolated config dir;
`OPENAI_TARGET_API_URL=https://integrate.api.nvidia.com/v1`,
`HEADROOM_LOSSLESS_ONLY=1`. Confirmed with `ss` that all LLM traffic
flowed agent → proxy → upstream with no direct bypass.
- Exact command / steps: Start the proxy with `python -m
headroom.proxy.server --host 127.0.0.1 --port 8787`; point the agent's
LLM base_url at `http://127.0.0.1:8787/v1`; run a real chat plus a
`search_files`-style task; read `/stats` and `/v1/retrieve/stats`; then
toggle `HEADROOM_LOSSLESS_ONLY` and repeat for the comparison.
- Observed result: With 150K+ tokens of real traffic processed,
`lossless_only` kept the CCR store empty (`entry_count: 0`) and emitted
zero markers. A synthetic before/after with opaque (>256B) cells
produced 12 `<<ccr:>>` markers in default mode and 0 under
`lossless_only`, with output round-tripping to the original JSON
structure.
- Not tested: A live `lossless_only`-vs-markers contrast on real agent
traffic. The SmartCrusher offload path never engaged on this agent's
tool outputs (`total_compressions: 0`; CCR store stayed at `entry_count:
0` even after a broad codebase search), and compression stayed marginal
(~0.2–0.4%) in both modes. The agent's tool results don't match the
crushable-array profile the offload paths target, so the marker path is
never exercised in that integration. Why SmartCrusher barely engages
with this agent's outputs is a separate integration question (output
format / routing / size thresholds), out of scope for this change.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation — N/A
(config docstrings updated in-tree; no separate docs)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable — N/A

## Additional Notes

- Rebased on top of merged #1130; the now-redundant opaque-blob gating
commit was dropped, so this PR is purely the `lossless_only` feature +
proxy wiring on top of #1130's gate.
- `mypy headroom` was not run in this environment; happy to add the
result if CI requires it.
- Default behavior is fully preserved: `enable_ccr_marker` defaults to
`true`, `lossless_only` defaults to `false`, and
`HEADROOM_LOSSLESS_ONLY` unset is a no-op.
2026-06-23 12:52:15 -05:00

174 lines
7 KiB
Python

"""Regression tests for SmartCrusher bugs.
Bug 1: _crush_number_array mixes types (string summary + numbers),
violating the schema-preserving guarantee.
Bug 2: _current_field_semantics is shared instance state, creating
a race condition when crushing concurrently.
"""
from __future__ import annotations
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from headroom import SmartCrusherConfig
from headroom.transforms.smart_crusher import SmartCrusher
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _make_crusher(max_items: int = 10, min_items: int = 3) -> SmartCrusher:
"""Build a SmartCrusher with deterministic small-K config for tests."""
config = SmartCrusherConfig(
enabled=True,
min_items_to_analyze=min_items,
min_tokens_to_crush=0,
max_items_after_crush=max_items,
variance_threshold=2.0,
)
return SmartCrusher(config=config)
# Bug #1 (number array schema preservation) — invariant pinned by the
# Rust port (`crates/headroom-core/src/transforms/smart_crusher/crushers.rs::
# crush_number_array` + its unit tests) and the parity fixtures
# (`tests/parity/fixtures/smart_crusher/number_array_40_changepoint*`).
# The Python `_crush_number_array` helper that the previous tests
# probed was removed when the Python implementation was retired in
# Stage 3c.1b.
# ---------------------------------------------------------------------------
# Bug 2: Race condition on _current_field_semantics
# ---------------------------------------------------------------------------
class TestFieldSemanticsThreadSafety:
"""_current_field_semantics must not leak between concurrent crushes.
Previously it was stored as instance state (self._current_field_semantics)
which created a race condition when the same SmartCrusher instance
was used from multiple threads.
"""
def test_concurrent_crushes_no_cross_contamination(self) -> None:
"""Two concurrent crushes must not share field_semantics state."""
crusher = _make_crusher(max_items=5)
# Two different array payloads
payload_a = json.dumps([{"name": f"item_{i}", "value": i} for i in range(20)])
payload_b = json.dumps([{"key": f"k_{i}", "score": i * 0.1} for i in range(20)])
results: dict[str, str] = {}
errors: list[Exception] = []
def crush_task(label: str, content: str) -> None:
try:
result, modified, info = crusher._smart_crush_content(content)
results[label] = result
except Exception as e:
errors.append(e)
with ThreadPoolExecutor(max_workers=4) as executor:
futures = []
# Run many concurrent crushes to increase race probability
for i in range(20):
futures.append(executor.submit(crush_task, f"a_{i}", payload_a))
futures.append(executor.submit(crush_task, f"b_{i}", payload_b))
for f in as_completed(futures):
f.result() # Re-raise exceptions
assert not errors, f"Concurrent crushes raised errors: {errors}"
# After all crushes, thread-local state must be clean
tl = getattr(crusher, "_thread_local", None)
if tl is not None:
semantics = getattr(tl, "field_semantics", None)
assert semantics is None, f"field_semantics leaked in thread-local: {semantics}"
# ---------------------------------------------------------------------------
# Issue 7: Recursion depth limit
# ---------------------------------------------------------------------------
class TestRecursionDepthLimit:
"""_process_value must not crash on deeply nested JSON."""
def test_deeply_nested_json_does_not_crash(self) -> None:
"""Nesting deeper than _MAX_PROCESS_DEPTH should return value unchanged."""
crusher = _make_crusher()
# Build a 100-level nested structure
nested: dict = {"leaf": "value"}
for _i in range(100):
nested = {"level": nested}
content = json.dumps(nested)
result, was_modified, info = crusher._smart_crush_content(content)
# Should not raise RecursionError
parsed = json.loads(result)
# The deep structure should be preserved (returned as-is past depth limit)
assert isinstance(parsed, dict)
def test_deeply_nested_list_does_not_crash(self) -> None:
"""Deeply nested lists should also be handled safely."""
crusher = _make_crusher()
nested: list = ["leaf"]
for _i in range(100):
nested = [nested]
content = json.dumps(nested)
result, was_modified, info = crusher._smart_crush_content(content)
parsed = json.loads(result)
assert isinstance(parsed, list)
class TestLosslessOnlyMode:
"""`lossless_only` produces marker-free, byte-recoverable output.
Strict mode: lossless tabular compaction still applies, but any path
that would need a CCR marker (lossy row-drop OR opaque-blob offload)
leaves the content uncompacted instead — so the result is always
marker-free and decodes back to the original input without loss.
"""
def _droppable_rows(self) -> list[dict]:
return [{"path": "a.py", "line": i, "content": "x" * 300} for i in range(50)]
def test_lossless_only_is_marker_free_and_byte_recoverable(self) -> None:
rows = self._droppable_rows()
config = SmartCrusherConfig(
min_items_to_analyze=3,
min_tokens_to_crush=0,
lossless_min_savings_ratio=0.99, # force the would-be-lossy path
lossless_only=True,
)
out = SmartCrusher(config=config).crush(json.dumps(rows))
assert "<<ccr:" not in out.compressed
assert json.loads(out.compressed) == rows
def test_crush_kwarg_overrides_configured_mode(self) -> None:
# Configured non-strict, but the per-call kwarg forces strict mode
# for this call: marker-free and fully recoverable.
rows = self._droppable_rows()
config = SmartCrusherConfig(
min_items_to_analyze=3,
min_tokens_to_crush=0,
lossless_min_savings_ratio=0.99,
)
crusher = SmartCrusher(config=config)
out = crusher.crush(json.dumps(rows), lossless_only=True)
assert "<<ccr:" not in out.compressed
assert json.loads(out.compressed) == rows
# Stage 3c.1 lockstep bug-fix tests previously lived here; they probed
# Python helpers (`_percentile_linear`, `_detect_sequential_pattern`,
# `_detect_rare_status_values`, `_compute_k_split`) that were removed
# along with the Python implementation in Stage 3c.1b. The Rust port
# pins the same invariants — see the `bug1_*` / `bug2_*` / `bug3_*` /
# `bug4_*` tests in `crates/headroom-core/src/transforms/smart_crusher/`
# (notably `crushers.rs` and `analyzer.rs`). Parity fixtures
# (`tests/parity/fixtures/smart_crusher/`) byte-compare the post-fix
# behavior across the language boundary.