headroom/tests/conftest.py

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

425 lines
15 KiB
Python
Raw Permalink Normal View History

"""Shared pytest fixtures for Headroom tests."""
# CRITICAL: Must be set before ANY imports that could trigger sentence_transformers
# The Rust tokenizers use parallelism that deadlocks with pytest-asyncio
import os
os.environ["TOKENIZERS_PARALLELISM"] = "false"
import json
import tempfile
from datetime import datetime
from pathlib import Path
from unittest.mock import Mock
import pytest
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273) ## Description Anthropic request-side CCR can still compress a turn into retrieval markers after the frozen-prefix cache guard suppresses `headroom_retrieve` registration. That leaves the model with marker-only context it cannot redeem, so the proxy silently drops recoverable data on exactly the turns where cache preservation deferred tool injection. This change couples the Anthropic request-side CCR path to tool availability so a turn never emits retrieve-only markers without the retrieval tool, even when token mode or cache-mode prefix replay could otherwise reuse already-compressed marker text. Closes #1006 After a collaborator merged current `main` into this branch, CI also picked up unrelated offline-memory failures from the merged base. Those follow-up changes are test-only: they keep the offline Hugging Face cache lanes skipping cleanly instead of failing in memory tests that are outside the CCR runtime path. ## 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 - Couple Anthropic request-side CCR compression to the same frozen-prefix guard that already defers `headroom_retrieve` registration. - Keep the existing cache-preservation behavior: frozen-prefix turns stop emitting CCR retrieval markers instead of forcing tool injection into the cached prefix. - Make the skip decision use the effective frozen prefix after token-mode reclamping, so turns that genuinely reclamp to zero still keep normal reversible CCR behavior. - Bypass cached marker reuse in both token mode and cache-mode prefix replay when tool injection is deferred. - Add focused regressions for the Anthropic request-path seams under this bug: - frozen-prefix turns do not emit marker-only payloads - unfrozen turns still keep normal reversible CCR behavior - token-mode reclamp back to zero still compresses normally - existing `headroom_retrieve` tools keep reversible CCR on frozen turns - cache-mode delta reuse and exact-prefix replay both forward original content when retrieval is unavailable - Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic CCR behavior changes. - Add a shared test skip helper for offline Hugging Face cache misses and apply it to the merged `main` memory tests that were failing only in the offline CI shards after the branch picked up current `main`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py`) - [x] Linting passes (`uv run ruff check . && uv run ruff format . --check`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py 14 passed, 1 warning in 34.19s uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q 4 passed, 5 skipped, 11 warnings in 7.65s uv run ruff check . All checks passed! uv run ruff format . --check 968 files already formatted ``` ## Real Behavior Proof - Environment: local FastAPI `TestClient` for the Anthropic request path, plus Windows Python 3.12 offline-memory repros with `TRANSFORMERS_OFFLINE=1` - Exact command / steps: Run the focused CCR regression command and the offline-memory repro subset below on the merged branch state. - `uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py` - `uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q` - Scenario coverage from the CCR pytest command: - frozen prefix with deferred tool injection and cached marker text available in token mode - unfrozen turn with normal CCR marker emission - token mode where the tracked frozen prefix reclamps back to zero - frozen prefix where the client already supplied `headroom_retrieve` - cache-mode append-only delta reuse with a previously forwarded compressed prefix - cache-mode exact-prefix replay where the previous forwarded prefix already contained a marker - Scenario coverage from the offline-memory repro command: - direct local embedder startup on CPU with no cached HF model - hierarchical memory embedder startup with offline model cache missing - bridge import through `LocalBackend` - `MemoryHandler` public init warmup path - `LocalBackend` save path under the offline lane - Observed result: The CCR regression keeps marker-free forwarding on frozen turns without tool availability, and the merged-`main` offline-memory lanes now skip cleanly instead of failing unrelated CI shards. - frozen-prefix Anthropic turns without tool availability forward the original long transcript across both token-mode and cache-mode reuse paths, while unfrozen turns, reclamped token-mode turns, and frozen turns that already advertise `headroom_retrieve` keep the reversible CCR marker path - the merged-`main` offline-memory regressions now skip cleanly when the Hugging Face cache is unavailable instead of failing unrelated CI shards - Not tested: live Zed session cache-hit behavior, provider latency under real Anthropic upstreams, and online Hugging Face download lanes ## 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 - [x] 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Runtime scope is still intentionally narrow to Anthropic request-side CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are unchanged. - The only non-CCR diff is the test-only offline-memory follow-up required after current `main` was merged into the branch. - The focused proxy pytest run still emits the Windows-local `StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx` bridge. The offline-memory repro command also emits existing datetime deprecation warnings and a pytest teardown warning around skipped offline lanes; none of those warnings were introduced by the CCR runtime change.
2026-06-23 13:48:05 -04:00
from tests._skip_helpers import external_model_skip_reason
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189) ## Description Running Claude Code (Anthropic) and Codex (OpenAI) against the **same** Headroom proxy instance on one port produced incorrect, unstable dashboard data. The proxy core is provider-isolated and multi-provider-safe by design; the defect was in the observability layer. The Codex `/v1/responses` **WebSocket** handler was the only path in the proxy that wrote to the request logger by hand instead of through the unified `emit_request_outcome` funnel, and it did so twice per session close: the per-turn funnel record plus an unconditional cumulative session-summary `RequestLog`. This PR removes the duplicate summary log so Codex WS emits exactly one request log per turn, matching the HTTP provider paths. ## 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 - Dropped the duplicate cumulative session-summary `RequestLog` in the Codex WS handler while preserving the per-turn `emit_request_outcome` path. - Preserved gated `request_messages` and `turn_id` on residual outcomes so dashboard telemetry keeps the useful attribution without double-counting tokens. - Ensured explicit `--anyllm-provider` wins over a leaked `HEADROOM_ANYLLM_PROVIDER` environment variable. - Registered retry delay settings that had drifted out of the settings registry. - Hardened tests against developer-shell `HEADROOM_*` / `ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused proxy/wrap test fixtures. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/pytest tests/ -q -p no:cacheprovider 8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36) $ .venv/bin/ruff check <touched files> All checks passed! ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff via project venv, branch `fix/multi-provider-runtime`. - Exact command / steps: Ran the full test suite without pytest cache provider and Ruff on all touched files; used `git stash` to confirm the stale fake-config failures pre-existed this change. - Observed result: Full suite passed with no failures; Ruff passed; Codex WS now routes end-of-session logging through `emit_request_outcome`, emitting one request log per turn with the same accounting model as Anthropic HTTP turns. - Not tested: Live simultaneous Claude + Codex dashboard run. `mypy headroom` was not run to completion; a scoped run reported one pre-existing `settings_store.py:470` coercion error outside this diff. ## 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 - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - server-side observability fix; no UI markup changed. ## Additional Notes - The proxy's multi-provider routing, header/auth isolation, and per-model cache keying are already correct and unchanged here; only the WS observability write path was double-counting. - Architectural assessment: `plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`; root-cause + resolution trail: `plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`. - No live simultaneous Claude + Codex dashboard run was performed; validation is from test coverage and code review of the WS logging path. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-16 01:18:34 +07:00
# A live `headroom` dev session exports HEADROOM_* into the shell (and the
# Claude wrap adds ANTHROPIC_CUSTOM_HEADERS). Click `envvar=` options pick
# those up inside CliRunner, so assertions would see the developer's proxy
# config instead of the test's. Scrub them so local runs match CI; tests
# that need a value set it explicitly via monkeypatch or CliRunner env.
fix(wrap): verify proxy deps before mutating Codex config (#1628) ## Description \`headroom wrap codex\` now verifies that optional proxy dependencies (\`headroom-ai[proxy]\`) are installed before mutating Codex \`config.toml\`. If the check fails, the command exits with the same error message as \`headroom proxy\` and leaves Codex config untouched. Fixes #1614 (Bug 1: config mutated before proxy dependency check). ## 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 - Extract \`ensure_proxy_dependencies()\` in \`headroom/cli/proxy.py\` (shared with \`headroom proxy\`) - Call it at the start of \`wrap codex\` when \`not no_proxy\`, before config snapshot/injection - Add regression tests for prepare-only abort, \`--no-proxy\` skip, and import failure messaging ## Testing - [x] Unit tests pass (\`pytest\`) - [x] Linting passes (\`ruff check .\`) - [ ] Type checking passes (\`mypy headroom\`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output \`\`\`bash pytest tests/test_cli/test_wrap_codex.py::test_wrap_codex_aborts_before_mutating_config_when_proxy_deps_missing \ tests/test_cli/test_wrap_codex.py::test_wrap_codex_skips_proxy_dependency_check_with_no_proxy \ tests/test_cli/test_wrap_codex.py::test_ensure_proxy_dependencies_exits_when_server_import_fails -q # 3 passed ruff check headroom/cli/wrap.py headroom/cli/proxy.py tests/test_cli/test_wrap_codex.py ruff format --check headroom/cli/wrap.py headroom/cli/proxy.py tests/test_cli/test_wrap_codex.py \`\`\` ## Real Behavior Proof Environment: Linux (Ubuntu), Python 3.12, local checkout with \`PYTHONPATH\` pointed at patched sources. Exact command / steps: 1. Created a temp \`~/.codex/config.toml\` with \`model_provider = "openai"\`. 2. Patched \`headroom.cli.wrap.ensure_proxy_dependencies\` to raise \`SystemExit(1)\` (simulating missing \`[proxy]\` extra). 3. Ran \`headroom wrap codex --prepare-only --no-serena --port 8787\`. Observed result: exit code 1; \`config.toml\` unchanged; no \`config.toml.headroom-backup\` created; no \`[mcp_servers.headroom]\` block written. Also verified: \`headroom wrap codex --prepare-only --no-proxy ...\` does not invoke the dependency check. Not tested: Windows-specific proxy selector behavior (covered separately in #1655). ## 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 - [x] 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 - [x] New and existing unit tests pass locally with my changes - [x] I did not edit CHANGELOG.md; release notes are generated automatically --------- Co-authored-by: syf2211 <syf2211@users.noreply.github.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-13 09:52:22 -07:00
@pytest.fixture(autouse=True)
def _skip_proxy_dependency_gate_unless_exercised(
request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Most CLI tests run without headroom-ai[proxy] extras installed."""
if request.node.get_closest_marker("proxy_dependency_gate") is not None:
return
try:
from headroom.cli import proxy
except ModuleNotFoundError:
# Native-wrapper jobs intentionally install only pytest and exercise the
# installer scripts without importing Headroom's runtime dependencies.
return
monkeypatch.setattr(proxy, "ensure_proxy_dependencies", lambda: None)
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189) ## Description Running Claude Code (Anthropic) and Codex (OpenAI) against the **same** Headroom proxy instance on one port produced incorrect, unstable dashboard data. The proxy core is provider-isolated and multi-provider-safe by design; the defect was in the observability layer. The Codex `/v1/responses` **WebSocket** handler was the only path in the proxy that wrote to the request logger by hand instead of through the unified `emit_request_outcome` funnel, and it did so twice per session close: the per-turn funnel record plus an unconditional cumulative session-summary `RequestLog`. This PR removes the duplicate summary log so Codex WS emits exactly one request log per turn, matching the HTTP provider paths. ## 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 - Dropped the duplicate cumulative session-summary `RequestLog` in the Codex WS handler while preserving the per-turn `emit_request_outcome` path. - Preserved gated `request_messages` and `turn_id` on residual outcomes so dashboard telemetry keeps the useful attribution without double-counting tokens. - Ensured explicit `--anyllm-provider` wins over a leaked `HEADROOM_ANYLLM_PROVIDER` environment variable. - Registered retry delay settings that had drifted out of the settings registry. - Hardened tests against developer-shell `HEADROOM_*` / `ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused proxy/wrap test fixtures. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/pytest tests/ -q -p no:cacheprovider 8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36) $ .venv/bin/ruff check <touched files> All checks passed! ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff via project venv, branch `fix/multi-provider-runtime`. - Exact command / steps: Ran the full test suite without pytest cache provider and Ruff on all touched files; used `git stash` to confirm the stale fake-config failures pre-existed this change. - Observed result: Full suite passed with no failures; Ruff passed; Codex WS now routes end-of-session logging through `emit_request_outcome`, emitting one request log per turn with the same accounting model as Anthropic HTTP turns. - Not tested: Live simultaneous Claude + Codex dashboard run. `mypy headroom` was not run to completion; a scoped run reported one pre-existing `settings_store.py:470` coercion error outside this diff. ## 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 - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - server-side observability fix; no UI markup changed. ## Additional Notes - The proxy's multi-provider routing, header/auth isolation, and per-model cache keying are already correct and unchanged here; only the WS observability write path was double-counting. - Architectural assessment: `plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`; root-cause + resolution trail: `plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`. - No live simultaneous Claude + Codex dashboard run was performed; validation is from test coverage and code review of the WS logging path. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-16 01:18:34 +07:00
@pytest.fixture(autouse=True)
def _scrub_developer_headroom_env(monkeypatch):
for key in list(os.environ):
if key.startswith("HEADROOM_"):
monkeypatch.delenv(key, raising=False)
monkeypatch.delenv("ANTHROPIC_CUSTOM_HEADERS", raising=False)
fix(proxy): repair main lint (ruff-format drift + mypy host_header) (#2268) ## Description `main`'s `lint` CI job is currently **red** (latest main `eac49656` → `lint: failure`), which blocks every open PR. Two causes, both from recent merges that were green in isolation but combined into a red `main`: - **ruff-format drift** on 7 files — committed with formatter output that ruff `0.15.17` (the CI-pinned version) rewrites. - **mypy error** in `server.py`: `_request_has_same_origin_or_no_provenance(request, host_header)` — `host_header` is `request.headers.get("host")` (`str | None`) but the function requires `str`. These passed per-PR because each PR's checks ran against an older base; the serialized `main` state is what went red — a logical-merge / tool-version gap that per-PR CI doesn't catch without a strict merge queue. ## Type of Change - [x] Bug fix (CI/lint repair) ## Changes Made - `ruff format` (0.15.17) the 7 drifted files — formatting only, no logic changes: `cli/proxy.py`, `proxy/forwarded_headers.py`, `proxy/savings_tracker.py`, `proxy/server.py`, `tests/conftest.py`, `tests/test_persistent_metrics_persistence.py`, `tests/test_proxy_loopback_gating.py`. - Add `assert host_header is not None` after the `is_ip_literal_host_header()` guard (which already rejects a missing Host), narrowing the type for the same-origin check. ## Testing - [x] `ruff check .` — clean - [x] `ruff format --check .` — clean (tracked) - [x] `mypy headroom --ignore-missing-imports` — clean ### Test Output ```text $ mypy headroom --ignore-missing-imports → Success: no issues found in 504 source files $ ruff check . → All checks passed (tracked) $ ruff format --check . → clean (tracked) ``` ## Real Behavior Proof - Environment: branch off current `main` (`eac49656`), ruff 0.15.17 + mypy 1.20.2 (CI-pinned). - Confirmed `lint: failure` on main's latest CI run; after this change all three lint steps pass locally. - Not tested: full pytest suite — formatting + a type-narrowing `assert` only, no behavior 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 (this *is* the style fix) - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [ ] Tests added (N/A — no behavior change) - [x] New and existing unit tests pass locally - [ ] CHANGELOG (N/A) ## Additional Notes The 7 files were touched by recent merges (#2198, #2247) whose local ruff differed from the pinned `0.15.17`. Merging this unblocks the `lint` gate for all open PRs (including #2207). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 23:18:50 -07:00
fix(telemetry): anonymous compression stats — no prompts, no data (#2728) ## In one line Headroom starts reporting **how well compression is working** — counters and percentages only. **No prompts. No code. No file paths. Nothing about what you're building.** ## Why Right now nobody knows whether compression actually helps real users. You can see your own numbers in `/stats`, but that's it — there's no way to tell whether a given workload compresses well, or why it sometimes doesn't. This closes that loop so we can make compression better for everyone. ## Exactly what gets sent One message per session, and every 5 minutes while you're active: ```json { "session": { "id": "random", "turns": 47, "duration_s": 4210, "seq": 3 }, "tokens": { "original": 890000, "attempted": 410000, "saved": 320000, "tool_saved": 48000, "cache_read": 210000 }, "rates": { "saved_pct": 35.96, "eligible_pct": 46.07, "yield_pct": 78.05, "cache_read_pct": 23.60, "overhead_pct": 1.96 }, "compression": { "transforms": {"crush": 47}, "passthrough_turns": 0 }, "skips": {}, "sources": { "proxy": 47 }, "providers": ["anthropic"], "models": ["claude-sonnet-4-5-20250929"], "failures": 2 } ``` Plus a random install ID, the Headroom version, and OS/architecture (`darwin`, `arm64`). That's the whole thing. A full example lives at `deploy/beacon/sample-event.json`. ## What is never sent - Your prompts or the model's responses - Your code - File paths, project names, repo names - Tool names or MCP server names - Hostname, username, or IP address - Custom or fine-tuned model names (an id like `ft:gpt-4o:acme-corp:…` contains a company name, so only models in a public registry are reported) **This is structural, not a pinky-swear.** Every value in the payload is a number, a fixed word, or a random ID — there is no free-text field anywhere for content to hide in. The receiver (`deploy/beacon/worker.js`, in this repo so you can read it) drops anything not on an explicit allowlist before storing. ## Turning it off Any one of these: ```bash HEADROOM_BEACON=off # or DO_NOT_TRACK=1 # or # offline mode ``` It's on by default, and Headroom says so at startup: ``` Telemetry: anonymous compression stats — never prompts, code, or file paths. Helps us improve compression | Off: HEADROOM_BEACON=off ``` `HEADROOM_TELEMETRY` is a **separate** switch that still only affects local stats. If you had turned that on, this change does not start uploading anything — you answered a different question, and upgrading should not change the answer. ## Why the percentages, not just "tokens saved" "We saved 36%" hides the interesting part. In the example above only **46% of tokens were eligible** for compression at all — the rest is frozen cache prefix and system prompts we deliberately do not touch. Of what we *could* touch, we removed **78%**. Those are two separate problems. Raising eligibility is proxy work; raising yield is compressor work. A single number cannot tell us which to fix. ## Coverage `emit_request_outcome` is a single chokepoint — `handler.metrics.record_request` is called from exactly one place, inside the funnel — so all 30 `RequestOutcome` construction sites are covered: Anthropic, OpenAI, Gemini, Bedrock, batch, streaming, and the long-lived Codex Responses-WS path. The `headroom_compress` MCP path bypassed that funnel and is now wired in separately. It has a different shape (no provider, no upstream latency, and everything handed to the tool is eligible by construction), so `sources` counts turns by origin — MCP turns always read `eligible_pct: 100` and must not drag the proxy's real eligibility ceiling upward. **Subagents.** All subagent traffic through the proxy merges into one session, which is correct for savings and retention but means `turns` conflates fan-out with depth. Fan-out is still derivable — `compression.latency_ms_total / session.duration_s` gives the concurrency ratio (~1x serial, ~4x for four parallel agents), so no extra field is needed. Verified no lost updates under 6-way concurrency (1,200 turns). **Known gap:** `--workers N` gives each process its own aggregator, so one user session becomes up to N. Token totals and fleet rates stay correct; session counts inflate. This matches the existing documented limitation that TOIN state, CostTracker, and the prefix tracker are all per-process. ## Notes for reviewers - **Cumulative snapshots, not deltas.** Every report restates running totals under one session ID, so the highest `seq` per `(install, session)` is the complete session. Dedupe is a window function, and a lost report costs nothing. - **Never breaks the proxy.** Every path swallows its own exceptions; uploads go out on a daemon thread so nothing blocks the request loop. - **Explicit User-Agent is load-bearing.** urllib's default is blocked by Cloudflare (error 1010). Combined with fire-and-forget error handling, that would have failed every upload while looking perfectly healthy. - **The exit flush was broken and is fixed.** `atexit` handed the POST to a daemon thread, and daemon threads are killed before they finish during interpreter shutdown — so nothing was sent. That silently dropped *every session shorter than the 5-minute heartbeat*, plus all short-lived subagent MCP processes. The exit path now posts synchronously with a 2s timeout. - Receiver and query tooling are in `deploy/beacon/`. ## Testing - `python -m headroom.telemetry.session` self-check: dedupe, cumulative totals, dropped-report recovery, payload contains no model id or prompt-derived string, allowlist coverage - 175 telemetry/outcome tests pass; 6 new ones cover the opt-out notice - Verified end to end against a live deployment: client → receiver → storage → query ## Still to do before release The default endpoint currently points at a temporary `workers.dev` URL. It needs to move to a Headroom-owned hostname before this ships in a tagged release — noted inline at `DEFAULT_ENDPOINT`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 05:43:25 -07:00
# The scrub above deletes every HEADROOM_* var — which includes HEADROOM_BEACON,
# and the beacon defaults to ON. So scrubbing for hermeticity is precisely what
# switches it on, and with HEADROOM_TELEMETRY_ENDPOINT scrubbed too it falls back
# to the real production endpoint. Every test that reaches the outcome funnel
# then POSTs a session event for real: observed writing into the live corpus
# during a local run, and CI would do the same on every push.
#
# Depends on the scrub fixture so it is guaranteed to run after it rather than
# relying on declaration order. A test that wants the beacon on just sets the
# var itself — monkeypatch inside the test wins over this.
@pytest.fixture(autouse=True)
def _disable_telemetry_beacon(monkeypatch, _scrub_developer_headroom_env):
monkeypatch.setenv("HEADROOM_BEACON", "off")
fix(wrap/serena): stop creating serena_config.yml, unbricking Serena on fresh installs (#2676) ## Description `_ensure_serena_dashboard_disabled()` wrote a one-key bootstrap config (`web_dashboard_open_on_launch: false`) into `~/.serena/serena_config.yml` when the file was absent, assuming Serena fills in any key it omits. Verified against **Serena 1.6.2.dev0** (`serena/config/serena_config.py`), that holds for every field except one. Serena autogenerates its own complete config **only when the path does not exist**: ```python if not os.path.exists(config_file_path): cls._generate_config_file(config_file_path) ``` Once any file is present it validates instead. Every other field falls back to a dataclass default via `get_value_or_default`, but a missing `projects` key is fatal (~line 1064): ``` SerenaConfigError: `projects` key not found in Serena configuration. ``` So Headroom's own bootstrap file killed Serena on **every machine without a pre-existing Serena config**. The MCP server exited during handshake — surfacing as `connection closed: initialize response` on Codex and a bare `MCP error -32000: Connection closed` on OpenCode (#2674) — and `serena project index` failed identically. Headroom now leaves that file to Serena. That is immune to Serena adding required keys later; guessing the schema is what caused the outage. The popup never needed the file anyway: `build_serena_spec` passes `--open-web-dashboard False`, which Serena applies *after* loading the config (`serena/mcp.py:361` — `config.web_dashboard_open_on_launch = open_web_dashboard`), so the flag wins regardless of what is on disk. An **existing** config is still edited in place — dashboard key flipped, `projects: []` backfilled to repair machines an affected version already wrote — preserving a populated `projects` list, other keys and comments. Closes #2674 ## 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 - [x] Code refactoring (no functional changes) ## Changes Made - `_ensure_serena_dashboard_disabled()` never creates `serena_config.yml`; it only edits an existing one, and backfills `projects: []` there to repair already-broken machines. - Dropped `_scope_serena_languages` + `_detect_repo_languages` + `_EXT_TO_SERENA_LANGUAGE` (**−133 lines**). Dead weight: Serena determines languages itself in `ProjectConfig.autogenerate` (`_determine_project_language_servers`) and records them under `language_servers` — `languages`, which Headroom wrote, is a legacy name Serena migrates via `RENAMED_FIELDS`. Serena's generated file uses a block-style list, so our single-line-flow regex never matched it: on any Serena-generated `project.yml` the function was a **verified no-op**. The only case where it acted was creating the file — the same partial-config trap — which also skipped the `project.local.yml` sidecar Serena writes alongside. - **Test isolation:** the MCP install ledger defaults to `~/.headroom/mcp_installs.json`, so any test registering a server wrote into the developer's real ledger (observed adding a live `claude/serena` entry during a local run). `conftest.py` now redirects it per-test. - **Repo config:** `.serena/project.yml` carried a stale `project_name` (`"feature-opencode-wrap"`) and listed only `typescript`, so Serena's symbol index skipped 1331 Python and 194 Rust files for every contributor. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_wrap_code_memory.py tests/test_cli/test_wrap_serena_boost.py \ tests/test_cli/test_serena_migrate.py tests/test_cli/test_serena_disable.py -q 35 passed, 1 skipped in 0.84s $ SERENA_SRC=<serena checkout> pytest tests/test_wrap_code_memory.py -q 13 passed in 0.62s # the skipped test runs when a Serena source tree is available $ ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ mypy headroom/cli/wrap.py Success: no issues found in 1 source file ``` New tests. The key one asserts the invariant rather than our own key list, so it stays correct even if Serena adds a required key — a test pinning `projects: []` would keep passing while users broke again: - `test_serena_config_is_never_created_by_headroom` — Headroom must not pre-empt Serena's bootstrap - `test_serena_dashboard_disabled_repairs_config_missing_projects` — heals a config an affected version wrote - `test_serena_dashboard_disabled_preserves_registered_projects` — never clobbers the real registry; comments kept, no duplicate key - `test_serena_dashboard_disabled_is_idempotent` - `test_serena_config_required_keys_match_serena_source` — reads Serena's real source and pins the two facts this fix rests on (bootstrap-only-when-absent, `projects` is the sole fatal omission). Skipped unless `SERENA_SRC` is set; deliberately **not** named `HEADROOM_*` because `conftest.py` scrubs that namespace, which would make it silently always-skip. ## Real Behavior Proof - **Environment:** macOS 15.4 (darwin 25.4.0), Python 3.12.6, Serena 1.6.2.dev0 via `uvx --from git+https://github.com/oraios/serena`, Codex CLI 0.146.0. - **Exact command / steps:** a probe doing a real JSON-RPC `initialize` handshake against the exact command `headroom wrap` registers — i.e. what Codex/OpenCode actually do — in a throwaway `HOME` per case. (A) pre-seeded with the one-line config an affected version wrote; (B) no config; (C) real `headroom wrap codex --prepare-only`, then handshake. - **Observed result:** ```text === A. BROKEN: single-key config (Headroom 0.33.0) === MCP handshake: FAIL — no initialize response (exit=1). stderr tail: File ".../serena/config/serena_config.py", line 1064, in from_config_file raise SerenaConfigError("`projects` key not found in Serena configuration. ...") serena.config.serena_config.SerenaConfigError: `projects` key not found ... config after run: 1 lines, has 'projects': False === B. FIXED: no config, Serena bootstraps it === MCP handshake: PASS — initialize OK — serverInfo.name='Serena' config after run: 213 lines, has 'projects': True === C. FULL FLOW: real `headroom wrap codex` then handshake === Serena: no serena_config.yml yet — letting Serena generate it Serena MCP: registered (restart OpenAI Codex CLI if it was already running) Serena: project pre-indexed (symbol cache warmed) serena_config.yml: 213 lines, written by Serena (correct) MCP handshake: PASS — initialize OK — serverInfo.name='Serena' --- verdict --- A (broken config) started: False <- expected False B (fixed, no config) started: True <- expected True C (after real wrap) started: True <- expected True ``` A second `wrap` in the same HOME flips the dashboard without damage: `true` → `false`, `projects` intact, all 153 comment lines intact. The writer was isolated against a pristine 213-line Serena config: **delta 0 newlines**. - **Not tested:** Windows and Linux (macOS only); Serena versions other than 1.6.2.dev0; the JetBrains language backend. The probe needs network + `uvx` (~2 min) so it is a manual verification tool, not wired into CI. ## 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 - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` — it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this) ## Additional Notes - **Docs:** N/A — no user-facing docs described the `serena_config.yml` bootstrap or the language scoping. - Also fixes the OpenCode report (#2674). The Codex-side report of the same root cause quotes the `SerenaConfigError` verbatim; OpenCode only surfaces the generic `-32000`, which is why it read as two different bugs. - Users already broken by an affected version are repaired automatically on their next `headroom wrap` — no manual `serena_config.yml` edit needed. - A stacked PR removing the rtk/lean-ctx CLI context tools is based on this branch; this one is deliberately small so it can land first.
2026-07-30 20:55:47 -07:00
# The MCP install ledger defaults to ``~/.headroom/mcp_installs.json``, so any
# test that registers a server (directly or through `wrap`) writes into the
# developer's REAL ledger — observed adding a live `claude/serena` entry during a
# local run. Since the scrub above deletes HEADROOM_WORKSPACE_DIR, the default is
# always the real home. Redirect the ledger per-test instead: every writer
# (`record_install` / `clear_install` / `headroom_installed_matching`) resolves it
# through this module-global, so one patch covers them all. Patched here rather
# than pointing workspace_dir() at a tmp path, which would break the tests that
# assert the default workspace layout.
@pytest.fixture(autouse=True)
def _isolate_mcp_ledger(monkeypatch, tmp_path_factory):
# Same guard as _reset_copilot_routing_flag below: the macos/windows-native-
# wrapper CI jobs install only pytest and drive the installer shell scripts
# via subprocess, so headroom isn't importable and there is no ledger to
# redirect. Skip there instead of erroring at setup.
try:
from headroom.mcp_registry import ledger
except ModuleNotFoundError:
return
ledger_file = tmp_path_factory.mktemp("mcp-ledger") / "mcp_installs.json"
monkeypatch.setattr(ledger, "ledger_path", lambda: ledger_file)
feat(proxy): label GitHub Copilot traffic as "copilot" in the outcome… (#2377) ## Description Requests routed to the GitHub Copilot API travel on the OpenAI or Anthropic wire, so the proxy handlers stamp the *wire* provider (`openai` / `anthropic`) on the outcome. As a result, Copilot traffic is attributed to OpenAI/Claude in the dashboard's per-request provider stats, hiding the real upstream. (This is distinct from the existing **Copilot Quota** panel, which is separate from per-request provider attribution.) This labels Copilot traffic as `copilot` in the single outcome funnel. `build_copilot_upstream_url()` is already the one routing chokepoint every Copilot surface goes through (OpenAI `/chat/completions` + `/responses` and the Anthropic `/v1/messages` route all build their upstream URL there), so it flags the request via a task-local `ContextVar`; `emit_request_outcome()` reads the flag and relabels the provider. The relabel runs before the `>= 500` failed guard, so a failed Copilot request is attributed to `copilot` too. Non-Copilot traffic never sets the flag and is untouched. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `headroom/copilot_auth.py`: add a task-local `_request_routed_to_copilot` `ContextVar` with `mark_request_routed_to_copilot()` / `request_routed_to_copilot()` helpers; set the flag in `build_copilot_upstream_url()` whenever the base is a Copilot API URL (the existing `is_copilot_api_url` check). `/v1` path normalization is unchanged. - `headroom/proxy/outcome.py`: in `emit_request_outcome()`, when the request was routed to Copilot and the wire provider is `openai`/`anthropic`, relabel the outcome provider to `copilot` (before the 5xx guard). - `tests/test_copilot_provider_label.py`: new tests for the chokepoint marking and the outcome relabel. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) — ran on the changed files only (clean) - [ ] Type checking passes (`mypy headroom`) — ran on the changed files only (clean) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_copilot_provider_label.py tests/test_outcome_records_5xx_as_failed.py -q tests/test_copilot_provider_label.py ..... [ 71%] tests/test_outcome_records_5xx_as_failed.py .. [100%] 7 passed $ python -m pytest tests/test_copilot_auth.py -k "build_copilot_upstream_url or copilot_api_url" -q 8 passed, 58 deselected # existing /v1-stripping behavior preserved $ python -m ruff check headroom/copilot_auth.py headroom/proxy/outcome.py tests/test_copilot_provider_label.py All checks passed! $ python -m mypy headroom/copilot_auth.py headroom/proxy/outcome.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: Python 3.11, headroom installed with the `proxy` extra. - Exact command / steps: the unit tests above drive `build_copilot_upstream_url()` followed by `emit_request_outcome()` in an isolated context and assert the recorded provider. - Observed result: an `anthropic`/`openai` outcome for a request routed to `https://api.githubcopilot.com` is recorded as provider `copilot`; a request not routed to Copilot is recorded under its wire provider unchanged. - Not tested: end-to-end against a live Copilot subscription (no live seat in the test environment). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes - The flag is a `ContextVar` (task-local), so it cannot bleed across concurrent requests; each request that is not routed to Copilot simply reads the `False` default. - No `CHANGELOG.md` edits (release-please generates it from the Conventional Commit PR title). --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-18 18:54:06 +02:00
# The Copilot "routed to Copilot" flag is a module-global ContextVar that
# build_copilot_upstream_url() sets as a side effect. Unit tests that call that
# builder directly (or otherwise run in the shared root context) would leave it
# set and mislabel a later test's request outcome as "copilot". Reset it around
# every test so build-time side effects can't leak between tests.
@pytest.fixture(autouse=True)
def _reset_copilot_routing_flag():
test: make copilot-flag fixture tolerate headroom not installed (#2407) ## Description The autouse `_reset_copilot_routing_flag` fixture in `tests/conftest.py` did an unconditional `from headroom.copilot_auth import reset_request_routed_to_copilot` for **every** test. That import pulls in the whole package (`headroom/__init__` → `compress.py` → `observability` → `opentelemetry`). The `macos-native-wrapper` and `windows-native-wrapper` CI jobs run `tests/test_install/test_native_installers.py` with **only `pytest` installed** (see `.github/workflows/ci.yml` — those jobs `pip install pytest` and nothing else). Those tests drive the installer shell scripts via `subprocess` and never import headroom, so the autouse fixture errored at setup: ``` tests/conftest.py:40: in _reset_copilot_routing_flag from headroom.copilot_auth import reset_request_routed_to_copilot headroom/__init__.py:86: from .compress import ... headroom/compress.py:65: from .observability import get_otel_metrics headroom/observability/metrics.py:11: from opentelemetry import metrics E ModuleNotFoundError: No module named 'opentelemetry' ``` Guard the import: when headroom isn't importable there is no routing flag to reset, so the fixture is a no-op. No production code changes; behavior is unchanged whenever headroom is installed (all other jobs). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `tests/conftest.py`: wrap the `_reset_copilot_routing_flag` fixture's `headroom.copilot_auth` import in `try/except ModuleNotFoundError` → yield-and-return when headroom is absent. ## Testing - [x] Ran the exact CI command locally - [x] Linting passes (`ruff check`) ### Test Output ```text $ ruff check tests/conftest.py All checks passed! $ pytest tests/test_install/test_native_installers.py -q collected 2 items tests/test_install/test_native_installers.py ss [100%] ============================== 2 skipped in 0.11s ============================== ``` (2 skipped = Docker not available on the local box; the point is **no more "ERROR at setup"**. Before this change the same run reported `1 error in 0.11s` with the `opentelemetry` traceback above.) ## Real Behavior Proof - Environment: macOS, Python 3.12, headroom installed (normal path exercised). - Exact command / steps: `pytest tests/test_install/test_native_installers.py -q` - Observed result: no setup error; fixture takes the normal (headroom-present) path — 2 tests skipped for lack of Docker. - Not tested: the headroom-absent branch can't be reproduced locally (headroom is installed here); it is exactly the CI job's environment, which this PR's CI run will exercise. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes Scope is intentionally the native-wrapper failures only. The separate `test-dashboard-ui` red X is unrelated (a stale UI-text assertion: `element(s) not found — "Completed 128 Failed 0 Rate Limited 0 Cached 96"`) and is not addressed here. Checklist items about docs/CHANGELOG/new-tests are N/A — this is a test-harness resilience fix, not a behavior change.
2026-07-18 20:42:09 -07:00
# The macos/windows-native-wrapper CI jobs run the installer tests with only
# pytest installed (no headroom): they drive the installer shell scripts via
# subprocess, so headroom isn't importable and there's no routing flag to
# reset. Skip the reset there instead of erroring at setup.
try:
from headroom.copilot_auth import reset_request_routed_to_copilot
except ModuleNotFoundError:
yield
return
feat(proxy): label GitHub Copilot traffic as "copilot" in the outcome… (#2377) ## Description Requests routed to the GitHub Copilot API travel on the OpenAI or Anthropic wire, so the proxy handlers stamp the *wire* provider (`openai` / `anthropic`) on the outcome. As a result, Copilot traffic is attributed to OpenAI/Claude in the dashboard's per-request provider stats, hiding the real upstream. (This is distinct from the existing **Copilot Quota** panel, which is separate from per-request provider attribution.) This labels Copilot traffic as `copilot` in the single outcome funnel. `build_copilot_upstream_url()` is already the one routing chokepoint every Copilot surface goes through (OpenAI `/chat/completions` + `/responses` and the Anthropic `/v1/messages` route all build their upstream URL there), so it flags the request via a task-local `ContextVar`; `emit_request_outcome()` reads the flag and relabels the provider. The relabel runs before the `>= 500` failed guard, so a failed Copilot request is attributed to `copilot` too. Non-Copilot traffic never sets the flag and is untouched. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `headroom/copilot_auth.py`: add a task-local `_request_routed_to_copilot` `ContextVar` with `mark_request_routed_to_copilot()` / `request_routed_to_copilot()` helpers; set the flag in `build_copilot_upstream_url()` whenever the base is a Copilot API URL (the existing `is_copilot_api_url` check). `/v1` path normalization is unchanged. - `headroom/proxy/outcome.py`: in `emit_request_outcome()`, when the request was routed to Copilot and the wire provider is `openai`/`anthropic`, relabel the outcome provider to `copilot` (before the 5xx guard). - `tests/test_copilot_provider_label.py`: new tests for the chokepoint marking and the outcome relabel. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) — ran on the changed files only (clean) - [ ] Type checking passes (`mypy headroom`) — ran on the changed files only (clean) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_copilot_provider_label.py tests/test_outcome_records_5xx_as_failed.py -q tests/test_copilot_provider_label.py ..... [ 71%] tests/test_outcome_records_5xx_as_failed.py .. [100%] 7 passed $ python -m pytest tests/test_copilot_auth.py -k "build_copilot_upstream_url or copilot_api_url" -q 8 passed, 58 deselected # existing /v1-stripping behavior preserved $ python -m ruff check headroom/copilot_auth.py headroom/proxy/outcome.py tests/test_copilot_provider_label.py All checks passed! $ python -m mypy headroom/copilot_auth.py headroom/proxy/outcome.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: Python 3.11, headroom installed with the `proxy` extra. - Exact command / steps: the unit tests above drive `build_copilot_upstream_url()` followed by `emit_request_outcome()` in an isolated context and assert the recorded provider. - Observed result: an `anthropic`/`openai` outcome for a request routed to `https://api.githubcopilot.com` is recorded as provider `copilot`; a request not routed to Copilot is recorded under its wire provider unchanged. - Not tested: end-to-end against a live Copilot subscription (no live seat in the test environment). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes - The flag is a `ContextVar` (task-local), so it cannot bleed across concurrent requests; each request that is not routed to Copilot simply reads the `False` default. - No `CHANGELOG.md` edits (release-please generates it from the Conventional Commit PR title). --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-18 18:54:06 +02:00
reset_request_routed_to_copilot()
yield
reset_request_routed_to_copilot()
fix(proxy): cache litellm model resolution to stop repeated Provider List spam ## Description The proxy repeatedly prints LiteLLM's `Provider List: https://docs.litellm.ai/docs/providers` banner during normal operation, with no explanation or way to suppress it (#2851). Root cause: `_resolve_litellm_model()` in `headroom/proxy/savings_tracker.py` runs on every savings-tracking update (i.e. every request). For any model LiteLLM can't price (a custom/local/gateway model name — e.g. the reporter's local oMLX setup), the uncached fallback path calls `litellm.cost_per_token(...)` purely to probe resolvability. When that probe fails, LiteLLM prints the banner as an internal side effect before raising, and since the probe was never cached, it re-fires on every single request for the same unresolvable model. **Update:** review flagged that the first version of this fix cached into a plain, unbounded `dict` keyed by the (client-controlled) model name — a memory-retention path on a request-facing proxy, since a caller can grow it without limit by sending a new model string on every request. Replaced with a bounded `functools.lru_cache`; see Changes Made below. Closes #2851 ## 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/proxy/savings_tracker.py`: `_resolve_litellm_model()` is now decorated with `@lru_cache(maxsize=256)` instead of backing onto a hand-rolled unbounded `dict`. An evicted model name simply re-probes LiteLLM on next use — never a correctness issue, only whether the noisy failure banner reruns for that specific name. - `tests/conftest.py`: added a global `autouse` fixture, `_reset_litellm_model_resolution_cache`, that clears the cache before and after every test. It's process-lifetime and module-global, and several existing tests monkeypatch `savings_tracker.litellm` with different behavior per test while reusing common model names like `"gpt-4o"` — without a reset, whichever test resolves a name first silently wins that cache slot for the rest of the run and later tests stop exercising their own fake. - `tests/test_savings_tracker_litellm_resolution_cache.py` (new): regression tests for the three properties that actually matter — repeated resolution of one unknown model only probes LiteLLM once, resolving far more distinct names than the bound never grows the cache past it, and an evicted name is transparently re-probed rather than reusing a slot it no longer owns. - No behavior change for models LiteLLM can already price (fast path via `model_cost` lookup) — only the noisy uncached probe path is memoized, same as before. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) — not run; `mypy` isn't installed in this environment - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python3 -m pytest tests/test_proxy_savings_history.py tests/test_savings_tracker_zero_price.py \ tests/test_savings_tracker_litellm_resolution_cache.py -q tests/test_proxy_savings_history.py .................................... [ 73%] ... [ 79%] tests/test_savings_tracker_zero_price.py ....... [ 93%] tests/test_savings_tracker_litellm_resolution_cache.py ... [100%] 49 passed, 1 warning in 1.26s # Re-run in reversed file order to check for the exact order-dependence the # review flagged — same 49 passed, no failures either direction: $ python3 -m pytest tests/test_savings_tracker_litellm_resolution_cache.py \ tests/test_savings_tracker_zero_price.py tests/test_proxy_savings_history.py -q 49 passed, 1 warning in 1.11s $ python3 -m ruff check headroom/proxy/savings_tracker.py tests/conftest.py \ tests/test_savings_tracker_litellm_resolution_cache.py All checks passed! ``` ## Real Behavior Proof - Environment: macOS, Python 3.12.3, this repo checked out locally. - What changed since the last review pass: I got the compiled `headroom._core` Rust extension in hand (by installing the published `headroom-ai[all]` wheel into a separate venv and copying its `_core.abi3.so` next to this local source tree — same Python ABI, pure-Python edits in `savings_tracker.py` don't touch the compiled boundary). That unblocked the full test files this fix touches, including `tests/test_proxy_savings_history.py`, which was previously reported as untestable here. - Exact command / steps: three properties asserted directly against the real (now-bounded) cache in `tests/test_savings_tracker_litellm_resolution_cache.py`: 1. Resolve the same unresolvable model 5 times → assert the underlying `litellm.cost_per_token` probe fired exactly once. 2. Resolve `_MODEL_RESOLUTION_CACHE_MAXSIZE + 50` distinct model names → assert `_resolve_litellm_model.cache_info().currsize` stays at exactly `_MODEL_RESOLUTION_CACHE_MAXSIZE` (256), never higher — this is the actual memory-retention fix the review asked for. 3. Resolve one model, push exactly `maxsize` other distinct names through to evict it via LRU, then resolve it again → assert it re-probed (call count went 1 → 2), proving eviction is real and not just an untested cache_info number. - Observed result: all three pass; full affected-file suite (49 tests) passes in both forward and reversed run order, confirming the new `conftest.py` fixture actually fixes the cross-test leakage risk (verified by literally reordering the files, not just by inspection). - Not tested: a live HTTP request against a running `headroom proxy` process specifically re-exercising this bounded-cache commit — the earlier "20 simulated requests" proof against the previous (unbounded-dict) version of this fix was via a standalone script, not a real server; I have not repeated that specific live-server pass against this commit. The unit-level proof above exercises the exact same function (`_resolve_litellm_model`) the real proxy calls per-request from `headroom/proxy/server.py`, so I'm confident it generalizes, but flagging the gap rather than implying I re-ran it live. ## 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 — the bound/eviction rationale is commented above `_resolve_litellm_model`, and the cross-test leakage rationale is commented above the new `conftest.py` fixture - [ ] I have made corresponding changes to the documentation — N/A, internal implementation detail with no user-facing API/doc surface - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` ## Additional Notes - `mypy` still hasn't been run — not installed in this sandbox, and I didn't want to widen the PR further by installing/configuring it just for this. Flagging rather than silently skipping. - The earlier "Additional Notes" gap about `test_proxy_savings_history.py` being untestable in this environment is resolved (see Real Behavior Proof) — it now runs and passes, including the pre-existing `test_litellm_resolution_and_savings_estimation_fallbacks` test that exercises `_resolve_litellm_model` with a mutated `model_cost` dict across several assertions in one test. - Deliberately did not also bound `headroom/pricing/litellm_pricing.py`'s sibling `_resolved_model_cache` — same shape of cache, arguably the same exposure — since it's outside this PR's diff and touching it wasn't asked for. Flagging in case a maintainer wants it as a fast follow-up rather than silently leaving it unmentioned. --------- Co-authored-by: connectsudhindra-gif <connectsudhindra-gif@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 11:55:24 -05:00
# `savings_tracker._resolve_litellm_model` is an `lru_cache`d, module-global,
# process-lifetime cache keyed by model name (bounded — see #2860). Many test
# files monkeypatch `savings_tracker.litellm` to a fake with different
# `model_cost`/`cost_per_token` behavior per test, but reuse common model
# names like "gpt-4o" across them. Without a reset, whichever test resolves
# "gpt-4o" first "wins" the cache entry for the rest of the run, and later
# tests silently stop exercising their own fake — a real-not-hypothetical
# order-dependence bug once the cache is process-lifetime instead of per-call.
# Clear before AND after so a test's own within-test resolutions never leak
# in from, or leak out to, a neighboring test either.
@pytest.fixture(autouse=True)
def _reset_litellm_model_resolution_cache():
try:
from headroom.proxy.savings_tracker import _resolve_litellm_model
except ModuleNotFoundError:
yield
return
_resolve_litellm_model.cache_clear()
yield
_resolve_litellm_model.cache_clear()
# =============================================================================
# Global test hooks
# =============================================================================
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273) ## Description Anthropic request-side CCR can still compress a turn into retrieval markers after the frozen-prefix cache guard suppresses `headroom_retrieve` registration. That leaves the model with marker-only context it cannot redeem, so the proxy silently drops recoverable data on exactly the turns where cache preservation deferred tool injection. This change couples the Anthropic request-side CCR path to tool availability so a turn never emits retrieve-only markers without the retrieval tool, even when token mode or cache-mode prefix replay could otherwise reuse already-compressed marker text. Closes #1006 After a collaborator merged current `main` into this branch, CI also picked up unrelated offline-memory failures from the merged base. Those follow-up changes are test-only: they keep the offline Hugging Face cache lanes skipping cleanly instead of failing in memory tests that are outside the CCR runtime path. ## 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 - Couple Anthropic request-side CCR compression to the same frozen-prefix guard that already defers `headroom_retrieve` registration. - Keep the existing cache-preservation behavior: frozen-prefix turns stop emitting CCR retrieval markers instead of forcing tool injection into the cached prefix. - Make the skip decision use the effective frozen prefix after token-mode reclamping, so turns that genuinely reclamp to zero still keep normal reversible CCR behavior. - Bypass cached marker reuse in both token mode and cache-mode prefix replay when tool injection is deferred. - Add focused regressions for the Anthropic request-path seams under this bug: - frozen-prefix turns do not emit marker-only payloads - unfrozen turns still keep normal reversible CCR behavior - token-mode reclamp back to zero still compresses normally - existing `headroom_retrieve` tools keep reversible CCR on frozen turns - cache-mode delta reuse and exact-prefix replay both forward original content when retrieval is unavailable - Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic CCR behavior changes. - Add a shared test skip helper for offline Hugging Face cache misses and apply it to the merged `main` memory tests that were failing only in the offline CI shards after the branch picked up current `main`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py`) - [x] Linting passes (`uv run ruff check . && uv run ruff format . --check`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py 14 passed, 1 warning in 34.19s uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q 4 passed, 5 skipped, 11 warnings in 7.65s uv run ruff check . All checks passed! uv run ruff format . --check 968 files already formatted ``` ## Real Behavior Proof - Environment: local FastAPI `TestClient` for the Anthropic request path, plus Windows Python 3.12 offline-memory repros with `TRANSFORMERS_OFFLINE=1` - Exact command / steps: Run the focused CCR regression command and the offline-memory repro subset below on the merged branch state. - `uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py` - `uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q` - Scenario coverage from the CCR pytest command: - frozen prefix with deferred tool injection and cached marker text available in token mode - unfrozen turn with normal CCR marker emission - token mode where the tracked frozen prefix reclamps back to zero - frozen prefix where the client already supplied `headroom_retrieve` - cache-mode append-only delta reuse with a previously forwarded compressed prefix - cache-mode exact-prefix replay where the previous forwarded prefix already contained a marker - Scenario coverage from the offline-memory repro command: - direct local embedder startup on CPU with no cached HF model - hierarchical memory embedder startup with offline model cache missing - bridge import through `LocalBackend` - `MemoryHandler` public init warmup path - `LocalBackend` save path under the offline lane - Observed result: The CCR regression keeps marker-free forwarding on frozen turns without tool availability, and the merged-`main` offline-memory lanes now skip cleanly instead of failing unrelated CI shards. - frozen-prefix Anthropic turns without tool availability forward the original long transcript across both token-mode and cache-mode reuse paths, while unfrozen turns, reclamped token-mode turns, and frozen turns that already advertise `headroom_retrieve` keep the reversible CCR marker path - the merged-`main` offline-memory regressions now skip cleanly when the Hugging Face cache is unavailable instead of failing unrelated CI shards - Not tested: live Zed session cache-hit behavior, provider latency under real Anthropic upstreams, and online Hugging Face download lanes ## 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 - [x] 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Runtime scope is still intentionally narrow to Anthropic request-side CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are unchanged. - The only non-CCR diff is the test-only offline-memory follow-up required after current `main` was merged into the branch. - The focused proxy pytest run still emits the Windows-local `StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx` bridge. The offline-memory repro command also emits existing datetime deprecation warnings and a pytest teardown warning around skipped offline lanes; none of those warnings were introduced by the CCR runtime change.
2026-06-23 13:48:05 -04:00
"""Wrap test execution to skip transient or offline external model failures.
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273) ## Description Anthropic request-side CCR can still compress a turn into retrieval markers after the frozen-prefix cache guard suppresses `headroom_retrieve` registration. That leaves the model with marker-only context it cannot redeem, so the proxy silently drops recoverable data on exactly the turns where cache preservation deferred tool injection. This change couples the Anthropic request-side CCR path to tool availability so a turn never emits retrieve-only markers without the retrieval tool, even when token mode or cache-mode prefix replay could otherwise reuse already-compressed marker text. Closes #1006 After a collaborator merged current `main` into this branch, CI also picked up unrelated offline-memory failures from the merged base. Those follow-up changes are test-only: they keep the offline Hugging Face cache lanes skipping cleanly instead of failing in memory tests that are outside the CCR runtime path. ## 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 - Couple Anthropic request-side CCR compression to the same frozen-prefix guard that already defers `headroom_retrieve` registration. - Keep the existing cache-preservation behavior: frozen-prefix turns stop emitting CCR retrieval markers instead of forcing tool injection into the cached prefix. - Make the skip decision use the effective frozen prefix after token-mode reclamping, so turns that genuinely reclamp to zero still keep normal reversible CCR behavior. - Bypass cached marker reuse in both token mode and cache-mode prefix replay when tool injection is deferred. - Add focused regressions for the Anthropic request-path seams under this bug: - frozen-prefix turns do not emit marker-only payloads - unfrozen turns still keep normal reversible CCR behavior - token-mode reclamp back to zero still compresses normally - existing `headroom_retrieve` tools keep reversible CCR on frozen turns - cache-mode delta reuse and exact-prefix replay both forward original content when retrieval is unavailable - Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic CCR behavior changes. - Add a shared test skip helper for offline Hugging Face cache misses and apply it to the merged `main` memory tests that were failing only in the offline CI shards after the branch picked up current `main`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py`) - [x] Linting passes (`uv run ruff check . && uv run ruff format . --check`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py 14 passed, 1 warning in 34.19s uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q 4 passed, 5 skipped, 11 warnings in 7.65s uv run ruff check . All checks passed! uv run ruff format . --check 968 files already formatted ``` ## Real Behavior Proof - Environment: local FastAPI `TestClient` for the Anthropic request path, plus Windows Python 3.12 offline-memory repros with `TRANSFORMERS_OFFLINE=1` - Exact command / steps: Run the focused CCR regression command and the offline-memory repro subset below on the merged branch state. - `uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py` - `uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q` - Scenario coverage from the CCR pytest command: - frozen prefix with deferred tool injection and cached marker text available in token mode - unfrozen turn with normal CCR marker emission - token mode where the tracked frozen prefix reclamps back to zero - frozen prefix where the client already supplied `headroom_retrieve` - cache-mode append-only delta reuse with a previously forwarded compressed prefix - cache-mode exact-prefix replay where the previous forwarded prefix already contained a marker - Scenario coverage from the offline-memory repro command: - direct local embedder startup on CPU with no cached HF model - hierarchical memory embedder startup with offline model cache missing - bridge import through `LocalBackend` - `MemoryHandler` public init warmup path - `LocalBackend` save path under the offline lane - Observed result: The CCR regression keeps marker-free forwarding on frozen turns without tool availability, and the merged-`main` offline-memory lanes now skip cleanly instead of failing unrelated CI shards. - frozen-prefix Anthropic turns without tool availability forward the original long transcript across both token-mode and cache-mode reuse paths, while unfrozen turns, reclamped token-mode turns, and frozen turns that already advertise `headroom_retrieve` keep the reversible CCR marker path - the merged-`main` offline-memory regressions now skip cleanly when the Hugging Face cache is unavailable instead of failing unrelated CI shards - Not tested: live Zed session cache-hit behavior, provider latency under real Anthropic upstreams, and online Hugging Face download lanes ## 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 - [x] 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Runtime scope is still intentionally narrow to Anthropic request-side CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are unchanged. - The only non-CCR diff is the test-only offline-memory follow-up required after current `main` was merged into the branch. - The focused proxy pytest run still emits the Windows-local `StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx` bridge. The offline-memory repro command also emits existing datetime deprecation warnings and a pytest teardown warning around skipped offline lanes; none of those warnings were introduced by the CCR runtime change.
2026-06-23 13:48:05 -04:00
This handles model-loading failures that occur when:
- HuggingFace Hub is slow during model downloads (sentence-transformers)
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273) ## Description Anthropic request-side CCR can still compress a turn into retrieval markers after the frozen-prefix cache guard suppresses `headroom_retrieve` registration. That leaves the model with marker-only context it cannot redeem, so the proxy silently drops recoverable data on exactly the turns where cache preservation deferred tool injection. This change couples the Anthropic request-side CCR path to tool availability so a turn never emits retrieve-only markers without the retrieval tool, even when token mode or cache-mode prefix replay could otherwise reuse already-compressed marker text. Closes #1006 After a collaborator merged current `main` into this branch, CI also picked up unrelated offline-memory failures from the merged base. Those follow-up changes are test-only: they keep the offline Hugging Face cache lanes skipping cleanly instead of failing in memory tests that are outside the CCR runtime path. ## 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 - Couple Anthropic request-side CCR compression to the same frozen-prefix guard that already defers `headroom_retrieve` registration. - Keep the existing cache-preservation behavior: frozen-prefix turns stop emitting CCR retrieval markers instead of forcing tool injection into the cached prefix. - Make the skip decision use the effective frozen prefix after token-mode reclamping, so turns that genuinely reclamp to zero still keep normal reversible CCR behavior. - Bypass cached marker reuse in both token mode and cache-mode prefix replay when tool injection is deferred. - Add focused regressions for the Anthropic request-path seams under this bug: - frozen-prefix turns do not emit marker-only payloads - unfrozen turns still keep normal reversible CCR behavior - token-mode reclamp back to zero still compresses normally - existing `headroom_retrieve` tools keep reversible CCR on frozen turns - cache-mode delta reuse and exact-prefix replay both forward original content when retrieval is unavailable - Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic CCR behavior changes. - Add a shared test skip helper for offline Hugging Face cache misses and apply it to the merged `main` memory tests that were failing only in the offline CI shards after the branch picked up current `main`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py`) - [x] Linting passes (`uv run ruff check . && uv run ruff format . --check`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py 14 passed, 1 warning in 34.19s uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q 4 passed, 5 skipped, 11 warnings in 7.65s uv run ruff check . All checks passed! uv run ruff format . --check 968 files already formatted ``` ## Real Behavior Proof - Environment: local FastAPI `TestClient` for the Anthropic request path, plus Windows Python 3.12 offline-memory repros with `TRANSFORMERS_OFFLINE=1` - Exact command / steps: Run the focused CCR regression command and the offline-memory repro subset below on the merged branch state. - `uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py` - `uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q` - Scenario coverage from the CCR pytest command: - frozen prefix with deferred tool injection and cached marker text available in token mode - unfrozen turn with normal CCR marker emission - token mode where the tracked frozen prefix reclamps back to zero - frozen prefix where the client already supplied `headroom_retrieve` - cache-mode append-only delta reuse with a previously forwarded compressed prefix - cache-mode exact-prefix replay where the previous forwarded prefix already contained a marker - Scenario coverage from the offline-memory repro command: - direct local embedder startup on CPU with no cached HF model - hierarchical memory embedder startup with offline model cache missing - bridge import through `LocalBackend` - `MemoryHandler` public init warmup path - `LocalBackend` save path under the offline lane - Observed result: The CCR regression keeps marker-free forwarding on frozen turns without tool availability, and the merged-`main` offline-memory lanes now skip cleanly instead of failing unrelated CI shards. - frozen-prefix Anthropic turns without tool availability forward the original long transcript across both token-mode and cache-mode reuse paths, while unfrozen turns, reclamped token-mode turns, and frozen turns that already advertise `headroom_retrieve` keep the reversible CCR marker path - the merged-`main` offline-memory regressions now skip cleanly when the Hugging Face cache is unavailable instead of failing unrelated CI shards - Not tested: live Zed session cache-hit behavior, provider latency under real Anthropic upstreams, and online Hugging Face download lanes ## 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 - [x] 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Runtime scope is still intentionally narrow to Anthropic request-side CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are unchanged. - The only non-CCR diff is the test-only offline-memory follow-up required after current `main` was merged into the branch. - The focused proxy pytest run still emits the Windows-local `StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx` bridge. The offline-memory repro command also emits existing datetime deprecation warnings and a pytest teardown warning around skipped offline lanes; none of those warnings were introduced by the CCR runtime change.
2026-06-23 13:48:05 -04:00
- Required HuggingFace model files were not restored into the offline CI cache
- External embedding APIs timeout
- Network connectivity issues in CI
"""
outcome = yield
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273) ## Description Anthropic request-side CCR can still compress a turn into retrieval markers after the frozen-prefix cache guard suppresses `headroom_retrieve` registration. That leaves the model with marker-only context it cannot redeem, so the proxy silently drops recoverable data on exactly the turns where cache preservation deferred tool injection. This change couples the Anthropic request-side CCR path to tool availability so a turn never emits retrieve-only markers without the retrieval tool, even when token mode or cache-mode prefix replay could otherwise reuse already-compressed marker text. Closes #1006 After a collaborator merged current `main` into this branch, CI also picked up unrelated offline-memory failures from the merged base. Those follow-up changes are test-only: they keep the offline Hugging Face cache lanes skipping cleanly instead of failing in memory tests that are outside the CCR runtime path. ## 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 - Couple Anthropic request-side CCR compression to the same frozen-prefix guard that already defers `headroom_retrieve` registration. - Keep the existing cache-preservation behavior: frozen-prefix turns stop emitting CCR retrieval markers instead of forcing tool injection into the cached prefix. - Make the skip decision use the effective frozen prefix after token-mode reclamping, so turns that genuinely reclamp to zero still keep normal reversible CCR behavior. - Bypass cached marker reuse in both token mode and cache-mode prefix replay when tool injection is deferred. - Add focused regressions for the Anthropic request-path seams under this bug: - frozen-prefix turns do not emit marker-only payloads - unfrozen turns still keep normal reversible CCR behavior - token-mode reclamp back to zero still compresses normally - existing `headroom_retrieve` tools keep reversible CCR on frozen turns - cache-mode delta reuse and exact-prefix replay both forward original content when retrieval is unavailable - Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic CCR behavior changes. - Add a shared test skip helper for offline Hugging Face cache misses and apply it to the merged `main` memory tests that were failing only in the offline CI shards after the branch picked up current `main`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py`) - [x] Linting passes (`uv run ruff check . && uv run ruff format . --check`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py 14 passed, 1 warning in 34.19s uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q 4 passed, 5 skipped, 11 warnings in 7.65s uv run ruff check . All checks passed! uv run ruff format . --check 968 files already formatted ``` ## Real Behavior Proof - Environment: local FastAPI `TestClient` for the Anthropic request path, plus Windows Python 3.12 offline-memory repros with `TRANSFORMERS_OFFLINE=1` - Exact command / steps: Run the focused CCR regression command and the offline-memory repro subset below on the merged branch state. - `uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py` - `uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q` - Scenario coverage from the CCR pytest command: - frozen prefix with deferred tool injection and cached marker text available in token mode - unfrozen turn with normal CCR marker emission - token mode where the tracked frozen prefix reclamps back to zero - frozen prefix where the client already supplied `headroom_retrieve` - cache-mode append-only delta reuse with a previously forwarded compressed prefix - cache-mode exact-prefix replay where the previous forwarded prefix already contained a marker - Scenario coverage from the offline-memory repro command: - direct local embedder startup on CPU with no cached HF model - hierarchical memory embedder startup with offline model cache missing - bridge import through `LocalBackend` - `MemoryHandler` public init warmup path - `LocalBackend` save path under the offline lane - Observed result: The CCR regression keeps marker-free forwarding on frozen turns without tool availability, and the merged-`main` offline-memory lanes now skip cleanly instead of failing unrelated CI shards. - frozen-prefix Anthropic turns without tool availability forward the original long transcript across both token-mode and cache-mode reuse paths, while unfrozen turns, reclamped token-mode turns, and frozen turns that already advertise `headroom_retrieve` keep the reversible CCR marker path - the merged-`main` offline-memory regressions now skip cleanly when the Hugging Face cache is unavailable instead of failing unrelated CI shards - Not tested: live Zed session cache-hit behavior, provider latency under real Anthropic upstreams, and online Hugging Face download lanes ## 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 - [x] 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Runtime scope is still intentionally narrow to Anthropic request-side CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are unchanged. - The only non-CCR diff is the test-only offline-memory follow-up required after current `main` was merged into the branch. - The focused proxy pytest run still emits the Windows-local `StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx` bridge. The offline-memory repro command also emits existing datetime deprecation warnings and a pytest teardown warning around skipped offline lanes; none of those warnings were introduced by the CCR runtime change.
2026-06-23 13:48:05 -04:00
if outcome.excinfo is not None:
exc_type, exc_value, exc_tb = outcome.excinfo
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273) ## Description Anthropic request-side CCR can still compress a turn into retrieval markers after the frozen-prefix cache guard suppresses `headroom_retrieve` registration. That leaves the model with marker-only context it cannot redeem, so the proxy silently drops recoverable data on exactly the turns where cache preservation deferred tool injection. This change couples the Anthropic request-side CCR path to tool availability so a turn never emits retrieve-only markers without the retrieval tool, even when token mode or cache-mode prefix replay could otherwise reuse already-compressed marker text. Closes #1006 After a collaborator merged current `main` into this branch, CI also picked up unrelated offline-memory failures from the merged base. Those follow-up changes are test-only: they keep the offline Hugging Face cache lanes skipping cleanly instead of failing in memory tests that are outside the CCR runtime path. ## 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 - Couple Anthropic request-side CCR compression to the same frozen-prefix guard that already defers `headroom_retrieve` registration. - Keep the existing cache-preservation behavior: frozen-prefix turns stop emitting CCR retrieval markers instead of forcing tool injection into the cached prefix. - Make the skip decision use the effective frozen prefix after token-mode reclamping, so turns that genuinely reclamp to zero still keep normal reversible CCR behavior. - Bypass cached marker reuse in both token mode and cache-mode prefix replay when tool injection is deferred. - Add focused regressions for the Anthropic request-path seams under this bug: - frozen-prefix turns do not emit marker-only payloads - unfrozen turns still keep normal reversible CCR behavior - token-mode reclamp back to zero still compresses normally - existing `headroom_retrieve` tools keep reversible CCR on frozen turns - cache-mode delta reuse and exact-prefix replay both forward original content when retrieval is unavailable - Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic CCR behavior changes. - Add a shared test skip helper for offline Hugging Face cache misses and apply it to the merged `main` memory tests that were failing only in the offline CI shards after the branch picked up current `main`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py`) - [x] Linting passes (`uv run ruff check . && uv run ruff format . --check`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py 14 passed, 1 warning in 34.19s uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q 4 passed, 5 skipped, 11 warnings in 7.65s uv run ruff check . All checks passed! uv run ruff format . --check 968 files already formatted ``` ## Real Behavior Proof - Environment: local FastAPI `TestClient` for the Anthropic request path, plus Windows Python 3.12 offline-memory repros with `TRANSFORMERS_OFFLINE=1` - Exact command / steps: Run the focused CCR regression command and the offline-memory repro subset below on the merged branch state. - `uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py` - `uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q` - Scenario coverage from the CCR pytest command: - frozen prefix with deferred tool injection and cached marker text available in token mode - unfrozen turn with normal CCR marker emission - token mode where the tracked frozen prefix reclamps back to zero - frozen prefix where the client already supplied `headroom_retrieve` - cache-mode append-only delta reuse with a previously forwarded compressed prefix - cache-mode exact-prefix replay where the previous forwarded prefix already contained a marker - Scenario coverage from the offline-memory repro command: - direct local embedder startup on CPU with no cached HF model - hierarchical memory embedder startup with offline model cache missing - bridge import through `LocalBackend` - `MemoryHandler` public init warmup path - `LocalBackend` save path under the offline lane - Observed result: The CCR regression keeps marker-free forwarding on frozen turns without tool availability, and the merged-`main` offline-memory lanes now skip cleanly instead of failing unrelated CI shards. - frozen-prefix Anthropic turns without tool availability forward the original long transcript across both token-mode and cache-mode reuse paths, while unfrozen turns, reclamped token-mode turns, and frozen turns that already advertise `headroom_retrieve` keep the reversible CCR marker path - the merged-`main` offline-memory regressions now skip cleanly when the Hugging Face cache is unavailable instead of failing unrelated CI shards - Not tested: live Zed session cache-hit behavior, provider latency under real Anthropic upstreams, and online Hugging Face download lanes ## 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 - [x] 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Runtime scope is still intentionally narrow to Anthropic request-side CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are unchanged. - The only non-CCR diff is the test-only offline-memory follow-up required after current `main` was merged into the branch. - The focused proxy pytest run still emits the Windows-local `StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx` bridge. The offline-memory repro command also emits existing datetime deprecation warnings and a pytest teardown warning around skipped offline lanes; none of those warnings were introduced by the CCR runtime change.
2026-06-23 13:48:05 -04:00
reason = external_model_skip_reason(exc_value)
if reason is not None:
pytest.skip(reason)
@pytest.fixture(autouse=True)
def _null_binary_pins():
"""Null the tools.json SHA-256 pins during tests.
Installer tests fetch small mock archives, whose digests can't match the
real published pins. Nulling the pins lets those download/extract mechanics
tests run (verification then falls back to HTTPS trust); the tests that
specifically exercise verification set their own pin explicitly. Production
keeps the real pins (this fixture is test-only) and the tools-hash-refresh
CI gate guarantees they stay correct.
"""
try:
from headroom import binaries
except Exception:
# Lean CI environments (e.g. the native-installer jobs) omit heavy deps
# such as opentelemetry that importing `binaries` pulls in. There are no
# tool pins to null there, so skip cleanly rather than erroring at setup.
yield
return
saved = [
(asset, asset.get("sha256"))
for tool in binaries._registry().get("tools", {}).values()
for asset in tool.get("assets", {}).values()
]
for asset, _original in saved:
asset["sha256"] = None
yield
for asset, original in saved:
asset["sha256"] = original
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
@pytest.fixture(autouse=True)
def _reset_headroom_logger_propagation():
"""Keep `headroom.*` log records flowing to pytest's caplog handler.
fix(tests): reset whole headroom logger subtree so caplog stays deterministic (#1117) ## Description Five `caplog`-based test assertions are order-dependent flakes: they pass in isolation but fail in full-suite runs. **Root cause** is a global logging-state leak. `benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging()` (exercised by `tests/test_claude_session_mode_benchmark.py`) sets `propagate = False` + `CRITICAL` on the `headroom`, `headroom.proxy`, `headroom.transforms` (and `headroom.cache`) loggers and never restores them. pytest's `caplog` attaches its handler to the **root** logger, so once any `headroom.*` child is left non-propagating, records from that subtree silently never reach `caplog` for **every test that runs afterwards** — which is exactly why these only fail in full-suite order. The repo already ships a `_reset_headroom_logger_propagation` autouse fixture for this hazard (its docstring documents the equivalent `_setup_file_logging` leak), but it only reset the **top** `headroom` logger, not children like `headroom.proxy`. A non-propagating child still blocks the record before it reaches root. This PR extends the existing fixture to reset the whole `headroom.*` subtree before each test. Scope is intentionally one file (`tests/conftest.py`) — test-harness only, no production change. > Design note: I extended the existing defensive fixture rather than restoring state inside the benchmark, because (a) the fixture already exists for exactly this and only needed completing, and (b) the same `propagate=False` hazard also originates from production `_setup_file_logging`, so a centralized per-test reset is the more durable fix. Happy to instead make the benchmark restore its own logging state if maintainers prefer fixing it at the source. ## 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 - Extend the `_reset_headroom_logger_propagation` autouse fixture in `tests/conftest.py` to reset `propagate = True` for **every** existing `headroom.*` logger (previously only the top `headroom` logger), so `caplog` capture is deterministic regardless of test execution order. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) — N/A, change is under `tests/` - [ ] New tests added — N/A, this fixes existing tests; they are themselves the proof - [x] Manual testing performed ### Test Output ```text # Causal proof: run the polluter first, then the 5 victims, in one process. # BEFORE (fixture reset scoped to only "headroom"): $ pytest tests/test_claude_session_mode_benchmark.py \ tests/test_corrupt_golden_bytes_recovery.py \ tests/test_forwarded_headers.py \ 'tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto' 5 failed, 54 passed FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_corrupt_bytes_logs_error FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_unicode_decode_error_handled FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptCcrGoldenBytes::test_corrupt_bytes_logs_error FAILED tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs FAILED tests/test_transforms/test_kompress_compressor.py::...test_unrecognized_backend_warns_and_falls_back_to_auto # AFTER (this PR — whole headroom.* subtree reset): $ pytest <same selection> 59 passed # Full suite (Rust core rebuilt locally): $ pytest 6251 passed, 496 skipped $ ruff check . All checks passed! $ ruff format --check tests/conftest.py 1 file already formatted ``` ## Real Behavior Proof - Environment: macOS, Python 3.13.3, branch off latest `main`, Rust `_core` rebuilt locally (`uv pip install -e .`). - Exact command / steps: ran the polluter (`test_claude_session_mode_benchmark`) together with the 5 victim tests in one process to reproduce the order-dependent failure, then toggled **only** the fixture change to confirm causality; then ran the full `pytest` suite and `ruff`. - Observed result: scoping the reset to only `"headroom"` → 5 failed / 54 passed; extending it to the `headroom.*` subtree → 59 passed. Full suite: 6251 passed, 496 skipped, 0 failed. Lint clean. - Not tested: behavior under CI's sharded `test (N)` jobs specifically — but the fix is order-independent (resets before *every* test), so sharding cannot reintroduce the leak. ## 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective — the 5 previously-flaky tests are the proof - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (test-harness only) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-27 01:05:26 +08:00
Two sources disable propagation on the headroom logger tree and never
restore it, which then makes later `caplog`-based assertions flaky in
full-suite runs (caplog attaches to root, so a `propagate=False` anywhere
on the chain silently drops the records):
- ``headroom.proxy.helpers._setup_file_logging`` sets
``getLogger("headroom").propagate = False`` on proxy startup.
- ``benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging``
(exercised by ``test_claude_session_mode_benchmark``) sets
``propagate = False`` + ``CRITICAL`` on ``headroom``, ``headroom.proxy``,
``headroom.transforms``, ``headroom.cache`` (and children).
Resetting only ``"headroom"`` is not enough a child like
``"headroom.proxy"`` left non-propagating blocks the record before it
reaches root. Reset the whole subtree before every test so capture is
deterministic regardless of run order.
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
"""
import logging as _logging
fix(tests): reset whole headroom logger subtree so caplog stays deterministic (#1117) ## Description Five `caplog`-based test assertions are order-dependent flakes: they pass in isolation but fail in full-suite runs. **Root cause** is a global logging-state leak. `benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging()` (exercised by `tests/test_claude_session_mode_benchmark.py`) sets `propagate = False` + `CRITICAL` on the `headroom`, `headroom.proxy`, `headroom.transforms` (and `headroom.cache`) loggers and never restores them. pytest's `caplog` attaches its handler to the **root** logger, so once any `headroom.*` child is left non-propagating, records from that subtree silently never reach `caplog` for **every test that runs afterwards** — which is exactly why these only fail in full-suite order. The repo already ships a `_reset_headroom_logger_propagation` autouse fixture for this hazard (its docstring documents the equivalent `_setup_file_logging` leak), but it only reset the **top** `headroom` logger, not children like `headroom.proxy`. A non-propagating child still blocks the record before it reaches root. This PR extends the existing fixture to reset the whole `headroom.*` subtree before each test. Scope is intentionally one file (`tests/conftest.py`) — test-harness only, no production change. > Design note: I extended the existing defensive fixture rather than restoring state inside the benchmark, because (a) the fixture already exists for exactly this and only needed completing, and (b) the same `propagate=False` hazard also originates from production `_setup_file_logging`, so a centralized per-test reset is the more durable fix. Happy to instead make the benchmark restore its own logging state if maintainers prefer fixing it at the source. ## 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 - Extend the `_reset_headroom_logger_propagation` autouse fixture in `tests/conftest.py` to reset `propagate = True` for **every** existing `headroom.*` logger (previously only the top `headroom` logger), so `caplog` capture is deterministic regardless of test execution order. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) — N/A, change is under `tests/` - [ ] New tests added — N/A, this fixes existing tests; they are themselves the proof - [x] Manual testing performed ### Test Output ```text # Causal proof: run the polluter first, then the 5 victims, in one process. # BEFORE (fixture reset scoped to only "headroom"): $ pytest tests/test_claude_session_mode_benchmark.py \ tests/test_corrupt_golden_bytes_recovery.py \ tests/test_forwarded_headers.py \ 'tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto' 5 failed, 54 passed FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_corrupt_bytes_logs_error FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_unicode_decode_error_handled FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptCcrGoldenBytes::test_corrupt_bytes_logs_error FAILED tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs FAILED tests/test_transforms/test_kompress_compressor.py::...test_unrecognized_backend_warns_and_falls_back_to_auto # AFTER (this PR — whole headroom.* subtree reset): $ pytest <same selection> 59 passed # Full suite (Rust core rebuilt locally): $ pytest 6251 passed, 496 skipped $ ruff check . All checks passed! $ ruff format --check tests/conftest.py 1 file already formatted ``` ## Real Behavior Proof - Environment: macOS, Python 3.13.3, branch off latest `main`, Rust `_core` rebuilt locally (`uv pip install -e .`). - Exact command / steps: ran the polluter (`test_claude_session_mode_benchmark`) together with the 5 victim tests in one process to reproduce the order-dependent failure, then toggled **only** the fixture change to confirm causality; then ran the full `pytest` suite and `ruff`. - Observed result: scoping the reset to only `"headroom"` → 5 failed / 54 passed; extending it to the `headroom.*` subtree → 59 passed. Full suite: 6251 passed, 496 skipped, 0 failed. Lint clean. - Not tested: behavior under CI's sharded `test (N)` jobs specifically — but the fix is order-independent (resets before *every* test), so sharding cannot reintroduce the leak. ## 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective — the 5 previously-flaky tests are the proof - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (test-harness only) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-27 01:05:26 +08:00
for _name in ("headroom", *list(_logging.root.manager.loggerDict)):
if _name == "headroom" or _name.startswith("headroom."):
logger = _logging.getLogger(_name)
logger.disabled = False
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189) ## Description Running Claude Code (Anthropic) and Codex (OpenAI) against the **same** Headroom proxy instance on one port produced incorrect, unstable dashboard data. The proxy core is provider-isolated and multi-provider-safe by design; the defect was in the observability layer. The Codex `/v1/responses` **WebSocket** handler was the only path in the proxy that wrote to the request logger by hand instead of through the unified `emit_request_outcome` funnel, and it did so twice per session close: the per-turn funnel record plus an unconditional cumulative session-summary `RequestLog`. This PR removes the duplicate summary log so Codex WS emits exactly one request log per turn, matching the HTTP provider paths. ## 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 - Dropped the duplicate cumulative session-summary `RequestLog` in the Codex WS handler while preserving the per-turn `emit_request_outcome` path. - Preserved gated `request_messages` and `turn_id` on residual outcomes so dashboard telemetry keeps the useful attribution without double-counting tokens. - Ensured explicit `--anyllm-provider` wins over a leaked `HEADROOM_ANYLLM_PROVIDER` environment variable. - Registered retry delay settings that had drifted out of the settings registry. - Hardened tests against developer-shell `HEADROOM_*` / `ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused proxy/wrap test fixtures. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/pytest tests/ -q -p no:cacheprovider 8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36) $ .venv/bin/ruff check <touched files> All checks passed! ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff via project venv, branch `fix/multi-provider-runtime`. - Exact command / steps: Ran the full test suite without pytest cache provider and Ruff on all touched files; used `git stash` to confirm the stale fake-config failures pre-existed this change. - Observed result: Full suite passed with no failures; Ruff passed; Codex WS now routes end-of-session logging through `emit_request_outcome`, emitting one request log per turn with the same accounting model as Anthropic HTTP turns. - Not tested: Live simultaneous Claude + Codex dashboard run. `mypy headroom` was not run to completion; a scoped run reported one pre-existing `settings_store.py:470` coercion error outside this diff. ## 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 - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - server-side observability fix; no UI markup changed. ## Additional Notes - The proxy's multi-provider routing, header/auth isolation, and per-model cache keying are already correct and unchanged here; only the WS observability write path was double-counting. - Architectural assessment: `plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`; root-cause + resolution trail: `plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`. - No live simultaneous Claude + Codex dashboard run was performed; validation is from test coverage and code review of the WS logging path. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-16 01:18:34 +07:00
# The benchmark also raises the level to CRITICAL; children
# inherit it (effective level), so a WARNING would be filtered
# at the logger before it can propagate to caplog. Reset to
# NOTSET so the subtree inherits root's level deterministically.
logger.setLevel(_logging.NOTSET)
fix(tests): reset whole headroom logger subtree so caplog stays deterministic (#1117) ## Description Five `caplog`-based test assertions are order-dependent flakes: they pass in isolation but fail in full-suite runs. **Root cause** is a global logging-state leak. `benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging()` (exercised by `tests/test_claude_session_mode_benchmark.py`) sets `propagate = False` + `CRITICAL` on the `headroom`, `headroom.proxy`, `headroom.transforms` (and `headroom.cache`) loggers and never restores them. pytest's `caplog` attaches its handler to the **root** logger, so once any `headroom.*` child is left non-propagating, records from that subtree silently never reach `caplog` for **every test that runs afterwards** — which is exactly why these only fail in full-suite order. The repo already ships a `_reset_headroom_logger_propagation` autouse fixture for this hazard (its docstring documents the equivalent `_setup_file_logging` leak), but it only reset the **top** `headroom` logger, not children like `headroom.proxy`. A non-propagating child still blocks the record before it reaches root. This PR extends the existing fixture to reset the whole `headroom.*` subtree before each test. Scope is intentionally one file (`tests/conftest.py`) — test-harness only, no production change. > Design note: I extended the existing defensive fixture rather than restoring state inside the benchmark, because (a) the fixture already exists for exactly this and only needed completing, and (b) the same `propagate=False` hazard also originates from production `_setup_file_logging`, so a centralized per-test reset is the more durable fix. Happy to instead make the benchmark restore its own logging state if maintainers prefer fixing it at the source. ## 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 - Extend the `_reset_headroom_logger_propagation` autouse fixture in `tests/conftest.py` to reset `propagate = True` for **every** existing `headroom.*` logger (previously only the top `headroom` logger), so `caplog` capture is deterministic regardless of test execution order. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) — N/A, change is under `tests/` - [ ] New tests added — N/A, this fixes existing tests; they are themselves the proof - [x] Manual testing performed ### Test Output ```text # Causal proof: run the polluter first, then the 5 victims, in one process. # BEFORE (fixture reset scoped to only "headroom"): $ pytest tests/test_claude_session_mode_benchmark.py \ tests/test_corrupt_golden_bytes_recovery.py \ tests/test_forwarded_headers.py \ 'tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto' 5 failed, 54 passed FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_corrupt_bytes_logs_error FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_unicode_decode_error_handled FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptCcrGoldenBytes::test_corrupt_bytes_logs_error FAILED tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs FAILED tests/test_transforms/test_kompress_compressor.py::...test_unrecognized_backend_warns_and_falls_back_to_auto # AFTER (this PR — whole headroom.* subtree reset): $ pytest <same selection> 59 passed # Full suite (Rust core rebuilt locally): $ pytest 6251 passed, 496 skipped $ ruff check . All checks passed! $ ruff format --check tests/conftest.py 1 file already formatted ``` ## Real Behavior Proof - Environment: macOS, Python 3.13.3, branch off latest `main`, Rust `_core` rebuilt locally (`uv pip install -e .`). - Exact command / steps: ran the polluter (`test_claude_session_mode_benchmark`) together with the 5 victim tests in one process to reproduce the order-dependent failure, then toggled **only** the fixture change to confirm causality; then ran the full `pytest` suite and `ruff`. - Observed result: scoping the reset to only `"headroom"` → 5 failed / 54 passed; extending it to the `headroom.*` subtree → 59 passed. Full suite: 6251 passed, 496 skipped, 0 failed. Lint clean. - Not tested: behavior under CI's sharded `test (N)` jobs specifically — but the fix is order-independent (resets before *every* test), so sharding cannot reintroduce the leak. ## 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective — the 5 previously-flaky tests are the proof - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (test-harness only) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-27 01:05:26 +08:00
logger.propagate = True
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
yield
# =============================================================================
# Sample messages fixtures
# =============================================================================
# Sample messages fixtures
@pytest.fixture
def sample_messages():
"""Basic conversation messages."""
return [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello, how are you?"},
{"role": "assistant", "content": "I'm doing well, thank you!"},
]
@pytest.fixture
def sample_messages_with_tools():
"""Conversation with tool calls and responses."""
return [
{"role": "system", "content": "You are a helpful assistant with tools."},
{"role": "user", "content": "Search for user 12345"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {"name": "search_user", "arguments": '{"user_id": "12345"}'},
}
],
},
{
"role": "tool",
"tool_call_id": "call_123",
"content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}',
},
{"role": "assistant", "content": "I found user Alice with ID 12345."},
]
@pytest.fixture
def sample_tool_output_large():
"""Large tool output for compression testing (100 items)."""
return json.dumps(
[
{
"id": i,
"name": f"Item {i}",
"score": i * 0.1,
"status": "active" if i % 2 == 0 else "inactive",
}
for i in range(100)
]
)
@pytest.fixture
def sample_tool_output_with_errors():
"""Tool output containing error items."""
items = [{"id": i, "status": "success"} for i in range(20)]
items[5] = {"id": 5, "status": "error", "message": "Connection refused"}
items[15] = {"id": 15, "status": "failed", "exception": "TimeoutError"}
return json.dumps(items)
@pytest.fixture
def sample_system_prompt_with_date():
"""System prompt containing dynamic date."""
return "You are a helpful assistant. Current date: 2025-01-06. Help the user with their tasks."
@pytest.fixture
def sample_anthropic_messages():
"""Anthropic-style messages with content blocks."""
return [
{
"role": "user",
"content": [
{"type": "text", "text": "Analyze this image"},
{
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": "..."},
},
],
}
]
# Mock client fixtures
@pytest.fixture
def mock_openai_response():
"""Mock OpenAI API response."""
mock = Mock()
mock.id = "chatcmpl-123"
mock.model = "gpt-4o"
mock.usage = Mock()
mock.usage.prompt_tokens = 100
mock.usage.completion_tokens = 50
mock.usage.total_tokens = 150
mock.choices = [Mock()]
mock.choices[0].message = Mock()
mock.choices[0].message.content = "This is a response."
mock.choices[0].message.role = "assistant"
mock.choices[0].finish_reason = "stop"
return mock
@pytest.fixture
def mock_openai_client(mock_openai_response):
"""Mock OpenAI client."""
client = Mock()
client.chat = Mock()
client.chat.completions = Mock()
client.chat.completions.create = Mock(return_value=mock_openai_response)
return client
# Storage fixtures
@pytest.fixture
def temp_sqlite_db():
"""Temporary SQLite database path."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
yield f.name
Path(f.name).unlink(missing_ok=True)
@pytest.fixture
def temp_jsonl_file():
"""Temporary JSONL file path."""
with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False) as f:
yield f.name
Path(f.name).unlink(missing_ok=True)
# Provider fixtures
@pytest.fixture
def openai_provider():
"""OpenAI provider instance."""
from headroom.providers.openai import OpenAIProvider
return OpenAIProvider()
@pytest.fixture
def openai_tokenizer():
"""OpenAI token counter for gpt-4o."""
from headroom.providers.openai import OpenAITokenCounter
return OpenAITokenCounter("gpt-4o")
# Config fixtures
@pytest.fixture
def default_config():
"""Default HeadroomConfig."""
from headroom.config import HeadroomConfig
return HeadroomConfig()
@pytest.fixture
def smart_crusher_config():
"""SmartCrusher config for testing."""
from headroom.config import SmartCrusherConfig
return SmartCrusherConfig(
enabled=True,
min_items_to_analyze=3,
min_tokens_to_crush=0, # Always crush for tests
max_items_after_crush=10,
)
# Helper for creating RequestMetrics
@pytest.fixture
def sample_request_metrics():
"""Sample RequestMetrics for storage tests."""
from headroom.config import RequestMetrics
return RequestMetrics(
request_id="test-123",
timestamp=datetime(2025, 1, 6, 12, 0, 0),
model="gpt-4o",
stream=False,
mode="audit",
tokens_input_before=1000,
tokens_input_after=800,
tokens_output=200,
block_breakdown={"system": 100, "user": 200, "assistant": 500},
waste_signals={"json_bloat": 50},
stable_prefix_hash="abc123",
cache_alignment_score=85.0,
cached_tokens=100,
transforms_applied=["CacheAligner", "SmartCrusher"],
tool_units_dropped=1,
turns_dropped=0,
messages_hash="def456",
)