headroom/tests/test_transforms
Parideboy c811007f81
fix(kompress): match all ONNX backends with startswith, not exact "onnx" (#2448)
## Description

With `HEADROOM_KOMPRESS_BACKEND=onnx_coreml`, every Kompress compression
call and the startup canary crash with `'_OnnxModel' object has no
attribute 'parameters'`, so Kompress silently degrades to passthrough
and `/health` reports `kompress: unhealthy, backend: null`.

Root cause: `headroom/transforms/kompress_compressor.py` gated the
ONNX-vs-PyTorch branch with an exact string match `backend == "onnx"`.
But `_load_kompress_onnx` returns `onnx_coreml` (CoreML) or `onnx_cpu` —
never the bare string `onnx`. So under `onnx_coreml` the code built
PyTorch tensors and dispatched to a device via
`next(model.parameters())`, which the `_OnnxModel` wrapper doesn't
implement. This is the accelerated backend Apple Silicon users reach
for, so the fast path is exactly the broken one.

Fixes #2442

## 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

- Change the four exact-match `backend == "onnx"` sites in
`headroom/transforms/kompress_compressor.py` to
`backend.startswith("onnx")`, matching the convention already used by
`_model_device_type`: `_timed_canary`, `compress`, `compress_batch`, and
the batch-parallelism guard in `_should_use_sequential_fallback`.
- Update the guard comment ("ONNX CPU provider" → "ONNX EPs") since it
now covers all ONNX execution providers.
- Add regression tests exercising `_timed_canary` on `onnx_coreml` (must
take the numpy path and never touch `.parameters()`) with a negative
control proving the PyTorch branch still dispatches to a device.
- Leave `CHANGELOG.md` untouched — release-please generates it from
conventional commits.
- Out of scope: the secondary `/health` under-reporting the issue flags
as informational (deferred-preload warmup object never flips to
`loaded`).

## Testing

- [x] Unit tests pass (`python -m pytest
tests/test_transforms/test_kompress_compressor.py::TestOnnxBackendPrefixGating
-q`)
- [x] Linting passes (`ruff check`, `ruff format --check` on the two
changed files)
- [x] Type checking passes (`mypy
headroom/transforms/kompress_compressor.py --ignore-missing-imports`)
- [x] New tests added for new functionality

### Test Output

```text
$ python -m pytest tests/test_transforms/test_kompress_compressor.py::TestOnnxBackendPrefixGating -q
collected 2 items
tests\test_transforms\test_kompress_compressor.py ..                     [100%]
2 passed in 2.20s

$ ruff check headroom/transforms/kompress_compressor.py tests/test_transforms/test_kompress_compressor.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, local dev checkout on a branch
off upstream/main (no Apple Silicon / CoreML hardware available)
- Exact command / steps: Ran the new `TestOnnxBackendPrefixGating`
regression; then temporarily reverted one site back to `backend ==
"onnx"` and re-ran to confirm the test discriminates.
- Observed result: With the fix, `_timed_canary(model, tokenizer,
"onnx_coreml")` returns a float and never touches `.parameters()`.
Reverting one site makes the onnx_coreml test fail (it takes the `pt`
tensor path and hits the paramless model), proving the test catches the
exact bug. The issue reporter separately verified the fix on real Apple
Silicon hardware (onnxruntime 1.27.0, CoreMLExecutionProvider): zero
occurrences of the error afterward and compression completing on the
CoreML session.
- Not tested: End-to-end run on real CoreML hardware from this
environment — reproduced via the unit-level device-dispatch seam
instead; hardware confirmation is in the issue.

## Review Readiness

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

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:29:08 -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(kompress): match all ONNX backends with startswith, not exact "onnx" (#2448) 2026-07-20 17:29:08 -07: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