headroom/tests/test_subscription_tracker_rtk_wired.py
chopratejas 44c605fbb0 fix(subscription): wire tokens_saved_rtk from RTK stats endpoint
PR-G2 (Realignment) — retire the dead `tokens_saved_rtk` data plane.

Previously, `SubscriptionTracker.update_contribution` silently mirrored
`tokens_saved_cli_filtering` into `tokens_saved_rtk`, making the two
counters identical at all times and hiding wrap-side RTK savings from
the dashboard.

Wiring:
- New `_last_rtk_tokens_saved` state on the tracker (init to 0).
- `update_contribution` now polls `_get_rtk_stats()` when the caller
  omits an explicit `tokens_saved_rtk`, computes the delta against the
  last lifetime total, and writes only the positive delta. State
  advances monotonically; a counter regression re-baselines without
  emitting a negative delta.
- `cli_filtering` and `rtk` are no longer aliased anywhere in the
  hot path.
- Persistence: `to_dict()` exposes raw `cli_filtering_raw` and
  `rtk_raw` keys (legacy dashboard `cli_filtering` / `rtk` still report
  `max(cli, rtk)` for back-compat). `_load_persisted_state` reads the
  raw keys when present and defaults to 0 otherwise so legacy state
  cannot silently re-inflate `tokens_saved_rtk` by mirroring
  `cli_filtering`.

Build constraints honoured:
- No silent fallback — transient `_get_rtk_stats()` exceptions are
  caught, structured-logged (`event=subscription_rtk_stats_fetch_failed`
  / `event=subscription_rtk_stats_unavailable`), and yield zero delta.
- Configurable — `HEADROOM_RTK_WIRING={enabled,disabled}` opts the
  polling out without disturbing tool selection. Unknown values raise
  loudly via `_rtk_wiring_mode`.
- Comprehensive tests — 9 new unit tests in
  `tests/test_subscription_tracker_rtk_wired.py` pin the wiring
  (baseline, delta across two/three polls, None payload, monotonic
  advance, counter regression, exception, env-var opt-out, explicit
  override, decoupling from cli_filtering). Existing tracker tests
  updated to reflect the no-mirror behaviour.

Refs: REALIGNMENT/09-phase-G-rtk-observability.md (PR-G2)
2026-05-22 13:48:42 -07:00

314 lines
11 KiB
Python

"""Tests for PR-G2 — RTK ``tokens_saved`` data-plane wiring.
Phase G of the Headroom realignment retires the dead ``tokens_saved_rtk``
field by sourcing it from RTK's own stats endpoint (``rtk gain --format
json`` via :func:`headroom.proxy.helpers._get_rtk_stats`) and writing the
per-call delta into ``HeadroomContribution.tokens_saved_rtk``. Previously,
the field silently mirrored ``tokens_saved_cli_filtering`` — making the
two counters identical at all times and breaking the dashboard's ability
to distinguish proxy-side compression from wrap-side RTK savings.
These tests pin the wiring:
1. The delta is computed correctly across two consecutive
:meth:`update_contribution` calls (monotonic counter advances).
2. ``tokens_saved_rtk`` is exactly zero when ``_get_rtk_stats()`` returns
``None`` (RTK not installed / not selected).
3. ``_last_rtk_tokens_saved`` advances monotonically; deltas are not
replayed across calls when the lifetime counter does not move.
Realignment build constraints honored:
- No silent fallback: a transient ``_get_rtk_stats()`` exception is
structured-logged and yields ``tokens_saved_rtk = 0`` (test 4).
- Configurable: ``HEADROOM_RTK_WIRING=disabled`` opts the polling out and
produces a clean zero, exercised by ``test_disabled_env_returns_zero``.
- Structured logs: each failure path emits a ``event=…`` line; the tests
do not pin the log payload to avoid coupling, but the helper signatures
surface them.
- Comprehensive tests: 6 unit tests + 1 explicit delta test cover the
contract.
"""
from __future__ import annotations
from typing import Any
import pytest
import headroom.subscription.tracker as tracker_module
from headroom.subscription.tracker import SubscriptionTracker
def _build_tracker(monkeypatch: pytest.MonkeyPatch) -> SubscriptionTracker:
"""Construct a tracker with persistence disabled (unit-test isolation)."""
monkeypatch.setattr(SubscriptionTracker, "_load_persisted_state", lambda self: None)
return SubscriptionTracker(enabled=True)
def _stub_rtk_stats(
monkeypatch: pytest.MonkeyPatch, payloads: list[dict[str, Any] | None]
) -> list[int]:
"""Stub ``_get_rtk_stats`` to return ``payloads`` in order.
Returns a counter list (mutated by the stub) so callers can assert the
number of polls.
"""
call_count: list[int] = [0]
def fake_get_rtk_stats() -> dict[str, Any] | None:
idx = call_count[0]
call_count[0] += 1
if idx >= len(payloads):
return payloads[-1]
return payloads[idx]
monkeypatch.setattr(
"headroom.proxy.helpers._get_rtk_stats",
fake_get_rtk_stats,
)
return call_count
# ---------------------------------------------------------------------------
# Test 1 — delta computed correctly across two consecutive polls
# ---------------------------------------------------------------------------
def test_tokens_saved_rtk_populated_from_rtk_stats(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""First call seeds the baseline; second call writes the positive delta."""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
_stub_rtk_stats(
monkeypatch,
[
{"lifetime_tokens_saved": 100},
{"lifetime_tokens_saved": 175},
],
)
# First call — establishes the baseline at 100, contributes 100 (the
# tracker starts at _last_rtk_tokens_saved == 0, so the first delta is
# the full lifetime total). That matches the spec: the field reflects
# cumulative session savings observed by the tracker.
tracker.update_contribution()
contribution_after_first = tracker._state.contribution.tokens_saved_rtk
assert contribution_after_first == 100
assert tracker._last_rtk_tokens_saved == 100
# Second call — delta is 175 - 100 = 75; cumulative contribution = 175.
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 175
assert tracker._last_rtk_tokens_saved == 175
def test_delta_computed_correctly_across_polls(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Three consecutive polls — each adds only the new RTK delta."""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
_stub_rtk_stats(
monkeypatch,
[
{"lifetime_tokens_saved": 0}, # baseline at zero
{"lifetime_tokens_saved": 50},
{"lifetime_tokens_saved": 250},
],
)
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 0
assert tracker._last_rtk_tokens_saved == 0
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 50
assert tracker._last_rtk_tokens_saved == 50
tracker.update_contribution()
# 50 + (250 - 50) = 250 cumulative; delta on the third call was 200.
assert tracker._state.contribution.tokens_saved_rtk == 250
assert tracker._last_rtk_tokens_saved == 250
# ---------------------------------------------------------------------------
# Test 2 — ``tokens_saved_rtk = 0`` when stats endpoint returns None
# ---------------------------------------------------------------------------
def test_rtk_stats_none_yields_zero_delta(monkeypatch: pytest.MonkeyPatch) -> None:
"""No RTK selected / installed — contribution stays at zero, no throw."""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
_stub_rtk_stats(monkeypatch, [None, None])
tracker.update_contribution()
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 0
assert tracker._last_rtk_tokens_saved == 0
# ---------------------------------------------------------------------------
# Test 3 — monotonic advancement; no replay on flat poll
# ---------------------------------------------------------------------------
def test_last_rtk_advances_monotonically(monkeypatch: pytest.MonkeyPatch) -> None:
"""Two polls returning the same lifetime total contribute exactly once."""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
_stub_rtk_stats(
monkeypatch,
[
{"lifetime_tokens_saved": 42},
{"lifetime_tokens_saved": 42}, # no movement
{"lifetime_tokens_saved": 42}, # still no movement
],
)
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 42
assert tracker._last_rtk_tokens_saved == 42
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 42 # unchanged
assert tracker._last_rtk_tokens_saved == 42
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 42
assert tracker._last_rtk_tokens_saved == 42
def test_counter_regression_rebaselines_without_negative_delta(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""RTK DB rebuild drops the lifetime total — re-baseline, do not subtract."""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
_stub_rtk_stats(
monkeypatch,
[
{"lifetime_tokens_saved": 500},
{"lifetime_tokens_saved": 100}, # regression!
{"lifetime_tokens_saved": 150},
],
)
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 500
assert tracker._last_rtk_tokens_saved == 500
tracker.update_contribution()
# Regression: contribution stays at 500 (no negative subtraction).
assert tracker._state.contribution.tokens_saved_rtk == 500
# Baseline now points at the new (smaller) lifetime total so subsequent
# polls can compute a meaningful delta.
assert tracker._last_rtk_tokens_saved == 100
tracker.update_contribution()
# 150 - 100 = 50 new delta; contribution = 500 + 50 = 550.
assert tracker._state.contribution.tokens_saved_rtk == 550
assert tracker._last_rtk_tokens_saved == 150
# ---------------------------------------------------------------------------
# Test 4 — transient exception in the stats endpoint
# ---------------------------------------------------------------------------
def test_rtk_stats_exception_zero_delta_no_throw(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A raised ``_get_rtk_stats()`` is caught, logged, and yields 0 delta."""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
def boom() -> dict[str, Any] | None:
raise RuntimeError("transient subprocess failure")
monkeypatch.setattr("headroom.proxy.helpers._get_rtk_stats", boom)
# Must not raise.
tracker.update_contribution()
assert tracker._state.contribution.tokens_saved_rtk == 0
assert tracker._last_rtk_tokens_saved == 0
# ---------------------------------------------------------------------------
# Test 5 — explicit env-var opt-out
# ---------------------------------------------------------------------------
def test_disabled_env_returns_zero(monkeypatch: pytest.MonkeyPatch) -> None:
"""``HEADROOM_RTK_WIRING=disabled`` skips the poll entirely."""
tracker = _build_tracker(monkeypatch)
monkeypatch.setenv(tracker_module._RTK_WIRING_ENV, "disabled")
polls = _stub_rtk_stats(
monkeypatch,
[{"lifetime_tokens_saved": 999}],
)
tracker.update_contribution()
# Stats endpoint never called when wiring is disabled.
assert polls[0] == 0
assert tracker._state.contribution.tokens_saved_rtk == 0
assert tracker._last_rtk_tokens_saved == 0
# ---------------------------------------------------------------------------
# Test 6 — explicit override from caller (back-compat for callers that
# already know the RTK delta out-of-band).
# ---------------------------------------------------------------------------
def test_explicit_rtk_override_skips_poll(monkeypatch: pytest.MonkeyPatch) -> None:
"""Caller-supplied ``tokens_saved_rtk`` short-circuits the poll."""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
polls = _stub_rtk_stats(monkeypatch, [{"lifetime_tokens_saved": 999}])
tracker.update_contribution(tokens_saved_rtk=17)
# Stats endpoint not consulted when the caller passes an explicit value.
assert polls[0] == 0
assert tracker._state.contribution.tokens_saved_rtk == 17
assert tracker._last_rtk_tokens_saved == 0
# ---------------------------------------------------------------------------
# Test 7 — cli_filtering decoupled from rtk
# ---------------------------------------------------------------------------
def test_cli_filtering_no_longer_mirrors_rtk(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pre-PR-G2 bug: ``cli_filtering`` and ``rtk`` were always equal.
After PR-G2 they are independent counters fed by separate sources.
"""
tracker = _build_tracker(monkeypatch)
monkeypatch.delenv(tracker_module._RTK_WIRING_ENV, raising=False)
_stub_rtk_stats(monkeypatch, [{"lifetime_tokens_saved": 25}])
tracker.update_contribution(tokens_saved_cli_filtering=8)
assert tracker._state.contribution.tokens_saved_cli_filtering == 8
# rtk comes from the polled delta, not from cli_filtering.
assert tracker._state.contribution.tokens_saved_rtk == 25