Closes the codecov gap flagged on PR 232 (88.89% → near 100% on the patch):
- A file with no marker block returns no prior recommendations.
- A marker block with nothing between the markers yields an empty list
(the re.split fast-path with zero sections).
- A stray `### ` with no heading text inside the block is silently
skipped (the `if not heading: continue` branch, previously
unexercised in tests) — a real section after it still parses cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`_finalize_stream_response` recorded metrics, cost, and prefix-cache stats
but never appended a `RequestLog` to the request logger. Because the
streaming Anthropic path is what Claude Code uses, this meant
`/stats.recent_requests` and `/transformations/feed` were permanently
empty for typical traffic — even when the proxy was started with
`--log-messages`. Only the non-streaming Anthropic path
(`anthropic.py:1167, 1599`) and the Bedrock streaming finalizer
(`streaming.py:_stream_response_bedrock`) were logged.
Wire the same `RequestLog` shape the other paths build, plumbing `tags`
through both `_finalize_stream_response` call sites in `_stream_response`
and respecting `config.log_full_messages` for `request_messages`.
Adds `tests/test_proxy_streaming_request_logger.py` covering the happy
path, both `log_full_messages` branches, the zero-original-tokens edge,
and the logger-disabled no-op.
Three independent pre-existing test-hygiene regressions on main, all
surfaced as cascading CI failures:
1. tests/test_cli/test_wrap_copilot.py (from #229) mutated
sys.modules["headroom.cli.main"] with a fake click.Group() at
module-import time and never restored it. Any later test that did
`from headroom.cli.main import main` got an empty group with no
version option and no registered subcommands, breaking ~20
test_cli/* and test_cli_proxy_env.py tests. Rewrite to import the
real `main` directly — the fake-group indirection served no
purpose.
2. tests/test_proxy_copilot_auth_hooks.py (from #229) installed fake
httpx / fastapi.responses / headroom.proxy.* modules into
sys.modules inside a helper called from test functions, never
cleaned up. Later tests that imported ASGITransport or JSONResponse
hit the fakes and failed with ImportError. Switch the helper to
monkeypatch.setitem so the fakes are scoped to the owning test.
3. tests/test_release_version.py hardcoded canonical=0.5.25 in the
subprocess-output assertion; the project version in pyproject.toml
has since bumped to 0.9.1. Compute the expected value dynamically
via get_canonical_version(ROOT) so the test tracks pyproject.
Drive-by: main is currently failing `ruff format --check .` because of
two missing blank lines between two top-level functions in this file
(introduced in 8bf11d2). Fixing it here so this PR's CI can go green —
no other way to unblock the format check without landing a separate PR
first.
Tests that do `patch("headroom.cli.<sub>.<attr>")` resolve the target by
walking attributes on the `headroom.cli` package object. That lookup
fails when `tests/test_cli/test_wrap_copilot.py` pops `headroom.cli`
from `sys.modules` at import time and re-imports it with a fake
`headroom.cli.main` — the re-imported package only has `.wrap` bound
because `_register_commands()` in `main.py` never runs against the fake.
Eagerly importing the subcommand submodules from `__init__.py` binds
them as package attributes regardless of how `main.py` is loaded, so
the patch lookup survives that kind of sys.modules mutation.
Fixes#234
`headroom learn` built the marker block from only the current run's
recommendations and wholesale-replaced any prior block via
`_MARKER_PATTERN.sub`. Sections learned weeks earlier that didn't
re-surface in a later run were silently dropped.
Fix: in `_merge_into_file`, parse recommendations out of the prior
block and union them with the new run's recommendations. Sections
re-surfaced by the new run take precedence (latest analysis wins);
sections not re-surfaced are carried forward so learnings accumulate
instead of getting clobbered.
To fully rebuild the block, delete it manually and re-run.
Tests: existing wholesale-replace test rewritten as a carry-forward
assertion. Added tests for same-section override, MEMORY.md
carry-forward, and round-trip of sections without a tokens annotation.
Closes#231