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
234 lines
9.0 KiB
Python
234 lines
9.0 KiB
Python
"""Behavior-driven Playwright validation for context-tool (RTK) availability
|
|
messaging on the dashboard (issue #1831).
|
|
|
|
Local runs only import/collect this file (Windows dev sandboxes don't run the
|
|
real browser here); the "Dashboard Playwright" CI check executes it for real.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
from urllib.parse import urlsplit
|
|
|
|
import pytest
|
|
|
|
from headroom.dashboard import get_dashboard_html
|
|
|
|
playwright = pytest.importorskip("playwright.sync_api")
|
|
Page = playwright.Page
|
|
expect = playwright.expect
|
|
sync_playwright = playwright.sync_playwright
|
|
|
|
|
|
def _base_stats() -> dict:
|
|
return {
|
|
"cost": {
|
|
"savings_usd": 12.34,
|
|
"compression_savings_usd": 12.34,
|
|
"cache_savings_usd": 5.25,
|
|
"cli_tokens_avoided": 0,
|
|
},
|
|
"requests": {
|
|
"total": 128,
|
|
"cached": 96,
|
|
"rate_limited": 0,
|
|
"failed": 0,
|
|
"by_provider": {"anthropic": 128},
|
|
"by_model": {"claude-opus-4-6": 128},
|
|
},
|
|
"tokens": {
|
|
"input": 245_000,
|
|
"output": 88_000,
|
|
"saved": 143_000,
|
|
"cli_tokens_avoided": 0,
|
|
"total_before_compression": 388_000,
|
|
"savings_percent": 36.86,
|
|
},
|
|
"overhead": {"average_ms": 14.2, "min_ms": 4.5, "max_ms": 42.7},
|
|
"ttfb": {"average_ms": 1320.0, "min_ms": 420.0, "max_ms": 2900.0},
|
|
"latency": {"average_ms": 1510.0, "min_ms": 520.0, "max_ms": 3300.0},
|
|
"waste_signals": {"json_bloat": 95_000, "repetition": 48_000},
|
|
"savings_history": [
|
|
["2026-04-01T00:00:00Z", 12_000],
|
|
["2026-04-05T00:00:00Z", 143_000],
|
|
],
|
|
"persistent_savings": {
|
|
"display_session": {},
|
|
"lifetime": {"tokens_saved": 143_000, "compression_savings_usd": 12.34},
|
|
},
|
|
"pipeline_timing": {},
|
|
"compression_cache": {"mode": "cache"},
|
|
"prefix_cache": {"by_provider": {}, "totals": {}, "prefix_freeze": {}},
|
|
}
|
|
|
|
|
|
def _sample_stats(*, available: bool, tokens_saved: int = 0) -> dict:
|
|
"""Build a /stats payload with context_tool/cli_filtering availability set.
|
|
|
|
`_base_stats()` has no `savings` key at all, so writing
|
|
`savings.by_layer.cli_filtering` requires a `.setdefault(...)` chain
|
|
rather than direct key assignment (would otherwise raise `KeyError`).
|
|
"""
|
|
stats = copy.deepcopy(_base_stats())
|
|
stats["tokens"]["cli_tokens_avoided"] = tokens_saved
|
|
stats["context_tool"] = {
|
|
"configured": "rtk",
|
|
"label": "RTK",
|
|
"available": available,
|
|
"stats": {"tool": "rtk", "label": "RTK", "installed": available},
|
|
}
|
|
cli_filtering = (
|
|
stats.setdefault("savings", {}).setdefault("by_layer", {}).setdefault("cli_filtering", {})
|
|
)
|
|
cli_filtering.update(
|
|
{
|
|
"tool": "rtk",
|
|
"label": "RTK",
|
|
"available": available,
|
|
"tokens": tokens_saved,
|
|
"tokens_saved": tokens_saved,
|
|
"session": {},
|
|
"lifetime": {"tokens_saved": 0},
|
|
"session_savings_pct": 0.0,
|
|
}
|
|
)
|
|
return stats
|
|
|
|
|
|
def _sample_history(*, available: bool, lifetime_tokens_saved: int = 456_700) -> dict:
|
|
return {
|
|
"history": [
|
|
{
|
|
"timestamp": "2026-04-05T00:00:00Z",
|
|
"total_tokens_saved": 143_000,
|
|
"compression_savings_usd": 12.34,
|
|
},
|
|
],
|
|
"series": {"daily": [], "weekly": [], "monthly": []},
|
|
"lifetime": {"tokens_saved": 143_000, "compression_savings_usd": 12.34},
|
|
"cli_filtering": {
|
|
"tool": "rtk",
|
|
"label": "RTK",
|
|
"available": available,
|
|
"lifetime": {"tokens_saved": lifetime_tokens_saved},
|
|
"session": {},
|
|
},
|
|
}
|
|
|
|
|
|
def _install_dashboard_routes(page: Page, stats: dict, history: dict) -> None:
|
|
health = {"status": "healthy", "version": "0.3.0"}
|
|
dashboard_html = get_dashboard_html()
|
|
|
|
def handler(route) -> None: # type: ignore[no-untyped-def]
|
|
# Match on the URL path only: the dashboard fetches /stats?cached=1,
|
|
# so suffix checks against the full URL miss it and the request
|
|
# escapes the harness to the real network.
|
|
path = urlsplit(route.request.url).path
|
|
if path in ("/dashboard", "/"):
|
|
route.fulfill(status=200, content_type="text/html", body=dashboard_html)
|
|
return
|
|
if "/stats-history" in path:
|
|
route.fulfill(
|
|
status=200,
|
|
content_type="application/json",
|
|
body=json.dumps(history),
|
|
)
|
|
return
|
|
if path.endswith("/stats"):
|
|
route.fulfill(status=200, content_type="application/json", body=json.dumps(stats))
|
|
return
|
|
if path.endswith("/health"):
|
|
route.fulfill(status=200, content_type="application/json", body=json.dumps(health))
|
|
return
|
|
route.continue_()
|
|
|
|
page.route("**/*", handler)
|
|
|
|
|
|
def test_dashboard_session_view_shows_not_installed_message_when_unavailable() -> None:
|
|
"""Session view shows a distinct "not installed" message, not `0`, when
|
|
`context_tool.available` is False (the #1831 bug this fix addresses).
|
|
"""
|
|
stats = _sample_stats(available=False, tokens_saved=0)
|
|
history = _sample_history(available=False)
|
|
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch()
|
|
page = browser.new_page(viewport={"width": 1720, "height": 1400}, color_scheme="dark")
|
|
_install_dashboard_routes(page, stats, history)
|
|
page.goto("http://headroom.local/dashboard", wait_until="load")
|
|
|
|
expect(page.get_by_text("RTK not installed", exact=True)).to_be_visible()
|
|
expect(page.get_by_text("not installed", exact=True)).to_be_visible()
|
|
expect(page.get_by_text("RTK 0 this session (0.0%)", exact=True)).to_have_count(0)
|
|
|
|
browser.close()
|
|
|
|
|
|
def test_dashboard_session_view_shows_real_zero_row_when_installed_but_zero() -> None:
|
|
"""Boundary value 0.0: installed but genuinely zero savings still renders
|
|
the real number, not the "not installed" message -- proves the new guard
|
|
doesn't over-trigger on the exact case the #1831 reporter would hit again.
|
|
"""
|
|
stats = _sample_stats(available=True, tokens_saved=0)
|
|
history = _sample_history(available=True, lifetime_tokens_saved=0)
|
|
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch()
|
|
page = browser.new_page(viewport={"width": 1720, "height": 1400}, color_scheme="dark")
|
|
_install_dashboard_routes(page, stats, history)
|
|
page.goto("http://headroom.local/dashboard", wait_until="load")
|
|
|
|
expect(page.get_by_text("RTK 0 this session (0.0%)", exact=True)).to_be_visible()
|
|
expect(page.get_by_text("RTK not installed", exact=True)).to_have_count(0)
|
|
# The Token Usage panel's "not installed" row uses `x-show`, which
|
|
# toggles CSS display and keeps the node in the DOM (unlike the
|
|
# ternary-swapped "RTK not installed" text above, which is genuinely
|
|
# absent). Assert hidden, not absent, matching the repo's existing
|
|
# `x-show` convention in tests/test_dashboard_cache_lifetime_playwright.py.
|
|
expect(page.get_by_text("not installed", exact=True)).to_be_hidden()
|
|
|
|
browser.close()
|
|
|
|
|
|
def test_dashboard_historical_tab_hides_lifetime_card_when_unavailable() -> None:
|
|
"""The Historical tab's lifetime card stays hidden (same as the existing
|
|
hard-failure hide-card behavior) when `cli_filtering.available` is False.
|
|
"""
|
|
stats = _sample_stats(available=True, tokens_saved=5_000)
|
|
history = _sample_history(available=False)
|
|
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch()
|
|
page = browser.new_page(viewport={"width": 1720, "height": 1400}, color_scheme="dark")
|
|
_install_dashboard_routes(page, stats, history)
|
|
page.goto("http://headroom.local/dashboard", wait_until="load")
|
|
|
|
page.get_by_role("button", name="Historical").click()
|
|
expect(page.get_by_text("Historical Summary")).to_be_visible()
|
|
expect(page.get_by_text("RTK Lifetime Saved")).to_have_count(0)
|
|
|
|
browser.close()
|
|
|
|
|
|
def test_dashboard_historical_tab_shows_lifetime_card_when_available() -> None:
|
|
"""The Historical tab's lifetime card still renders the real number when
|
|
`cli_filtering.available` is True (existing behavior, unchanged).
|
|
"""
|
|
stats = _sample_stats(available=True, tokens_saved=5_000)
|
|
history = _sample_history(available=True, lifetime_tokens_saved=456_700)
|
|
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch()
|
|
page = browser.new_page(viewport={"width": 1720, "height": 1400}, color_scheme="dark")
|
|
_install_dashboard_routes(page, stats, history)
|
|
page.goto("http://headroom.local/dashboard", wait_until="load")
|
|
|
|
page.get_by_role("button", name="Historical").click()
|
|
expect(page.get_by_text("RTK Lifetime Saved")).to_be_visible()
|
|
expect(page.get_by_text("456.7k")).to_be_visible()
|
|
|
|
browser.close()
|