headroom/tests/test_transforms
Abhay Singh b75999017f
fix(transforms/kompress-remote): keep compress fail-open on malformed 200 (#2320)
## Description

`RemoteKompressCompressor` (the opt-in `HEADROOM_KOMPRESS_ENDPOINT`
remote compression client) documents a fail-open contract in its own
docstring:

> Fails OPEN: any network/HTTP error returns the content verbatim so a
flaky endpoint degrades compression rather than breaking the proxy.

But only the network call and the `compressed` field check actually run
inside the fail-open guard. The metadata coercions run **after** the
`except`, outside it:

```python
try:
    resp = self._client.post(...)
    resp.raise_for_status()
    data = resp.json()
    compressed = data["compressed"]
    if not isinstance(compressed, str):
        raise TypeError("...")
except Exception as e:  # fail OPEN
    logger.warning("Remote Kompress failed (%s); passing through", e)
    return self._passthrough(content, n_words)

result = KompressResult(
    compressed=compressed,
    original=content,
    original_tokens=int(data.get("original_tokens", n_words)),
    compressed_tokens=int(data.get("compressed_tokens", len(compressed.split()))),
    compression_ratio=float(data.get("compression_ratio", 1.0)),   # <-- outside the guard
    model_used=str(data.get("model_used", self.config.model_id)),
)
```

So a hosted `/compress` endpoint that returns a 200 with a valid
`compressed` string but a malformed metadata field escapes the guard and
raises out of `compress`, breaking the proxy request instead of passing
through. The most realistic trigger is an explicit JSON `null`:
`data.get("compression_ratio", 1.0)` returns `None` for a **present**
key (the default only applies to a missing key), and `float(None)`
raises `TypeError`. A non-numeric string like `"original_tokens":
"lots"` raises `ValueError` the same way. Since the whole point of the
flag is to support arbitrary self-hosted endpoints, a slightly-off but
well-meaning endpoint (sending `null` for a field it could not compute)
takes down the request path this class exists to protect.

## Fix

Move the response parsing (the `KompressResult` construction with its
`int`/`float`/`str` coercions) inside the fail-open `try`, so any
malformed field degrades to verbatim passthrough like every other
bad-response case:

```python
try:
    ...
    compressed = data["compressed"]
    if not isinstance(compressed, str):
        raise TypeError("...")
    result = KompressResult(
        compressed=compressed,
        original=content,
        original_tokens=int(data.get("original_tokens", n_words)),
        compressed_tokens=int(data.get("compressed_tokens", len(compressed.split()))),
        compression_ratio=float(data.get("compression_ratio", 1.0)),
        model_used=str(data.get("model_used", self.config.model_id)),
    )
except Exception as e:  # fail OPEN
    logger.warning("Remote Kompress failed (%s); passing through", e)
    return self._passthrough(content, n_words)
```

No behavior change on a well-formed response; only the malformed-200
path changes (raise to passthrough).

## Type of Change

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

## Changes Made

- `headroom/transforms/kompress_remote.py`: move the `KompressResult`
construction and its field coercions inside the fail-open `try`.
- `tests/test_transforms/test_kompress_remote.py`: add
`test_remote_kompress_null_numeric_field_fails_open` (explicit JSON
`null`) and `test_remote_kompress_non_numeric_field_fails_open`
(non-numeric string), both asserting verbatim passthrough.
- `CHANGELOG.md`: Bug Fixes entry.

## Testing

- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ uvx ruff@0.15.17 check headroom/transforms/kompress_remote.py tests/test_transforms/test_kompress_remote.py
All checks passed!
$ uvx ruff@0.15.17 format --check headroom/transforms/kompress_remote.py tests/test_transforms/test_kompress_remote.py
2 files already formatted
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/transforms/kompress_remote.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOMs this box (ML-stack import), so I
reproduced the control flow with a dependency-free script and left the
full pytest to CI.
- Exact command / steps: modeled the OLD (coercions outside the `try`)
and NEW (inside the `try`) parsing against a 200 body `{"compressed":
"short result", "compression_ratio": null}` and against a well-formed
body.
- Observed result: OLD raised `TypeError` on the null field (proxy
request breaks); NEW returned passthrough; a well-formed body still
compressed under NEW. The added tests assert both malformed cases
(`null` and non-numeric string) return the original content with
`compression_ratio == 1.0`.
- Not tested: a live remote Kompress endpoint; the added tests drive
`RemoteKompressCompressor` through an `httpx.MockTransport`, matching
the existing test harness in this file.

## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

The "unit tests pass locally" box is unchecked because a local pytest
run imports the ML stack and OOMs this box; the added tests reuse the
existing `httpx.MockTransport` harness in `test_kompress_remote.py` and
run under the normal CI pytest job, and the behavior is corroborated by
the standalone proof above.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-17 12:11:41 -07:00
..
__init__.py Initial commit: Headroom SDK - LLM context optimization toolkit 2026-01-06 23:16:58 -08:00
test_code_compressor.py fix(code-compressor): recover valid Python rewrites after local syntax rejection (#2202) 2026-07-14 20:19:46 -07:00
test_code_compressor_cjk.py fix(code-compressor): CJK-aware relevance-query symbol matching (#1747) 2026-07-07 12:49:26 -05:00
test_content_router.py fix(proxy): cold-start fast pass — defer only Kompress, not the whole pipeline (#2073) 2026-07-14 06:34:41 -04:00
test_detect_fallback_1123.py fix(deps): remediate dependency CVEs and publish SBOM (#1509) 2026-06-27 15:28:12 -07:00
test_diff_compressor.py fix(transforms): normalize diff compressor context (#1801) 2026-07-05 14:03:33 -07:00
test_diff_compressor_rust_parity.py feat(rust): retire python diff_compressor, ship rust-only via pyo3 2026-04-26 09:15:37 -07:00
test_html_extractor.py fix(tests): skip HTML extractor tests when trafilatura not installed 2026-01-31 15:39:55 -08:00
test_kompress_compressor.py fix(proxy): make Kompress eager preload cache-only so a cold cache can't block startup (#783) 2026-06-11 12:53:03 -05:00
test_kompress_deadline.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_kompress_remote.py fix(transforms/kompress-remote): keep compress fail-open on malformed 200 (#2320) 2026-07-17 12:11:41 -07:00
test_kompress_size_gate.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_ort_dylib.py fix(core): load ONNX Runtime dynamically so headroom._core imports on non-AVX2 x86-64 (#1715) 2026-07-14 13:25:41 -04:00
test_pipeline_waste_signal_limit.py fix(proxy): keep large compression results on the critical path (#296) (#1352) 2026-06-24 10:15:59 -05:00
test_read_lifecycle.py fix(read-lifecycle): persist STALE Read originals in the CCR store (#1488) 2026-06-28 14:50:45 -07:00
test_smart_crusher_attribution.py feat(transforms): attribute read_lifecycle + smart_crush tags (#249) 2026-06-11 11:51:26 -05:00
test_smart_crusher_audit_safe.py feat(compression): add audit-safe mode with protected pattern matching (#1899) 2026-07-09 09:39:35 -04:00
test_smart_crusher_bugs.py Marker-free lossless_only mode + gate opaque-blob CCR markers behind enable_ccr_marker (#1129) 2026-06-23 12:52:15 -05:00
test_smart_crusher_ccr_retrieve_exemption.py fix(proxy): stop re-compressing headroom_retrieve output and emitting unredeemable markers (#1323) 2026-06-25 10:11:42 -05:00
test_smart_crusher_ccr_roundtrip.py chore(rust): SmartCrusher CCR marker injection + walker unification 2026-04-27 20:25:22 -07:00
test_smart_crusher_lossless_default.py feat(rust): SmartCrusher PR4 — lossless-first default + CCR-Dropped restoration 2026-04-27 16:30:22 -07:00
test_smart_crusher_rust_parity.py feat(rust): SmartCrusher PR4 — lossless-first default + CCR-Dropped restoration 2026-04-27 16:30:22 -07:00
test_tag_protector.py fix: A9 — tag protector discards wrap on placeholder loss 2026-05-02 18:01:24 -07:00
test_text_crusher.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_text_crusher_cjk_eval.py feat(text-crusher): CJK-aware segmentation + relevance via ICU (#1504) 2026-07-15 19:58:48 +00:00
test_text_crusher_parity.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_text_crusher_routing.py fix(router): compact JSON evades compression via whitespace token counting (#1857) 2026-07-14 06:54:01 -04:00
test_tree_sitter_thread_safety.py fix(code): pin tree-sitter-language-pack <1.0.0 in [code] extra (#1219) 2026-07-15 20:54:25 +00:00