0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
310 lines
10 KiB
Python
310 lines
10 KiB
Python
"""Session RTK savings must be the delta from the proxy-startup baseline.
|
|
|
|
Regression for the scope-mixing bug: the dashboard's *session* RTK number must
|
|
be computed from token deltas since the baseline pinned at proxy startup — NOT
|
|
from RTK's lifetime average (which dilutes a 62%-this-session rate down to an
|
|
18.5% all-time number). This exercises the real ``_get_context_tool_stats()``
|
|
plumbing rather than asserting the arithmetic in the abstract.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import headroom.proxy.helpers as helpers
|
|
|
|
|
|
def _reset(monkeypatch):
|
|
monkeypatch.delenv(helpers._RTK_GAIN_SCOPE_ENV, raising=False)
|
|
monkeypatch.setenv("HEADROOM_CONTEXT_TOOL", "rtk")
|
|
helpers._context_tool_stats_cache.update(
|
|
{"expires_at": 0.0, "has_value": False, "tool": None, "value": None}
|
|
)
|
|
helpers._context_tool_session_baseline.update(
|
|
{
|
|
"initialized": False,
|
|
"tool": None,
|
|
"total_commands": 0,
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"tokens_saved": 0,
|
|
"total_time_ms": 0,
|
|
"captured_at": 0.0,
|
|
}
|
|
)
|
|
|
|
|
|
def _bust_cache():
|
|
helpers._context_tool_stats_cache.update(
|
|
{"expires_at": 0.0, "has_value": False, "tool": None, "value": None}
|
|
)
|
|
|
|
|
|
def test_session_savings_is_delta_not_lifetime_average(monkeypatch):
|
|
_reset(monkeypatch)
|
|
|
|
state: dict = {"summary": None}
|
|
|
|
def fake_lifetime(tool):
|
|
return helpers._context_tool_summary_payload(
|
|
tool="rtk", installed=True, scope="global", summary=state["summary"]
|
|
)
|
|
|
|
monkeypatch.setattr(helpers, "_read_context_tool_lifetime_stats", fake_lifetime)
|
|
|
|
# First poll pins the baseline to the current lifetime → session delta is 0,
|
|
# but the lifetime number is preserved untouched.
|
|
state["summary"] = {"total_input": 1000, "total_output": 400, "total_saved": 600}
|
|
first = helpers._get_context_tool_stats()
|
|
assert first is not None
|
|
assert first["session"]["tokens_saved"] == 0
|
|
assert first["lifetime"]["tokens_saved"] == 600
|
|
|
|
# Lifetime advances (more RTK commands run this session); the session number
|
|
# is the DELTA, not the 800 lifetime total.
|
|
_bust_cache()
|
|
state["summary"] = {"total_input": 1300, "total_output": 500, "total_saved": 800}
|
|
second = helpers._get_context_tool_stats()
|
|
assert second["session"]["tokens_saved"] == 200 # 800 - 600
|
|
assert second["lifetime"]["tokens_saved"] == 800
|
|
# Session % is derived from the delta (200 saved / 300 input delta), not the
|
|
# lifetime-diluted average.
|
|
assert second["session"]["savings_pct"] == round(200 / 300 * 100, 4)
|
|
|
|
|
|
# --- Failure semantics: a failed read is "no data", never a zero counter ---
|
|
|
|
|
|
def _fake_run_raises(*args, **kwargs):
|
|
import subprocess
|
|
|
|
raise subprocess.TimeoutExpired(cmd="rtk", timeout=5)
|
|
|
|
|
|
def test_rtk_reader_returns_none_on_timeout(monkeypatch):
|
|
import headroom.rtk as rtk_mod
|
|
|
|
monkeypatch.setattr(rtk_mod, "get_rtk_path", lambda: "/fake/rtk")
|
|
monkeypatch.setattr(helpers, "run", _fake_run_raises)
|
|
assert helpers._read_rtk_lifetime_stats() is None
|
|
|
|
|
|
def test_rtk_reader_returns_none_on_nonzero_exit(monkeypatch, caplog):
|
|
import logging
|
|
from types import SimpleNamespace
|
|
|
|
import headroom.rtk as rtk_mod
|
|
|
|
monkeypatch.setattr(rtk_mod, "get_rtk_path", lambda: "/fake/rtk")
|
|
monkeypatch.setattr(
|
|
helpers,
|
|
"run",
|
|
lambda *a, **k: SimpleNamespace(returncode=1, stdout="", stderr="boom"),
|
|
)
|
|
with caplog.at_level(logging.WARNING):
|
|
assert helpers._read_rtk_lifetime_stats() is None
|
|
assert "rtk_stats_subprocess_failed" in caplog.text
|
|
|
|
|
|
def test_rtk_reader_returns_none_on_bad_json(monkeypatch):
|
|
from types import SimpleNamespace
|
|
|
|
import headroom.rtk as rtk_mod
|
|
|
|
monkeypatch.setattr(rtk_mod, "get_rtk_path", lambda: "/fake/rtk")
|
|
monkeypatch.setattr(
|
|
helpers,
|
|
"run",
|
|
lambda *a, **k: SimpleNamespace(returncode=0, stdout="not-json{", stderr=""),
|
|
)
|
|
assert helpers._read_rtk_lifetime_stats() is None
|
|
|
|
|
|
def test_rtk_reader_not_installed_keeps_zero_payload(monkeypatch):
|
|
import headroom.rtk as rtk_mod
|
|
|
|
monkeypatch.setattr(rtk_mod, "get_rtk_path", lambda: None)
|
|
payload = helpers._read_rtk_lifetime_stats()
|
|
assert payload is not None
|
|
assert payload["installed"] is False
|
|
assert payload["tokens_saved"] == 0
|
|
|
|
|
|
def test_lean_ctx_reader_returns_none_on_failure_and_logs(monkeypatch, caplog):
|
|
import logging
|
|
|
|
import headroom.lean_ctx as lean_mod
|
|
|
|
monkeypatch.setattr(lean_mod, "get_lean_ctx_path", lambda: "/fake/lean-ctx")
|
|
monkeypatch.setattr(helpers, "run", _fake_run_raises)
|
|
with caplog.at_level(logging.WARNING):
|
|
assert helpers._read_lean_ctx_lifetime_stats() is None
|
|
assert "stats_subprocess_failed" in caplog.text
|
|
|
|
|
|
# --- Baseline guards: pin only from successful installed-tool reads ---
|
|
|
|
|
|
def _payload(saved: int, *, input_tokens: int = 1000, installed: bool = True):
|
|
return helpers._context_tool_summary_payload(
|
|
tool="rtk",
|
|
installed=installed,
|
|
scope="global",
|
|
summary={"total_input": input_tokens, "total_output": 400, "total_saved": saved},
|
|
)
|
|
|
|
|
|
def _stub_reads(monkeypatch, sequence):
|
|
calls = {"n": 0}
|
|
|
|
def fake(tool):
|
|
idx = min(calls["n"], len(sequence) - 1)
|
|
calls["n"] += 1
|
|
item = sequence[idx]
|
|
return item() if callable(item) else item
|
|
|
|
monkeypatch.setattr(helpers, "_read_context_tool_lifetime_stats", fake)
|
|
return calls
|
|
|
|
|
|
def test_transient_failure_does_not_repin_baseline_or_inflate_session(monkeypatch):
|
|
"""The headline regression, end to end through the real reader.
|
|
|
|
A transient rtk subprocess failure between two identical successful reads
|
|
must not re-pin the session baseline; today the reader converts the
|
|
failure into a synthetic zero payload and recovery reports the tool's
|
|
entire lifetime as session savings.
|
|
"""
|
|
import json as json_mod
|
|
from types import SimpleNamespace
|
|
|
|
import headroom.rtk as rtk_mod
|
|
|
|
_reset(monkeypatch)
|
|
monkeypatch.setattr(rtk_mod, "get_rtk_path", lambda: "/fake/rtk")
|
|
|
|
good = json_mod.dumps(
|
|
{"summary": {"total_input": 1000, "total_output": 400, "total_saved": 600}}
|
|
)
|
|
behaviors = [
|
|
lambda: SimpleNamespace(returncode=0, stdout=good, stderr=""),
|
|
_fake_run_raises,
|
|
lambda: SimpleNamespace(returncode=0, stdout=good, stderr=""),
|
|
]
|
|
calls = {"n": 0}
|
|
|
|
def fake_run(*args, **kwargs):
|
|
behavior = behaviors[min(calls["n"], len(behaviors) - 1)]
|
|
calls["n"] += 1
|
|
return behavior()
|
|
|
|
monkeypatch.setattr(helpers, "run", fake_run)
|
|
|
|
first = helpers._get_context_tool_stats()
|
|
assert first["session"]["tokens_saved"] == 0
|
|
assert first["lifetime"]["tokens_saved"] == 600
|
|
|
|
_bust_cache()
|
|
helpers._get_context_tool_stats()
|
|
# Baseline survives the failed poll untouched.
|
|
assert helpers._context_tool_session_baseline["tokens_saved"] == 600
|
|
|
|
_bust_cache()
|
|
recovered = helpers._get_context_tool_stats()
|
|
# Recovery must NOT report the full lifetime as session savings.
|
|
assert recovered["session"]["tokens_saved"] == 0
|
|
assert recovered["counter_reset_detected"] is False
|
|
|
|
|
|
def test_boot_fail_then_poll_fail_never_pins_zero_baseline(monkeypatch):
|
|
import asyncio
|
|
|
|
_reset(monkeypatch)
|
|
_stub_reads(monkeypatch, [None, None, _payload(600)])
|
|
|
|
asyncio.run(helpers.initialize_context_tool_session_baseline())
|
|
assert helpers._context_tool_session_baseline["initialized"] is False
|
|
|
|
_bust_cache()
|
|
assert helpers._get_context_tool_stats() is None
|
|
# Lazy-init must not have pinned zeros from the failed poll.
|
|
assert helpers._context_tool_session_baseline["initialized"] is False
|
|
|
|
_bust_cache()
|
|
recovered = helpers._get_context_tool_stats()
|
|
assert recovered["session"]["tokens_saved"] == 0
|
|
assert recovered["lifetime"]["tokens_saved"] == 600
|
|
|
|
|
|
def test_stats_reset_with_failing_read_defers_to_next_success(monkeypatch):
|
|
import asyncio
|
|
|
|
_reset(monkeypatch)
|
|
_stub_reads(monkeypatch, [_payload(600), None, _payload(650)])
|
|
|
|
first = helpers._get_context_tool_stats()
|
|
assert first["session"]["tokens_saved"] == 0
|
|
|
|
# /stats/reset while rtk is down: old baseline dropped, pin deferred.
|
|
asyncio.run(helpers.initialize_context_tool_session_baseline())
|
|
assert helpers._context_tool_session_baseline["initialized"] is False
|
|
|
|
_bust_cache()
|
|
after = helpers._get_context_tool_stats()
|
|
# First successful read after the deferred reset pins fresh: delta 0.
|
|
assert after["session"]["tokens_saved"] == 0
|
|
assert after["lifetime"]["tokens_saved"] == 650
|
|
|
|
|
|
def test_genuine_counter_reset_still_repins(monkeypatch):
|
|
_reset(monkeypatch)
|
|
_stub_reads(monkeypatch, [_payload(600), _payload(50)])
|
|
|
|
helpers._get_context_tool_stats()
|
|
_bust_cache()
|
|
second = helpers._get_context_tool_stats()
|
|
assert second["counter_reset_detected"] is True
|
|
assert second["session"]["tokens_saved"] == 0
|
|
assert second["lifetime"]["tokens_saved"] == 50
|
|
|
|
|
|
def test_not_installed_payload_does_not_repin_baseline(monkeypatch):
|
|
_reset(monkeypatch)
|
|
_stub_reads(
|
|
monkeypatch,
|
|
[_payload(600), _payload(0, input_tokens=0, installed=False), _payload(600)],
|
|
)
|
|
|
|
helpers._get_context_tool_stats()
|
|
_bust_cache()
|
|
absent = helpers._get_context_tool_stats()
|
|
# Tool vanished at resolution time: honest zeros display, baseline intact.
|
|
assert absent["installed"] is False
|
|
assert helpers._context_tool_session_baseline["tokens_saved"] == 600
|
|
|
|
_bust_cache()
|
|
back = helpers._get_context_tool_stats()
|
|
assert back["session"]["tokens_saved"] == 0
|
|
assert back["counter_reset_detected"] is False
|
|
|
|
|
|
def test_tool_switch_with_failing_first_read_does_not_zero_pin(monkeypatch):
|
|
_reset(monkeypatch)
|
|
_stub_reads(monkeypatch, [_payload(600), None])
|
|
|
|
helpers._get_context_tool_stats()
|
|
monkeypatch.setenv("HEADROOM_CONTEXT_TOOL", "lean-ctx")
|
|
_bust_cache()
|
|
assert helpers._get_context_tool_stats() is None
|
|
# Switching tools with a failing first read must not pin a zero baseline
|
|
# for the new tool.
|
|
assert helpers._context_tool_session_baseline.get("tool") != "lean-ctx"
|
|
|
|
|
|
def test_failed_poll_caches_none_for_ttl(monkeypatch):
|
|
_reset(monkeypatch)
|
|
calls = _stub_reads(monkeypatch, [None])
|
|
|
|
assert helpers._get_context_tool_stats() is None
|
|
assert helpers._get_context_tool_stats() is None
|
|
# Second call inside the TTL is served from cache — no re-read storm.
|
|
assert calls["n"] == 1
|