headroom/tests/test_subscription_session_tracking.py
Gautam Sharma 793d20fb2a
fix(subscription): read newest transcript tail (#2310)
## Description

Large Claude Code transcript files were capped by reading the first 10
MB of each append-only JSONL file. Because recent entries are appended
at the end, current-window and weighted token usage could silently omit
the newest activity.

Oversized transcripts are now read from EOF. If the capped tail begins
within a JSONL record, only that partial record is discarded. A
  complete record beginning exactly at the boundary remains included.

  ## Type of Change

  - [x] Bug fix
  - [ ] New feature
  - [ ] Breaking change
  - [ ] Documentation-only change
  - [ ] Refactoring

  ## Changes Made

- Read the newest capped transcript bytes instead of the oldest prefix.
  - Determine the tail offset using the opened file handle.
- Inspect the preceding byte to distinguish a partial record from an
exact line boundary.
  - Remove partial bytes before UTF-8 decoding.
  - Preserve existing behavior for transcripts below the 10 MB cap.
- Add direct session-tracking tests for aggregation and boundary
behavior.
  - Add an Unreleased changelog entry.

  ## Testing

  - [x] Added regression tests
  - [x] Focused tests pass
  - [x] Subscription test suite passes
  - [x] Ruff checks pass
  - [x] Mypy passes
  - [x] Changed files pass formatting checks
  - [ ] Entire repository test suite passes without baseline failures

  Commands and results:

- `uv run --extra dev --frozen pytest
tests/test_subscription_session_tracking.py -q`
    - `4 passed`
  - Subscription-focused suite
    - `53 passed`
  - `uv run --extra dev --frozen ruff check .`
    - Passed
  - `uv run --extra dev --frozen mypy headroom --ignore-missing-imports`
    - Success across 504 source files
  - Changed-file Ruff formatting
    - Passed
  - `uv run --extra dev --frozen pytest -q`
    - `9364 passed, 565 skipped, 4 failed`
    - The four existing, unrelated failures are:
      - `test_l2_appends_transform_label`
      - `test_recovery_records_sockets_and_secures_both_backups`
      - `test_dashboard_uses_cached_stats_and_lazy_history_feed_polling`
      - `test_smart_crusher_log_fallback_runs_for_valid_json`

Repository-wide `ruff format --check .` identifies pre-existing
formatting drift only in the untouched
`headroom/proxy/handlers/anthropic.py`.

  ## Real Behavior Proof

A focused reproduction created a 10,485,787-byte transcript with a
marker entry appended after the 10 MB boundary.

  Before the fix:

  ```text
{'file_bytes': 10485787, 'line_count': 1, 'newest_entry_present': False}

  After the fix:

{'file_bytes': 10485787, 'line_count': 1, 'newest_entry_present': True}

  The regression tests additionally verify that:

1. Recent token usage beyond the cap contributes to raw and weighted
totals.
  2. A partial initial JSONL record is discarded.
  3. A complete record exactly at the tail boundary is preserved.
  4. Small transcripts retain their existing behavior.

  Environment: macOS arm64, CPython 3.12.13.

Not tested: mutation of the transcript during the individual file read
by a live Claude Code process. Reads remain bounded to a single recent
  snapshot.

  ## Review Readiness

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

  ## Checklist

- [x] The change follows existing project style and error-handling
conventions.
  - [x] Tests cover the reported failure and relevant boundary cases.
  - [x] The 10 MB memory/read cap remains enforced.
  - [x] No unrelated files or formatting changes are included.
  - [x] No temporary logging or debug code remains.
  - [x] The changelog has been updated.

  ## Additional Notes

The four full-suite failures listed above occur outside the modified
subscription code and are unrelated to this PR. All tests covering
  transcript reading and subscription tracking pass.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-16 14:39:10 -07:00

89 lines
2.9 KiB
Python

from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
import pytest
from headroom.subscription import session_tracking
def test_compute_window_tokens_reads_recent_entries_from_large_transcript(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
timestamp = "2026-01-01T00:00:00Z"
recent_entry = {
"timestamp": timestamp,
"message": {
"model": "claude-opus-4-1",
"usage": {"input_tokens": 11, "output_tokens": 7},
},
}
recent_line = json.dumps(recent_entry).encode() + b"\n"
max_file_bytes = len(recent_line) + 8
transcript = tmp_path / "session.jsonl"
transcript.write_bytes(b"x" * max_file_bytes + b"\n" + recent_line)
monkeypatch.setattr(session_tracking, "_MAX_FILE_BYTES", max_file_bytes)
monkeypatch.setattr(session_tracking, "find_transcript_files", lambda: [transcript])
entry_ts = datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()
tokens = session_tracking.compute_window_tokens(entry_ts - 1, entry_ts + 1)
assert tokens.input == 11
assert tokens.output == 7
assert tokens.weighted_token_equivalent == 36.0
assert tokens.by_model == {
"claude-opus-4-1": {
"input": 11,
"output": 7,
"cache_reads": 0,
"cache_writes_5m": 0,
"cache_writes_1h": 0,
"cache_writes_total": 0,
}
}
def test_read_transcript_lines_discards_partial_initial_line(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
complete_line = b'{"marker":"recent"}\n'
max_file_bytes = len(complete_line) + 5
transcript = tmp_path / "session.jsonl"
transcript.write_bytes(b"partial-old-entry\n" + complete_line)
monkeypatch.setattr(session_tracking, "_MAX_FILE_BYTES", max_file_bytes)
assert session_tracking._read_transcript_lines(transcript) == ['{"marker":"recent"}']
def test_read_transcript_lines_preserves_line_at_tail_boundary(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
tail = b'{"marker":"first"}\n{"marker":"second"}\n'
transcript = tmp_path / "session.jsonl"
transcript.write_bytes(b'{"marker":"old"}\n' + tail)
monkeypatch.setattr(session_tracking, "_MAX_FILE_BYTES", len(tail))
assert session_tracking._read_transcript_lines(transcript) == [
'{"marker":"first"}',
'{"marker":"second"}',
]
def test_read_transcript_lines_preserves_small_transcript(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
transcript = tmp_path / "session.jsonl"
transcript.write_text('{"marker":"first"}\n\n{"marker":"second"}\n')
monkeypatch.setattr(session_tracking, "_MAX_FILE_BYTES", 1024)
assert session_tracking._read_transcript_lines(transcript) == [
'{"marker":"first"}',
'{"marker":"second"}',
]