Files
wehub-resource-sync f3d80b4628
Auto Release / auto-release (push) Failing after 1s
CI / lint (push) Failing after 0s
CI / screenshot-quality (push) Failing after 3s
CI / pr-policy (push) Has been skipped
CI / test (3.11) (push) Failing after 0s
CI / test (3.12) (push) Failing after 0s
CI / test (3.13) (push) Failing after 3s
CI / coverage (push) Failing after 1s
Legibility / legibility (push) Failing after 3s
chore: import upstream snapshot with attribution
2026-07-13 12:31:48 +08:00

2313 lines
93 KiB
Python

import asyncio
import base64
import json
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
import aiohttp
import pytest
from aiohttp.test_utils import make_mocked_request
from claude_tap.dashboard import (
DASHBOARD_SUMMARY_VERSION,
_clean_user_prompt_text,
_content_text,
_event_payload,
_first_error,
_infer_agent,
_input_user_text,
_parts_text,
_preview,
_record_host,
_record_model,
_record_response_text,
_record_usage,
_request_user_text,
_response_events,
_response_text,
dashboard_trace_snapshot,
list_trace_agents,
list_trace_sessions,
load_trace_session,
read_dashboard_template,
)
from claude_tap.history import migrate_legacy_traces
from claude_tap.live import LiveViewerServer, _record_limit_from_request
from claude_tap.trace import TraceWriter
from claude_tap.trace_log_handler import SQLiteLogHandler
from claude_tap.trace_store import get_trace_store
def _write_jsonl(path: Path, records: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
"\n".join(json.dumps(record, ensure_ascii=False, separators=(",", ":")) for record in records) + "\n",
encoding="utf-8",
)
def test_record_limit_from_request_preserves_large_loaded_windows() -> None:
request = make_mocked_request("GET", "/api/sessions/example/records?limit=1500")
assert _record_limit_from_request(request) == 1500
def test_dashboard_lists_sessions_by_normalized_updated_at(trace_db) -> None:
store = get_trace_store()
older = store.create_session(started_at=datetime(2026, 5, 1, 10, 0, tzinfo=timezone.utc))
newer = store.create_session(started_at=datetime(2026, 5, 1, 11, 0, tzinfo=timezone.utc))
conn = store._connect()
conn.execute("UPDATE sessions SET updated_at = '2026-05-01T10:00:00+09:00' WHERE id = ?", (older,))
conn.execute("UPDATE sessions SET updated_at = '2026-05-01T02:30:00+00:00' WHERE id = ?", (newer,))
conn.commit()
assert [session["id"] for session in list_trace_sessions()][:2] == [newer, older]
def _anthropic_record(turn: int = 1) -> dict:
return {
"timestamp": "2026-05-20T08:00:00+00:00",
"request_id": "req_claude",
"turn": turn,
"duration_ms": 1200,
"capture": {"client": "claude", "proxy_mode": "reverse"},
"request": {
"method": "POST",
"path": "/v1/messages",
"headers": {"Host": "api.anthropic.com"},
"body": {
"model": "claude-sonnet-4-6",
"messages": [{"role": "user", "content": "Explain this repository"}],
},
},
"response": {
"status": 200,
"headers": {},
"body": {
"model": "claude-sonnet-4-6",
"content": [{"type": "text", "text": "This is a trace viewer."}],
"usage": {"input_tokens": 42, "output_tokens": 9},
},
},
}
def _antigravity_record() -> dict:
return {
"timestamp": "2026-05-20T09:00:00+00:00",
"request_id": "req_agy",
"turn": 1,
"duration_ms": 900,
"request": {
"method": "POST",
"path": "/v1internal:streamGenerateContent?alt=sse",
"headers": {"Host": "antigravity-unleash.goog"},
"body": {
"request": {
"contents": [{"role": "user", "parts": [{"text": "What model are you?"}]}],
}
},
},
"response": {
"status": 200,
"headers": {},
"body": {
"candidates": [{"content": {"parts": [{"text": "I am Sonnet."}]}}],
"usageMetadata": {"promptTokenCount": 100, "candidatesTokenCount": 5},
},
},
}
def _bedrock_frame(payload: dict) -> str:
encoded = base64.b64encode(json.dumps(payload, separators=(",", ":")).encode()).decode()
return "\x00\x00binary-prefix" + json.dumps({"bytes": encoded, "p": "abcdefghijk"}) + "\ufffd"
def _bedrock_stream_record() -> dict:
body = "".join(
[
_bedrock_frame(
{
"type": "message_start",
"message": {
"id": "msg_dashboard_bedrock",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-6",
"content": [],
"usage": {"input_tokens": 3, "output_tokens": 0},
},
}
),
_bedrock_frame({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
_bedrock_frame({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "OK"}}),
_bedrock_frame({"type": "content_block_stop", "index": 0}),
_bedrock_frame({"type": "message_delta", "delta": {"stop_reason": "end_turn"}}),
_bedrock_frame({"type": "message_stop", "amazon-bedrock-invocationMetrics": {"inputTokenCount": 3}}),
]
)
return {
"timestamp": "2026-05-20T08:30:00+00:00",
"request_id": "req_bedrock_dashboard",
"turn": 1,
"request": {
"method": "POST",
"path": "/model/global.anthropic.claude-opus-4-6-v1/invoke-with-response-stream",
"headers": {"Host": "bedrock-runtime.us-east-1.amazonaws.com"},
"body": {"messages": [{"role": "user", "content": [{"type": "text", "text": "ping"}]}]},
},
"response": {"status": 200, "headers": {"Content-Type": "application/vnd.amazon.eventstream"}, "body": body},
}
def _seed_legacy(tmp_path: Path) -> None:
migrate_legacy_traces(tmp_path)
def _seed_dashboard_summary(
*,
session_id: str,
agent: str,
status: str,
record_count: int,
first_user: str,
updated_at: str,
date_key: str,
) -> str:
return json.dumps(
{
"id": session_id,
"summary_version": DASHBOARD_SUMMARY_VERSION,
"date": date_key,
"agent": agent,
"status": status,
"record_count": record_count,
"turn_count": record_count,
"updated_at": updated_at,
"first_user": first_user,
"last_response": "",
"model": "gpt-5.5",
"total_tokens": 0,
},
ensure_ascii=False,
separators=(",", ":"),
)
def test_dashboard_lists_sessions_across_agents(trace_db, tmp_path: Path) -> None:
claude_trace = tmp_path / "2026-05-20" / "trace_080000.jsonl"
agy_trace = tmp_path / "2026-05-20" / "trace_090000.jsonl"
_write_jsonl(claude_trace, [_anthropic_record()])
_write_jsonl(agy_trace, [_antigravity_record()])
_seed_legacy(tmp_path)
sessions = list_trace_sessions()
assert [session["agent"] for session in sessions] == ["Antigravity", "Claude Code"]
assert sessions[0]["first_user"] == "What model are you?"
assert sessions[0]["last_response"] == "I am Sonnet."
assert sessions[1]["input_tokens"] == 42
assert sessions[1]["output_tokens"] == 9
agents = list_trace_agents()
assert [(agent["label"], agent["sessions"]) for agent in agents] == [("Antigravity", 1), ("Claude Code", 1)]
def test_dashboard_indexes_sessions_in_sqlite(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
_write_jsonl(trace_path, [_anthropic_record()])
_seed_legacy(tmp_path)
sessions = list_trace_sessions()
assert trace_db.exists()
assert sessions[0]["record_count"] == 1
assert sessions[0]["first_user"] == "Explain this repository"
second_path = tmp_path / "2026-05-20" / "trace_081500.jsonl"
_write_jsonl(second_path, [_anthropic_record(turn=2)])
migrate_legacy_traces(tmp_path)
sessions = list_trace_sessions()
first_session = next(item for item in sessions if item["legacy_rel_path"] == "2026-05-20/trace_080000.jsonl")
payload = load_trace_session(first_session["id"])
assert len(sessions) == 2
assert payload is not None
assert payload["records"][0]["turn"] == 1
def test_dashboard_summarizes_null_usage_token_fields(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_082000.jsonl"
record = _anthropic_record()
record["response"]["body"]["usage"] = {
"input_tokens": 42,
"output_tokens": 9,
"cache_read_input_tokens": None,
"cache_creation_input_tokens": None,
}
_write_jsonl(trace_path, [record])
_seed_legacy(tmp_path)
summary = list_trace_sessions()[0]
assert summary["input_tokens"] == 42
assert summary["output_tokens"] == 9
assert summary["cache_read_tokens"] == 0
assert summary["cache_create_tokens"] == 0
assert summary["total_tokens"] == 51
def test_dashboard_load_session_can_page_sqlite_records(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
_write_jsonl(trace_path, [_anthropic_record(), _anthropic_record(turn=2), _anthropic_record(turn=3)])
_seed_legacy(tmp_path)
session_id = list_trace_sessions()[0]["id"]
payload = load_trace_session(session_id, record_limit=1, record_offset=1)
assert payload is not None
assert payload["session"]["record_count"] == 3
assert [record["turn"] for record in payload["records"]] == [2]
def test_dashboard_lists_stale_cached_summary_without_record_scan(trace_db, monkeypatch) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
conn = store._connect()
conn.execute(
"""
UPDATE sessions
SET status = 'active',
record_count = 25,
summary_json = ?
WHERE id = ?
""",
(
json.dumps(
{
"agent": "Claude Code",
"agent_key": "claude-code",
"record_count": 25,
"total_tokens": 500,
"first_user": "Cached prompt",
},
separators=(",", ":"),
),
session_id,
),
)
conn.commit()
def fail_load_records(*_args, **_kwargs):
raise AssertionError("list view must not load full records")
monkeypatch.setattr(store, "load_records", fail_load_records)
summary = list_trace_sessions()[0]
assert summary["id"] == session_id
assert summary["record_count"] == 25
assert summary["status"] == "active"
assert summary["first_user"] == "Cached prompt"
def test_dashboard_lists_uncached_session_without_record_scan(trace_db, monkeypatch) -> None:
store = get_trace_store()
session_id = store.create_session(client="codex", proxy_mode="reverse")
conn = store._connect()
conn.execute(
"UPDATE sessions SET status = 'complete', record_count = 7, summary_json = NULL WHERE id = ?",
(session_id,),
)
conn.commit()
def fail_load_records(*_args, **_kwargs):
raise AssertionError("list view must not load full records")
monkeypatch.setattr(store, "load_records", fail_load_records)
summary = list_trace_sessions()[0]
assert summary["id"] == session_id
assert summary["record_count"] == 7
assert summary["agent"] == "Codex"
assert summary["status"] == "complete"
def test_dashboard_list_bad_session_summary_does_not_empty_page(trace_db) -> None:
store = get_trace_store()
good_id = store.create_session(client="codex", proxy_mode="reverse")
bad_id = store.create_session(client="", proxy_mode="")
conn = store._connect()
conn.execute(
"""
UPDATE sessions
SET status = 'complete', record_count = 1, summary_json = ?
WHERE id = ?
""",
(
json.dumps({"agent": "Codex", "status": "complete", "total_tokens": 10}, separators=(",", ":")),
good_id,
),
)
conn.execute(
"""
UPDATE sessions
SET status = 'error', record_count = 1, summary_json = ?
WHERE id = ?
""",
(
json.dumps({"agent": "Unknown", "status": "error", "total_tokens": 0}, separators=(",", ":")),
bad_id,
),
)
conn.execute(
"""
INSERT INTO records (session_id, record_index, turn, timestamp, payload_json)
VALUES (?, 1, 1, ?, ?)
""",
(bad_id, "2026-05-20T11:00:00+00:00", "{not-json"),
)
conn.commit()
sessions = list_trace_sessions()
by_id = {session["id"]: session for session in sessions}
assert good_id in by_id
assert bad_id in by_id
assert by_id[bad_id]["agent"] == "Unknown"
assert by_id[bad_id]["record_count"] == 1
assert by_id[bad_id]["status"] == "error"
def test_dashboard_agent_buckets_use_aggregate_query(trace_db, monkeypatch) -> None:
store = get_trace_store()
codex_id = store.create_session(client="codex", proxy_mode="reverse")
claude_id = store.create_session(client="claude", proxy_mode="reverse")
conn = store._connect()
conn.execute(
"""
UPDATE sessions
SET status = 'complete', record_count = 3, summary_json = ?
WHERE id = ?
""",
(
_seed_dashboard_summary(
session_id=codex_id,
agent="Codex",
status="complete",
record_count=3,
first_user="Codex prompt",
updated_at="2026-06-01T10:00:00+00:00",
date_key="2026-06-01",
),
codex_id,
),
)
conn.execute(
"""
UPDATE sessions
SET status = 'complete', record_count = 2, summary_json = ?
WHERE id = ?
""",
(
_seed_dashboard_summary(
session_id=claude_id,
agent="Claude Code",
status="complete",
record_count=2,
first_user="Claude prompt",
updated_at="2026-06-01T11:00:00+00:00",
date_key="2026-06-01",
),
claude_id,
),
)
conn.commit()
def fail_list_session_rows(*_args, **_kwargs):
raise AssertionError("agent buckets must not load every session row")
monkeypatch.setattr(store, "list_session_rows", fail_list_session_rows)
agents = list_trace_agents()
assert [(agent["label"], agent["sessions"], agent["records"]) for agent in agents] == [
("Claude Code", 1, 2),
("Codex", 1, 3),
]
def test_dashboard_detail_reads_from_sqlite(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
_write_jsonl(trace_path, [_anthropic_record()])
_seed_legacy(tmp_path)
session_id = list_trace_sessions()[0]["id"]
payload = load_trace_session(session_id)
assert payload is not None
assert payload["records"][0]["request_id"] == "req_claude"
def test_dashboard_first_message_uses_first_user_prompt(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_100000.jsonl"
_write_jsonl(
trace_path,
[
{
"timestamp": "2026-05-20T10:00:00+00:00",
"turn": 1,
"request": {
"method": "POST",
"path": "/v1/responses",
"body": {
"model": "gpt-5.5",
"input": [
{"role": "developer", "content": [{"type": "input_text", "text": "developer setup"}]},
{"type": "function_call_output", "output": "tool result"},
{
"role": "user",
"content": [{"type": "input_text", "text": "# AGENTS.md instructions\nSkip"}],
},
{"role": "user", "content": [{"type": "input_text", "text": "What is this project?"}]},
],
},
},
"response": {"status": 200, "body": {"model": "gpt-5.5", "usage": {"input_tokens": 1}}},
}
],
)
_seed_legacy(tmp_path)
summary = list_trace_sessions()[0]
assert summary["first_user"] == "What is this project?"
def test_dashboard_first_message_skips_injected_user_content_blocks(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_101500.jsonl"
_write_jsonl(
trace_path,
[
{
"timestamp": "2026-05-20T10:15:00+00:00",
"turn": 1,
"request": {
"method": "POST",
"path": "/v1/messages",
"body": {
"model": "claude-sonnet-4-6",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "<system-reminder>\nInjected context\n</system-reminder>",
},
{"type": "text", "text": "Fix the failing dashboard prompt preview."},
],
}
],
},
},
"response": {"status": 200, "body": {"model": "claude-sonnet-4-6", "usage": {"input_tokens": 1}}},
}
],
)
_seed_legacy(tmp_path)
summary = list_trace_sessions()[0]
assert summary["first_user"] == "Fix the failing dashboard prompt preview."
def test_dashboard_recomputes_stale_first_message_summary_cache(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(
client="claude",
proxy_mode="reverse",
started_at=datetime(2026, 5, 20, 10, 15, tzinfo=timezone.utc),
)
store.append_record(
session_id,
{
"timestamp": "2026-05-20T10:15:00+00:00",
"turn": 1,
"request": {
"method": "POST",
"path": "/v1/messages",
"body": {
"model": "claude-sonnet-4-6",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<system-reminder>\nInjected context\n</system-reminder>"},
{"type": "tool_result", "content": "stale tool output"},
{"type": "text", "text": "Fix the failing dashboard prompt preview."},
],
}
],
},
},
"response": {"status": 200, "body": {"model": "claude-sonnet-4-6", "usage": {"input_tokens": 1}}},
},
)
stale_summary = {
"id": session_id,
"status": "complete",
"record_count": 1,
"updated_at": "2026-05-20T10:15:00+00:00",
"first_user": "stale tool output",
}
conn = store._connect()
conn.execute(
"UPDATE sessions SET status = 'complete', summary_json = ? WHERE id = ?",
(json.dumps(stale_summary, ensure_ascii=False, separators=(",", ":")), session_id),
)
conn.commit()
summary = list_trace_sessions()[0]
cached = json.loads(store.load_session_row(session_id)["summary_json"])
assert summary["first_user"] == "Fix the failing dashboard prompt preview."
assert cached["first_user"] == "Fix the failing dashboard prompt preview."
assert cached["summary_version"] == DASHBOARD_SUMMARY_VERSION
def test_dashboard_recomputes_stale_active_summary_cache_on_append(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(
client="claude",
proxy_mode="reverse",
started_at=datetime(2026, 5, 20, 10, 15, tzinfo=timezone.utc),
)
first_record = {
"timestamp": "2026-05-20T10:15:00+00:00",
"turn": 1,
"request": {
"method": "POST",
"path": "/v1/messages",
"body": {
"model": "claude-sonnet-4-6",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<system-reminder>\nInjected context\n</system-reminder>"},
{"type": "tool_result", "content": "stale tool output"},
{"type": "text", "text": "Fix the active dashboard prompt preview."},
],
}
],
},
},
"response": {"status": 200, "body": {"model": "claude-sonnet-4-6", "usage": {"input_tokens": 1}}},
}
store.append_record(session_id, first_record)
stale_summary = {
"id": session_id,
"status": "active",
"record_count": 1,
"updated_at": "2026-05-20T10:15:00+00:00",
"first_user": "stale tool output",
}
conn = store._connect()
conn.execute(
"UPDATE sessions SET status = 'active', summary_json = ? WHERE id = ?",
(json.dumps(stale_summary, ensure_ascii=False, separators=(",", ":")), session_id),
)
conn.commit()
store.append_record(
session_id,
{
"timestamp": "2026-05-20T10:16:00+00:00",
"turn": 2,
"request": {"method": "POST", "path": "/v1/messages", "body": {"model": "claude-sonnet-4-6"}},
"response": {"status": 200, "body": {"model": "claude-sonnet-4-6", "usage": {"input_tokens": 1}}},
},
)
summary = list_trace_sessions(current_session_id=session_id)[0]
cached = json.loads(store.load_session_row(session_id)["summary_json"])
assert summary["first_user"] == "Fix the active dashboard prompt preview."
assert cached["first_user"] == "Fix the active dashboard prompt preview."
assert cached["summary_version"] == DASHBOARD_SUMMARY_VERSION
assert cached["record_count"] == 2
def test_dashboard_loads_session_by_id(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
_write_jsonl(trace_path, [_anthropic_record()])
_seed_legacy(tmp_path)
session_id = list_trace_sessions()[0]["id"]
payload = load_trace_session(session_id)
assert payload is not None
assert payload["session"]["legacy_rel_path"] == "2026-05-20/trace_080000.jsonl"
assert payload["records"][0]["request_id"] == "req_claude"
def test_dashboard_rejects_missing_session_ids(trace_db) -> None:
template = read_dashboard_template()
assert "session-list" in template
assert "lang-select" in template
assert "DASHBOARD_I18N" in template
assert 'data-i18n="table_first_message"' in template
assert "export_jsonl" in template
assert 'export_compact: "Export JSON"' in template
assert "export_log" not in template
assert "export_html" in template
assert "export_menu" in template
assert load_trace_session("not-a-valid-session-id") is None
def test_dashboard_detail_navigation_uses_lazy_shell_route() -> None:
template = read_dashboard_template()
assert "function sessionDetailUrl(sessionId)" in template
assert "window.location.assign(sessionDetailUrl(sessionId))" in template
assert "/dashboard/session/" in template
assert "detailRecordTotal: 0" in template
assert 'detailFingerprint: ""' in template
assert "detailSession: null" in template
assert 'activeTab: "raw"' in template
assert "function detailRecordFetchLimit(sessionId, preserveLoaded)" in template
assert "function sessionDetailFingerprint(session)" in template
assert "function updateDetailSessionSummary(session)" in template
assert "function updateDetailI18n(session)" in template
assert 'params.set("search", search)' in template
assert 'params.set("agent", state.selectedAgent)' in template
assert "function refreshForFilters()" in template
assert 'state.view === "detail" && state.selectedSessionId' in template
assert "const detailLoaded = state.detailSessionId === state.selectedSessionId" in template
assert "updateDetailSessionSummary(selected)" in template
assert "updateDetailI18n(state.detailSession)" in template
assert "knownTotal > previousTotal && previousTotal <= loadedRecords" in template
assert "const limit = detailRecordFetchLimit(sessionId, preserveLoaded)" in template
assert "state.detailRecordTotal = totalRecords" in template
assert "state.detailFingerprint = sessionDetailFingerprint(session)" in template
assert "function ensureViewerFrame(session)" in template
assert "data-tab-toggle" in template
assert 'container.querySelector("[data-viewer-frame]")' in template
assert "setDetailTab(event.currentTarget.dataset.tab, session)" in template
def test_dashboard_template_exposes_session_delete_controls() -> None:
template = read_dashboard_template()
assert 'data-i18n="table_actions"' in template
assert 'id="edit-sessions"' in template
assert 'id="select-all-sessions"' in template
assert 'id="delete-selected-sessions"' in template
assert "data-select-session" in template
assert "delete-session-modal" in template
assert "data-delete-session" not in template
assert "delete_active_session_title" in template
assert "session.active" in template
assert "function isSessionRowActionTarget(target)" in template
assert "event.target !== row" in template
assert "function confirmDeleteSession()" in template
assert "body: JSON.stringify({session_ids: sessionIds})" in template
assert 'method: "DELETE"' in template
def test_dashboard_template_exposes_quit_control() -> None:
template = read_dashboard_template()
assert 'id="logo-version"' in template
assert 'const CLAUDE_TAP_VERSION = "";' in template
assert "function applyVersionBadge()" in template
assert 'id="dashboard-quit"' in template
assert "quit_dashboard_confirm" in template
assert "Stop dashboard service" in template
assert "function quitDashboard()" in template
assert 'const DASHBOARD_QUIT_TOKEN = "";' in template
assert "const DASHBOARD_CAN_STOP = false;" in template
assert '"X-Claude-Tap-Dashboard-Token": DASHBOARD_QUIT_TOKEN' in template
assert "let dashboardEvents = null;" in template
assert "function closeDashboardEvents()" in template
assert "if (state.quittingDashboard) return;" in template
def test_dashboard_summarize_session_and_migration(trace_db, tmp_path: Path) -> None:
assert dashboard_trace_snapshot() == {}
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
trace_path.parent.mkdir(parents=True, exist_ok=True)
trace_path.write_text(
'\nnot-json\n[]\n{"request_id":"ok","request":{},"response":{}}\n',
encoding="utf-8",
)
manifest_path = tmp_path / ".cloudtap-manifest.json"
manifest_path.write_text(
json.dumps(
{
"traces": [
{
"client": "kimi",
"files": ["2026-05-20/trace_080000.jsonl"],
"created_at": "2026-05-20T00:00:00+00:00",
},
]
}
),
encoding="utf-8",
)
_seed_legacy(tmp_path)
summary = list_trace_sessions()[0]
assert summary["agent"] == "Kimi"
assert summary["record_count"] == 1
def test_dashboard_parses_provider_fallbacks(trace_db, tmp_path: Path) -> None:
html_trace = tmp_path / "2026-05-20" / "trace_090000.jsonl"
html_trace.parent.mkdir(parents=True, exist_ok=True)
_write_jsonl(html_trace, [_antigravity_record()])
_seed_legacy(tmp_path)
session_id = list_trace_sessions()[0]["id"]
summary = list_trace_sessions(current_session_id=session_id)[0]
assert summary["status"] == "active"
assert summary["model"] == "unknown"
provider_cases = [
({"metadata": {"client": "agy"}}, [], "Antigravity"),
({}, [{"capture": {"client": "cursor"}}], "Cursor"),
({}, [{"request": {"headers": {"host": "generativelanguage.googleapis.com"}}}], "Gemini"),
({}, [{"request": {"path": "/v1/responses"}}], "Codex"),
({}, [{"request": {"headers": {"Host": "api.moonshot.cn"}}}], "Kimi"),
({}, [{"request": {"headers": {"Host": "qoder.example"}}}], "Qoder"),
({}, [{"request": {"headers": {"Host": "opencode.example"}}}], "OpenCode"),
({}, [{"request": {"headers": {"Host": "mimo.xiaomi.example"}}}], "MiMo Code"),
({}, [{"request": {"headers": {"Host": "hermes.example"}}}], "Hermes"),
({}, [{"upstream_base_url": "https://api.anthropic.com/v1"}], "Claude Code"),
({}, [], "Unknown"),
]
for manifest_entry, records, expected in provider_cases:
assert _infer_agent(records, manifest_entry) == expected
assert _record_host({"request": {"headers": {"host": "lowercase.example"}}}) == "lowercase.example"
assert _record_host({"upstream_base_url": "https://upstream.example/path"}) == "upstream.example"
def test_dashboard_extracts_usage_models_errors_and_text() -> None:
assert _record_usage({"response": {"body": {"usageMetadata": {"promptTokenCount": 3}}}})["input_tokens"] == 3
assert (
_record_usage(
{"response": {"ws_events": [{"data": '{"response":{"usage":{"input_tokens":4,"output_tokens":2}}}'}]}}
)["output_tokens"]
== 2
)
assert _record_usage({"response": {"body": {"input_tokens": 5}}})["input_tokens"] == 5
assert _record_model({"request": {"body": {"modelId": "gemini-3.1"}}}) == "gemini-3.1"
assert _record_model({"request": {"body": {"request": {"model": "sonnet-4-6"}}}}) == "sonnet-4-6"
assert _record_model({"response": {"body": {"model": "gpt-oss"}}}) == "gpt-oss"
assert _record_model({"request": {"path": "/v1beta/models/gemini-pro:generateContent"}}) == "gemini-pro"
assert _record_model({}) == ""
assert _first_error([{"response": {"error": "failed hard"}}]) == "failed hard"
assert _first_error([{"response": {"body": {"error": "body failed"}}}]) == "body failed"
assert _first_error([{"response": {"body": {"error": {"message": "nested failed"}}}}]) == "nested failed"
assert _first_error([{"response": {"body": {}}}]) == ""
assert _request_user_text("raw prompt") == "raw prompt"
assert _request_user_text(None) == ""
assert _request_user_text({"prompt": "fallback prompt"}) == "fallback prompt"
assert _request_user_text({"messages": [{"role": "user", "content": "<session>\nwrapped prompt\n</session>"}]}) == (
"wrapped prompt"
)
assert (
_request_user_text(
{
"request": {
"contents": [
{"role": "user", "parts": [{"text": "<session_context>\ncontext\n</session_context>"}]},
{"role": "user", "parts": [{"text": "actual Gemini prompt"}]},
]
}
}
)
== "actual Gemini prompt"
)
assert (
_request_user_text(
{
"request": {
"contents": [
{
"role": "user",
"parts": [
{
"text": "<USER_REQUEST>\n--print-timeout\n</USER_REQUEST>\n"
"<ADDITIONAL_METADATA>time</ADDITIONAL_METADATA>"
}
],
}
]
}
}
)
== "--print-timeout"
)
assert (
_request_user_text(
{
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<USER_REQUEST>\n--print-timeout\n</USER_REQUEST>"},
{"type": "text", "text": "<ADDITIONAL_METADATA>time</ADDITIONAL_METADATA>"},
],
}
]
}
)
== "--print-timeout"
)
assert _request_user_text({"input": [{"type": "message", "content": [{"text": "input text"}]}]}) == "input text"
assert (
_request_user_text(
{
"input": [
{"role": "developer", "content": [{"type": "input_text", "text": "developer setup"}]},
{"type": "function_call_output", "output": "tool result"},
{"role": "user", "content": [{"type": "input_text", "text": "raw user prompt"}]},
]
}
)
== "raw user prompt"
)
assert (
_request_user_text(
{
"input": [
{
"role": "user",
"content": [
{"type": "text", "text": "<system-reminder>\nskip\n</system-reminder>"},
{"type": "text", "text": "actual response prompt"},
],
}
]
}
)
== "actual response prompt"
)
assert _request_user_text({"messages": [{"role": "user", "content": ["hello", {"text": "world"}]}]}) == (
"hello\nworld"
)
assert (
_request_user_text(
{
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<system-reminder>\nskip\n</system-reminder>"},
{"type": "text", "text": "actual message prompt"},
],
}
]
}
)
== "actual message prompt"
)
assert (
_request_user_text(
{
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<system-reminder>\nskip\n</system-reminder>"},
{"type": "tool_result", "content": "tool output should not be first prompt"},
{"type": "function_call_output", "output": "function output should not be first prompt"},
{"type": "text", "text": "actual prompt after tools"},
],
}
]
}
)
== "actual prompt after tools"
)
assert (
_request_user_text(
{"contents": [{"role": "model", "parts": [{"text": "skip"}]}, {"role": "USER", "parts": [{"text": "use"}]}]}
)
== "use"
)
assert (
_request_user_text(
{
"contents": [
{
"role": "user",
"parts": [
{"text": "<session_context>\nskip\n</session_context>"},
{"text": "actual gemini prompt"},
],
}
]
}
)
== "actual gemini prompt"
)
assert _response_text("raw response") == "raw response"
assert _response_text(None) == ""
assert _response_text({"choices": [{"message": {"content": "choice"}}]}) == "choice"
assert _response_text({"choices": [{"delta": {"content": [{"text": "delta"}]}}]}) == "delta"
assert _response_text({"output": [{"output_text": "out"}]}) == "out"
assert _response_text({"response": {"content": "response field"}}) == "response field"
assert _content_text({"text": ["nested", {"content": "dict"}]}) == "nested\ndict"
assert _content_text({"input_text": "typed prompt"}) == "typed prompt"
assert _content_text([{"type": "message", "content": [{"output_text": "message text"}]}]) == "message text"
assert _input_user_text([{"role": "developer", "content": "dev"}, {"content": "implicit user"}]) == "implicit user"
assert _clean_user_prompt_text('"quoted prompt"') == "quoted prompt"
assert _clean_user_prompt_text("<system-reminder>\nskip\n</system-reminder>") == ""
assert _parts_text("not-list") == ""
assert _preview(" a \n b ", 20) == "a b"
assert _preview("abcdef", 4) == "abc..."
assert _response_events({"response": "bad"}) == []
assert _response_events({"response": {"sse_events": [{"data": "{}"}, "bad"]}}) == [{"data": "{}"}]
assert _event_payload({"data": "not-json"}) == {}
assert _event_payload({"data": {"response": {"content": "payload"}}}) == {"content": "payload"}
assert _event_payload({"data": 1}) == {}
assert _record_response_text({"response": {"body": "body text"}}) == "body text"
assert (
_record_response_text(
{"response": {"ws_events": [{"item": {"content": "item text"}}, {"part": {"text": "part text"}}]}}
)
== "part text"
)
assert _record_response_text({"response": {"ws_events": [{"text": "event text"}]}}) == "event text"
assert (
_record_response_text({"response": {"ws_events": [{"data": '{"content":"payload text"}'}]}}) == "payload text"
)
assert _record_response_text({"response": {}}) == ""
def test_dashboard_preview_skips_auxiliary_auth_records(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_100000.jsonl"
_write_jsonl(
trace_path,
[
{
"timestamp": "2026-05-20T10:00:00+00:00",
"turn": 1,
"request": {
"method": "POST",
"path": "/token",
"body": "refresh_token=secret-token&client_id=client",
},
"response": {"status": 200, "body": {}},
},
{
"timestamp": "2026-05-20T10:00:01+00:00",
"turn": 2,
"request": {"method": "POST", "path": "/log?format=json", "body": {}},
"response": {"status": 403, "body": "<!DOCTYPE html> challenge page"},
},
{
"timestamp": "2026-05-20T10:00:02+00:00",
"turn": 3,
"request": {
"method": "POST",
"path": "/v1internal:streamGenerateContent?alt=sse",
"headers": {"Host": "generativelanguage.googleapis.com"},
"body": {
"request": {
"contents": [
{
"role": "user",
"parts": [{"text": "Gemini dashboard prompt"}],
}
]
}
},
},
"response": {
"status": 200,
"body": {
"candidates": [
{
"content": {
"parts": [{"text": "Gemini dashboard response."}],
}
}
]
},
},
},
],
)
_seed_legacy(tmp_path)
summary = list_trace_sessions()[0]
assert summary["first_user"] == "Gemini dashboard prompt"
assert summary["last_response"] == "Gemini dashboard response."
assert summary["status"] == "complete"
@pytest.mark.asyncio
async def test_dashboard_server_serves_session_api_and_exports(trace_db, tmp_path: Path) -> None:
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
second_trace_path = tmp_path / "2026-05-20" / "trace_081500.jsonl"
_write_jsonl(trace_path, [_anthropic_record()])
_write_jsonl(second_trace_path, [_anthropic_record(turn=2), _anthropic_record(turn=3)])
trace_path.with_suffix(".log").write_text("10:00:00 proxy log\n", encoding="utf-8")
_seed_legacy(tmp_path)
server = LiveViewerServer(port=0, migrate_from=tmp_path, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/") as resp:
assert resp.status == 200
html = await resp.text()
assert "session-list" in html
assert "export_jsonl" in html
assert 'export_compact: "Export JSON"' in html
assert "export_log" not in html
assert "export_html" in html
assert "export_menu" in html
async with session.get(f"http://127.0.0.1:{port}/api/sessions") as resp:
assert resp.status == 200
payload = await resp.json()
assert len(payload["sessions"]) == 2
second_session_id = next(item["id"] for item in payload["sessions"] if item["record_count"] == 2)
session_id = next(item["id"] for item in payload["sessions"] if item["record_count"] == 1)
async with session.get(
f"http://127.0.0.1:{port}/dashboard?session_id={session_id}",
allow_redirects=False,
) as resp:
assert resp.status == 302
assert resp.headers["Location"] == f"/dashboard/session/{session_id}"
async with session.get(f"http://127.0.0.1:{port}/dashboard/session/{session_id}") as resp:
assert resp.status == 200
html = await resp.text()
assert "session-list" in html
assert "back-to-list" not in html
assert "EMBEDDED_TRACE_COMPACT_DATA" not in html
assert "req_claude" not in html
assert "/api/sessions/${encodeURIComponent(session.id)}/html" in html
async with session.get(f"http://127.0.0.1:{port}/api/agents") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["agents"][0]["label"] == "Claude Code"
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/records") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["records"][0]["request_id"] == "req_claude"
async with session.get(
f"http://127.0.0.1:{port}/api/sessions/{session_id}/html",
allow_redirects=False,
) as resp:
assert resp.status == 200
html = await resp.text()
assert "EMBEDDED_TRACE_META" in html
assert "const EMBEDDED_TRACE_COMPACT_DATA =" not in html
assert f'const __TRACE_RECORDS_API__ = "/api/sessions/{session_id}/records";' in html
assert "req_claude" in html
assert f'const __TRACE_JSONL_PATH__ = "/api/sessions/{session_id}/export/compact";' in html
assert f'const __TRACE_HTML_PATH__ = "/dashboard/session/{session_id}";' in html
assert f"session-{session_id[:8]}.jsonl" not in html
assert f"session-{session_id[:8]}.html" not in html
assert f"/api/sessions/{session_id}/export/html" in html
assert f"/api/sessions/{session_id}/export/log" not in html
async with session.get(
f"http://127.0.0.1:{port}/api/sessions/{second_session_id}/records?offset=1&limit=1"
) as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["session"]["record_count"] == 2
assert [record["turn"] for record in payload["records"]] == [3]
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/export/jsonl") as resp:
assert resp.status == 200
body = await resp.text()
assert "req_claude" in body
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/export/compact") as resp:
assert resp.status == 200
assert resp.content_type == "application/json"
body = await resp.text()
assert "__claude_tap_compact_trace__" in body
assert "req_claude" in body
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/export/log") as resp:
assert resp.status == 200
assert resp.content_type == "text/plain"
assert resp.charset == "utf-8"
body = await resp.text()
assert body == "10:00:00 proxy log\n"
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/export/html") as resp:
assert resp.status == 200
assert resp.content_type == "text/html"
assert resp.charset == "utf-8"
assert f'filename="trace_{session_id[:8]}.html"' in resp.headers["Content-Disposition"]
html = await resp.text()
assert "EMBEDDED_TRACE_COMPACT_DATA" in html
assert "const EMBEDDED_TRACE_DATA =" not in html
assert "req_claude" in html
assert f'const __TRACE_JSONL_PATH__ = "/api/sessions/{session_id}/export/compact";' in html
assert f'const __TRACE_HTML_PATH__ = "/api/sessions/{session_id}/export/html";' in html
assert f"session-{session_id[:8]}.jsonl" not in html
async with session.delete(f"http://127.0.0.1:{port}/api/sessions/{second_session_id}") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["deleted_sessions"] == 1
assert payload["deleted_records"] == 2
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{second_session_id}/records") as resp:
assert resp.status == 404
async with session.get(f"http://127.0.0.1:{port}/api/sessions") as resp:
assert resp.status == 200
payload = await resp.json()
assert [item["id"] for item in payload["sessions"]] == [session_id]
async with session.get(f"http://127.0.0.1:{port}/api/sessions/bad/records") as resp:
assert resp.status == 404
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_export_html_normalizes_bedrock_eventstream(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(session_id, _bedrock_stream_record())
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/export/html") as resp:
assert resp.status == 200
html = await resp.text()
finally:
await server.stop()
assert "EMBEDDED_TRACE_COMPACT_DATA" in html
assert '"sse_events":' in html
assert '"content":[{"type":"text","text":"OK"}]' in html
assert "binary-prefix" not in html
@pytest.mark.asyncio
async def test_dashboard_session_detail_redacts_sensitive_display_records(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="agy", proxy_mode="reverse")
store.append_record(
session_id,
{
"timestamp": "2026-05-20T10:00:00+00:00",
"request_id": "req_auth",
"turn": 1,
"request": {
"method": "POST",
"path": (
"/login?redirect_uri=%2Foauth%2Fcallback%3Faccess_token%3Dredirect-secret"
"&access_token=path-secret&client_id=public-client"
),
"url": "https://oauth.example/callback?id_token=url-secret&state=ok",
"body": (
"client_id=public-client&client_secret=client-secret&refresh_token=refresh-secret"
"&redirect_uri=/oauth/callback?access_token=nested-secret"
),
},
"response": {
"status": 200,
"body": {
"access_token": "access-secret",
"usage": {"input_tokens": 3, "output_tokens": 1},
},
},
},
)
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/records") as resp:
assert resp.status == 200
payload = await resp.json()
record = payload["records"][0]
assert "redirect-secret" not in record["request"]["path"]
assert "path-secret" not in record["request"]["path"]
assert "redirect_uri=%2Foauth%2Fcallback%3Faccess_token%3DREDACTED" in record["request"]["path"]
assert "access_token=REDACTED" in record["request"]["path"]
assert record["request"]["url"] == "https://oauth.example/callback?id_token=REDACTED&state=ok"
assert "client_secret=REDACTED" in record["request"]["body"]
assert "refresh_token=REDACTED" in record["request"]["body"]
assert "nested-secret" not in record["request"]["body"]
assert "access_token%3DREDACTED" in record["request"]["body"]
assert record["response"]["body"]["access_token"] == "REDACTED"
assert record["response"]["body"]["usage"]["input_tokens"] == 3
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/html") as resp:
assert resp.status == 200
html = await resp.text()
assert "client-secret" not in html
assert "refresh-secret" not in html
assert "access-secret" not in html
assert "path-secret" not in html
assert "url-secret" not in html
assert "redirect-secret" not in html
assert "nested-secret" not in html
assert "REDACTED" in html
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/export/jsonl") as resp:
assert resp.status == 200
body = await resp.text()
assert "client-secret" in body
assert "refresh-secret" in body
assert "access-secret" in body
assert "path-secret" in body
assert "url-secret" in body
assert "redirect-secret" in body
assert "nested-secret" in body
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_session_html_uses_remote_records_for_long_codex_prompt(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="codexapp", proxy_mode="transcript")
long_prompt = ("sandbox_permissions=" * 1200) + "done"
store.append_record(
session_id,
{
"timestamp": "2026-06-13T09:00:00+00:00",
"request_id": "req_codex_app",
"turn": 1,
"request": {
"method": "CODEX_APP_TRANSCRIPT",
"path": "/v1/responses",
"headers": {"x-codex-app-session-id": "019ec061-b6cd-74b0-abdf-1d51267d1355"},
"body": {
"type": "response.create",
"model": "gpt-5.5",
"input": [
{
"type": "message",
"role": "developer",
"content": [{"type": "input_text", "text": long_prompt}],
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Add Codex App listening"}],
},
],
},
},
"response": {"status": 200, "body": {"usage": {"input_tokens": 100, "output_tokens": 20}}},
},
)
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/html") as resp:
assert resp.status == 200
html = await resp.text()
assert len(html) < 500_000
assert "EMBEDDED_TRACE_META" in html
assert "__TRACE_RECORDS_API__" in html
assert "const EMBEDDED_TRACE_COMPACT_DATA =" not in html
assert long_prompt not in html
assert "Add Codex App listening" in html
async with session.get(
f"http://127.0.0.1:{port}/api/sessions/{session_id}/records?offset=0&limit=1"
) as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["records"][0]["request"]["body"]["input"][0]["content"][0]["text"] == long_prompt
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_summary_preview_fields_are_redacted(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(
session_id,
{
"timestamp": "2026-05-20T10:00:00+00:00",
"request_id": "req_summary",
"turn": 1,
"request": {
"method": "POST",
"path": "/v1/messages",
"body": "client_id=public-client&client_secret=summary-secret&prompt=hi",
},
"response": {
"status": 200,
"body": "message=ok&refresh_token=response-secret",
},
},
)
cached = json.loads(store.load_session_row(session_id)["summary_json"])
listed = next(item for item in list_trace_sessions() if item["id"] == session_id)
assert "summary-secret" not in cached["first_user"]
assert "response-secret" not in cached["last_response"]
assert "summary-secret" not in listed["first_user"]
assert "response-secret" not in listed["last_response"]
assert "client_secret=REDACTED" in listed["first_user"]
assert "refresh_token=REDACTED" in listed["last_response"]
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/records") as resp:
assert resp.status == 200
payload = await resp.json()
assert "summary-secret" not in payload["session"]["first_user"]
assert "response-secret" not in payload["session"]["last_response"]
assert "client_secret=REDACTED" in payload["session"]["first_user"]
assert "refresh_token=REDACTED" in payload["session"]["last_response"]
async with session.get(f"http://127.0.0.1:{port}/api/sessions/{session_id}/export/jsonl") as resp:
assert resp.status == 200
body = await resp.text()
assert "summary-secret" in body
assert "response-secret" in body
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_session_filters_apply_before_paging(trace_db) -> None:
store = get_trace_store()
conn = store._connect()
for index in range(120):
session_id = store.create_session(
client="claude",
proxy_mode="reverse",
started_at=datetime(2026, 6, 1, 12, index % 60, tzinfo=timezone.utc),
)
updated_at = f"2026-06-01T12:{index % 60:02d}:{index // 60:02d}+00:00"
conn.execute(
"""
UPDATE sessions
SET status = 'complete',
record_count = 1,
updated_at = ?,
date_key = '2026-06-01',
summary_json = ?
WHERE id = ?
""",
(
updated_at,
_seed_dashboard_summary(
session_id=session_id,
agent="Claude Code",
status="complete",
record_count=1,
first_user=f"Noise prompt {index}",
updated_at=updated_at,
date_key="2026-06-01",
),
session_id,
),
)
target_id = store.create_session(
client="codex",
proxy_mode="reverse",
started_at=datetime(2026, 5, 1, 8, 0, tzinfo=timezone.utc),
)
conn.execute(
"""
UPDATE sessions
SET status = 'error',
record_count = 7,
started_at = '2026-05-01T08:00:00+00:00',
updated_at = '2026-05-01T08:00:00+00:00',
date_key = '2026-05-01',
summary_json = ?
WHERE id = ?
""",
(
_seed_dashboard_summary(
session_id=target_id,
agent="Codex",
status="error",
record_count=7,
first_user="Find me past the first page",
updated_at="2026-05-01T08:00:00+00:00",
date_key="2026-05-01",
),
target_id,
),
)
conn.commit()
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/api/sessions?limit=100") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["total"] == 121
assert payload["total_records"] == 127
assert target_id not in {item["id"] for item in payload["sessions"]}
for query in (
"search=Find%20me%20past",
"date=2026-05-01",
"status=error",
"agent=codex",
):
async with session.get(f"http://127.0.0.1:{port}/api/sessions?limit=10&{query}") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["total"] == 1
assert payload["total_records"] == 7
assert [item["id"] for item in payload["sessions"]] == [target_id]
assert "2026-05-01" in payload["dates"]
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_server_sse_events(trace_db) -> None:
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(f"http://127.0.0.1:{port}/api/agents") as resp:
assert resp.status == 200
assert await resp.json() == {"agents": []}
async with session.get(f"http://127.0.0.1:{port}/api/sessions") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["sessions"] == []
assert payload["total"] == 0
async with session.get(f"http://127.0.0.1:{port}/api/sessions/anything/records") as resp:
assert resp.status == 404
async with session.get(f"http://127.0.0.1:{port}/dashboard/events") as resp:
assert resp.status == 200
assert await asyncio.wait_for(resp.content.readline(), timeout=1) == b"event: ready\n"
ready_data = await asyncio.wait_for(resp.content.readline(), timeout=1)
assert b'"type":"ready"' in ready_data
assert await asyncio.wait_for(resp.content.readline(), timeout=1) == b"\n"
await server._broadcast_dashboard_event({"type": "refresh"})
assert await asyncio.wait_for(resp.content.readline(), timeout=1) == b"event: refresh\n"
refresh_data = await asyncio.wait_for(resp.content.readline(), timeout=1)
assert b'"type":"refresh"' in refresh_data
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_server_quit_route_stops_dashboard(trace_db) -> None:
from claude_tap.shared_dashboard import CLAUDE_TAP_VERSION, is_dashboard_healthy, wait_for_dashboard_stopped
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(f"http://127.0.0.1:{port}/dashboard") as resp:
assert resp.status == 200
html = await resp.text()
assert f'const CLAUDE_TAP_VERSION = "{CLAUDE_TAP_VERSION}";' in html
assert f'const DASHBOARD_QUIT_TOKEN = "{server._dashboard_quit_token}";' in html
assert "const DASHBOARD_CAN_STOP = true;" in html
async with session.post(f"http://127.0.0.1:{port}/dashboard/quit") as resp:
assert resp.status == 403
payload = await resp.json()
assert payload["ok"] is False
async with session.get(f"http://127.0.0.1:{port}/dashboard/health") as resp:
assert resp.status == 200
health = await resp.json()
assert health["version"] == CLAUDE_TAP_VERSION
assert health["quit_token"] == server._dashboard_quit_token
async with session.post(
f"http://127.0.0.1:{port}/dashboard/quit",
headers={"X-Claude-Tap-Dashboard-Token": health["quit_token"]},
) as resp:
assert resp.status == 200
assert await resp.json() == {"ok": True}
assert await wait_for_dashboard_stopped("127.0.0.1", port, timeout=2.0) is True
assert await is_dashboard_healthy("127.0.0.1", port, require_current_db=False) is False
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_quit_token_requires_trusted_host_and_origin(trace_db) -> None:
from claude_tap.shared_dashboard import CLAUDE_TAP_VERSION, is_dashboard_healthy
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(
f"http://127.0.0.1:{port}/dashboard",
headers={"Host": f"attacker.example:{port}", "Origin": f"http://attacker.example:{port}"},
) as resp:
assert resp.status == 200
html = await resp.text()
assert f'const CLAUDE_TAP_VERSION = "{CLAUDE_TAP_VERSION}";' in html
assert 'const DASHBOARD_QUIT_TOKEN = "";' in html
assert "const DASHBOARD_CAN_STOP = false;" in html
assert "session-list" in html
async with session.get(
f"http://127.0.0.1:{port}/dashboard/health",
headers={"Host": f"attacker.example:{port}", "Origin": f"http://attacker.example:{port}"},
) as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["ok"] is True
assert payload["dashboard_mode"] is True
assert payload["version"] == CLAUDE_TAP_VERSION
assert "quit_token" not in payload
async with session.get(f"http://127.0.0.1:{port}/dashboard/health") as resp:
assert resp.status == 200
health = await resp.json()
token = health["quit_token"]
for headers in (
{"Host": f"attacker.example:{port}", "X-Claude-Tap-Dashboard-Token": token},
{
"Origin": f"http://attacker.example:{port}",
"X-Claude-Tap-Dashboard-Token": token,
},
{
"Origin": f"http://127.0.0.1:{port + 1}",
"X-Claude-Tap-Dashboard-Token": token,
},
):
async with session.post(f"http://127.0.0.1:{port}/dashboard/quit", headers=headers) as resp:
assert resp.status == 403
payload = await resp.json()
assert payload["ok"] is False
assert await is_dashboard_healthy("127.0.0.1", port, require_current_db=False) is True
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_quit_route_rejects_non_dashboard_server(trace_db) -> None:
server = LiveViewerServer(port=0, dashboard_mode=False)
port = await server.start()
try:
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(f"http://127.0.0.1:{port}/dashboard/quit") as resp:
assert resp.status == 403
payload = await resp.json()
assert payload["ok"] is False
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_session_route_serves_standalone_viewer(trace_db, tmp_path: Path) -> None:
playwright = pytest.importorskip("playwright.async_api")
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
_write_jsonl(trace_path, [_anthropic_record(turn=turn) for turn in range(1, 13)])
_seed_legacy(tmp_path)
session_id = list_trace_sessions()[0]["id"]
server = LiveViewerServer(port=0, migrate_from=tmp_path, dashboard_mode=True)
port = await server.start()
try:
async with playwright.async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
try:
page = await browser.new_page(accept_downloads=True)
await page.goto(
f"http://127.0.0.1:{port}/dashboard/session/{session_id}",
wait_until="domcontentloaded",
)
await page.wait_for_selector("#raw-tab .section", timeout=5000)
assert await page.locator(".header").count() == 1
assert await page.locator(".viewer-frame").count() == 0
assert await page.locator("#back-to-list").count() == 0
assert await page.locator("#list-view.hidden").count() == 1
assert await page.locator("#raw-tab .section").count() == 10
assert await page.locator("[data-load-more]").count() == 1
tab_toggle = page.locator("[data-tab-toggle]")
assert await tab_toggle.inner_text() == "Full viewer"
await tab_toggle.click()
await page.wait_for_selector("#conversation-tab:not(.hidden) .viewer-frame", timeout=5000)
assert await page.locator(".viewer-frame").count() == 1
frame = page.frame_locator(".viewer-frame")
await frame.locator(".sidebar-item").first.wait_for(timeout=5000)
await frame.locator("#detail .section").first.wait_for(timeout=5000)
assert not await frame.locator("#drop-zone").is_visible()
await page.locator(".viewer-frame").evaluate("(frame) => { frame.dataset.reuseMarker = 'kept'; }")
assert await tab_toggle.inner_text() == "Trace"
await tab_toggle.click()
await page.wait_for_selector("#raw-tab:not(.hidden) .section", timeout=5000)
assert await page.locator(".viewer-frame").count() == 1
assert await page.locator(".viewer-frame").get_attribute("data-reuse-marker") == "kept"
assert await tab_toggle.inner_text() == "Full viewer"
await tab_toggle.click()
await page.wait_for_selector("#conversation-tab:not(.hidden) .viewer-frame", timeout=5000)
assert await page.locator(".viewer-frame").count() == 1
assert await page.locator(".viewer-frame").get_attribute("data-reuse-marker") == "kept"
export_button = page.locator(".detail-inspector-bar .export-menu > summary")
assert await export_button.count() == 1
assert await export_button.inner_text() == "Export"
export_items = page.locator(".detail-inspector-bar .export-menu-item")
assert await export_items.count() == 2
assert await export_items.all_text_contents() == ["Export JSON", "Export HTML"]
hrefs = await export_items.evaluate_all("(links) => links.map((link) => link.getAttribute('href'))")
assert f"/api/sessions/{session_id}/export/compact" in hrefs
assert f"/api/sessions/{session_id}/export/html" in hrefs
assert f"/api/sessions/{session_id}/export/log" not in hrefs
async with page.expect_download() as download_info:
await export_button.click()
await page.locator('.detail-inspector-bar .export-menu-item[href$="/export/html"]').click()
download = await download_info.value
assert download.suggested_filename == f"trace_{session_id[:8]}.html"
download_path = await download.path()
assert download_path is not None
exported_html = Path(download_path).read_text(encoding="utf-8")
assert "EMBEDDED_TRACE_COMPACT_DATA" in exported_html
assert "const EMBEDDED_TRACE_DATA =" not in exported_html
assert "req_claude" in exported_html
finally:
await browser.close()
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_session_export_menu_is_not_clipped_on_mobile(trace_db, tmp_path: Path) -> None:
playwright = pytest.importorskip("playwright.async_api")
trace_path = tmp_path / "2026-05-20" / "trace_080000.jsonl"
_write_jsonl(trace_path, [_anthropic_record()])
_seed_legacy(tmp_path)
session_id = list_trace_sessions()[0]["id"]
server = LiveViewerServer(port=0, migrate_from=tmp_path, dashboard_mode=True)
port = await server.start()
try:
async with playwright.async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
try:
page = await browser.new_page(viewport={"width": 390, "height": 900})
await page.goto(
f"http://127.0.0.1:{port}/dashboard/session/{session_id}",
wait_until="domcontentloaded",
)
await page.wait_for_selector(".detail-inspector-bar .export-menu > summary", timeout=5000)
await page.locator(".detail-inspector-bar .export-menu > summary").click()
menu = page.locator(".detail-inspector-bar .export-menu-list")
assert await menu.is_visible()
export_items = page.locator(".detail-inspector-bar .export-menu-item")
assert await export_items.count() == 2
assert await export_items.all_text_contents() == ["Export JSON", "Export HTML"]
layout = await page.evaluate(
"""() => {
const actions = document.querySelector('.detail-inspector-bar .action-bar');
const menu = document.querySelector('.detail-inspector-bar .export-menu-list');
const actionsBox = actions.getBoundingClientRect();
const menuBox = menu.getBoundingClientRect();
const actionStyles = getComputedStyle(actions);
const menuStyles = getComputedStyle(menu);
return {
actionsBottom: actionsBox.bottom,
menuBottom: menuBox.bottom,
menuRight: menuBox.right,
viewportWidth: window.innerWidth,
menuHeight: menuBox.height,
overflowX: actionStyles.overflowX,
menuPosition: menuStyles.position,
};
}"""
)
assert layout["overflowX"] == "visible"
assert layout["menuHeight"] > 50
assert layout["menuRight"] <= layout["viewportWidth"]
assert layout["menuBottom"] > layout["actionsBottom"]
finally:
await browser.close()
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_bulk_delete_edit_mode_focuses_confirmation_dialog(trace_db) -> None:
playwright = pytest.importorskip("playwright.async_api")
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(session_id, _anthropic_record())
store.finalize_session(session_id, {"api_calls": 1})
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with playwright.async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
try:
page = await browser.new_page()
await page.goto(f"http://127.0.0.1:{port}/dashboard", wait_until="domcontentloaded")
assert await page.locator("[data-delete-session]").count() == 0
await page.locator("#edit-sessions").click()
checkbox = page.locator(f'[data-select-session="{session_id}"]')
await checkbox.wait_for(state="visible", timeout=5000)
await checkbox.check()
assert await page.locator("#bulk-selected-count").inner_text() == "1 selected"
await page.locator("#delete-selected-sessions").click()
await page.wait_for_selector("#delete-session-modal:not(.hidden)", timeout=5000)
assert page.url == f"http://127.0.0.1:{port}/dashboard"
assert await page.evaluate("document.activeElement && document.activeElement.id") == (
"delete-session-cancel"
)
await page.locator("#delete-session-confirm").click()
await page.wait_for_selector("#delete-session-modal.hidden", state="attached", timeout=5000)
await page.wait_for_selector(f'[data-session="{session_id}"]', state="detached", timeout=5000)
finally:
await browser.close()
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_delete_current_live_session_is_protected(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
server = LiveViewerServer(port=0, session_id=session_id, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.delete(f"http://127.0.0.1:{port}/api/sessions/{session_id}") as resp:
assert resp.status == 409
payload = await resp.json()
assert payload["error"] == "Live session cannot be deleted"
assert store.load_session_row(session_id) is not None
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_delete_active_session_is_protected(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(session_id, _anthropic_record())
conn = store._connect()
conn.execute(
"UPDATE sessions SET updated_at = ? WHERE id = ?", (datetime.now(timezone.utc).isoformat(), session_id)
)
conn.commit()
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/api/sessions") as resp:
assert resp.status == 200
payload = await resp.json()
active_session = next(item for item in payload["sessions"] if item["id"] == session_id)
assert active_session["active"] is True
assert active_session["status"] == "active"
async with session.delete(f"http://127.0.0.1:{port}/api/sessions/{session_id}") as resp:
assert resp.status == 409
payload = await resp.json()
assert payload["error"] == "Active session cannot be deleted"
assert store.load_session_row(session_id) is not None
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_lists_stale_active_session_as_complete(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(session_id, _anthropic_record())
stale_updated_at = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
conn = store._connect()
conn.execute(
"UPDATE sessions SET updated_at = ?, summary_json = ? WHERE id = ?",
(
stale_updated_at,
json.dumps(
{
"id": session_id,
"status": "active",
"agent": "Claude Code",
"record_count": 1,
"total_tokens": 51,
},
separators=(",", ":"),
),
session_id,
),
)
conn.commit()
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/api/sessions") as resp:
assert resp.status == 200
payload = await resp.json()
stale_session = next(item for item in payload["sessions"] if item["id"] == session_id)
assert stale_session["active"] is False
assert stale_session["live"] is False
assert stale_session["status"] == "complete"
row = store.load_session_row(session_id)
assert row is not None
assert row["status"] == "complete"
finally:
await server.stop()
@pytest.mark.asyncio
async def test_dashboard_delete_stale_active_session_is_allowed(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(session_id, _anthropic_record())
stale_updated_at = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
conn = store._connect()
conn.execute("UPDATE sessions SET updated_at = ? WHERE id = ?", (stale_updated_at, session_id))
conn.commit()
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.delete(f"http://127.0.0.1:{port}/api/sessions/{session_id}") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["deleted_sessions"] == 1
assert payload["deleted_records"] == 1
assert store.load_session_row(session_id) is None
finally:
await server.stop()
def test_append_record_reactivates_stale_finalized_session(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(session_id, _anthropic_record())
stale_updated_at = "2026-05-20T08:00:00+00:00"
conn = store._connect()
conn.execute(
"UPDATE sessions SET updated_at = ?, summary_json = ? WHERE id = ?",
(
stale_updated_at,
json.dumps(
{
"id": session_id,
"status": "active",
"agent": "Claude Code",
"record_count": 1,
},
separators=(",", ":"),
),
session_id,
),
)
conn.commit()
assert store.finalize_stale_active_sessions(now=datetime(2026, 5, 22, 9, 0, tzinfo=timezone.utc)) == 1
assert store.load_session_row(session_id)["status"] == "complete"
resumed = _anthropic_record(turn=2)
resumed["timestamp"] = "2026-05-22T09:05:00+00:00"
store.append_record(session_id, resumed)
row = store.load_session_row(session_id)
assert row is not None
assert row["status"] == "active"
assert row["record_count"] == 2
assert json.loads(row["summary_json"])["status"] == "active"
@pytest.mark.asyncio
async def test_dashboard_bulk_delete_skips_active_sessions(trace_db) -> None:
store = get_trace_store()
first_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(first_id, _anthropic_record())
store.finalize_session(first_id, {"api_calls": 1})
second_id = store.create_session(client="codex", proxy_mode="reverse")
store.append_record(second_id, _anthropic_record(turn=2))
store.finalize_session(second_id, {"api_calls": 1})
active_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(active_id, _anthropic_record(turn=3))
conn = store._connect()
conn.execute("UPDATE sessions SET updated_at = ? WHERE id = ?", (datetime.now(timezone.utc).isoformat(), active_id))
conn.commit()
stale_active_id = store.create_session(client="claude", proxy_mode="reverse")
store.append_record(stale_active_id, _anthropic_record(turn=4))
stale_updated_at = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
conn.execute("UPDATE sessions SET updated_at = ? WHERE id = ?", (stale_updated_at, stale_active_id))
conn.commit()
server = LiveViewerServer(port=0, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.delete(
f"http://127.0.0.1:{port}/api/sessions",
json={"session_ids": [first_id, second_id, active_id, stale_active_id, "missing"]},
) as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["deleted_sessions"] == 3
assert payload["deleted_records"] == 3
assert payload["skipped_active_sessions"] == [active_id]
assert payload["missing_sessions"] == ["missing"]
assert store.load_session_row(first_id) is None
assert store.load_session_row(second_id) is None
assert store.load_session_row(stale_active_id) is None
assert store.load_session_row(active_id) is not None
finally:
await server.stop()
def test_live_viewer_exposes_current_session_id(trace_db) -> None:
session_id = get_trace_store().create_session(client="claude", proxy_mode="reverse")
server = LiveViewerServer(session_id=session_id)
assert server.session_id == session_id
assert LiveViewerServer().session_id is None
def test_sqlite_log_handler_exports_single_timestamp(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
handler = SQLiteLogHandler(session_id, store=store)
handler.setFormatter(logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S"))
record = logging.LogRecord("test", logging.INFO, __file__, 1, "proxy started", (), None)
record.created = datetime(2026, 5, 20, 8, 0, tzinfo=timezone.utc).timestamp()
handler.emit(record)
assert store.export_log(session_id) == "08:00:00 proxy started\n"
@pytest.mark.asyncio
async def test_trace_writer_adds_capture_metadata(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="codex", proxy_mode="forward")
writer = TraceWriter(session_id, store=store, metadata={"client": "codex", "proxy_mode": "forward"})
try:
await writer.write(_anthropic_record())
finally:
writer.close()
record = store.load_records(session_id)[0]
assert record["capture"] == {"client": "claude", "proxy_mode": "reverse"}
session_id = store.create_session(client="codex", proxy_mode="forward")
writer = TraceWriter(session_id, store=store, metadata={"client": "codex", "proxy_mode": "forward"})
try:
await writer.write({"request": {"body": {}}, "response": {"body": {}}})
finally:
writer.close()
records = store.load_records(session_id)
assert records[-1]["capture"] == {"client": "codex", "proxy_mode": "forward"}
@pytest.mark.asyncio
async def test_trace_writer_persists_records_to_sqlite(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
writer = TraceWriter(session_id, store=store, metadata={"client": "claude", "proxy_mode": "reverse"})
try:
await writer.write(_anthropic_record())
finally:
writer.close()
records = store.load_records(session_id)
assert len(records) == 1
assert records[0]["capture"]["client"] == "claude"
def test_get_session_aggregates(trace_db) -> None:
from claude_tap.trace_store import SessionQuery
store = get_trace_store()
conn = store._connect()
# 1. Active session with no error
active_id = store.create_session(client="claude", proxy_mode="reverse")
conn.execute(
"UPDATE sessions SET status = 'active', record_count = 5, summary_json = ? WHERE id = ?",
(
json.dumps({"agent": "Claude Code", "status": "active", "total_tokens": 120}, separators=(",", ":")),
active_id,
),
)
# 2. Active session with error in summary_json
active_err_id = store.create_session(client="claude", proxy_mode="reverse")
conn.execute(
"UPDATE sessions SET status = 'active', record_count = 2, summary_json = ? WHERE id = ?",
(
json.dumps({"agent": "Claude Code", "status": "error", "total_tokens": 80}, separators=(",", ":")),
active_err_id,
),
)
# 3. Completed session with error status
completed_err_id = store.create_session(client="claude", proxy_mode="reverse")
conn.execute(
"UPDATE sessions SET status = 'error', record_count = 10, summary_json = ? WHERE id = ?",
(
json.dumps({"agent": "Claude Code", "status": "error", "total_tokens": 300}, separators=(",", ":")),
completed_err_id,
),
)
conn.commit()
# Check global aggregates
aggregates = store.get_session_aggregates()
assert aggregates["total_sessions"] == 3
assert aggregates["total_records"] == 17
assert aggregates["total_tokens"] == 500
assert aggregates["total_errors"] == 2 # active_err_id and completed_err_id
# Check query-based status filtering for active status
active_query = SessionQuery(status="active")
active_aggs = store.get_session_aggregates(active_query)
assert active_aggs["total_sessions"] == 1 # only active_id
# Check query-based status filtering for error status
error_query = SessionQuery(status="error")
error_aggs = store.get_session_aggregates(error_query)
assert error_aggs["total_sessions"] == 2 # active_err_id and completed_err_id
def test_agent_filter_values_resolves_custom_agents(trace_db) -> None:
from claude_tap.dashboard import _agent_filter_values
store = get_trace_store()
conn = store._connect()
# Create a session with a custom agent client "My-Agent"
custom_id = store.create_session(client="My-Agent", proxy_mode="reverse")
conn.execute(
"UPDATE sessions SET status = 'complete', record_count = 1, summary_json = ? WHERE id = ?",
(
json.dumps({"agent": "My-Agent", "status": "complete", "total_tokens": 10}, separators=(",", ":")),
custom_id,
),
)
conn.commit()
clients, labels = _agent_filter_values("my-agent")
assert "My-Agent" in clients
assert "My-Agent" in labels
@pytest.mark.asyncio
async def test_search_uncached_records_fallback(trace_db) -> None:
from claude_tap.trace_store import SessionQuery
store = get_trace_store()
conn = store._connect()
# Create a session with NULL summary_json (uncached) but records with a specific text in payload_json
session_id = store.create_session(client="claude", proxy_mode="reverse")
writer = TraceWriter(session_id, store=store, metadata={"client": "claude", "proxy_mode": "reverse"})
try:
await writer.write(
{
"timestamp": "2026-05-20T10:15:00+00:00",
"request": {"method": "POST", "path": "/v1/messages", "body": "unusual_secret_string"},
"response": {"status": 200, "body": {}},
}
)
finally:
writer.close()
# Force summary_json to be NULL to simulate uncached session
conn.execute("UPDATE sessions SET summary_json = NULL WHERE id = ?", (session_id,))
conn.commit()
search_query = SessionQuery(search="unusual_secret_string")
sessions = store.list_session_rows(query=search_query)
assert len(sessions) == 1
assert sessions[0]["id"] == session_id
def test_search_matches_compacted_record_blobs(trace_db) -> None:
from claude_tap.trace_store import SessionQuery
store = get_trace_store()
session_id = store.create_session(client="claude", proxy_mode="reverse")
hidden_term = "dashboard-needle-inside-compacted-user-message"
long_prompt = "intro " * 120 + hidden_term
store.append_record(
session_id,
{
"timestamp": "2026-07-08T10:15:00+00:00",
"request": {
"method": "POST",
"path": "/v1/messages",
"body": {
"model": "claude-sonnet-4-6",
"messages": [{"role": "user", "content": long_prompt}],
},
},
"response": {"status": 200, "body": {"content": [{"type": "text", "text": "ok"}]}},
},
)
conn = store._connect()
payload_json = conn.execute(
"SELECT payload_json FROM records WHERE session_id = ?",
(session_id,),
).fetchone()["payload_json"]
assert hidden_term not in payload_json
assert conn.execute("SELECT COUNT(*) FROM record_blobs WHERE session_id = ?", (session_id,)).fetchone()[0] > 0
sessions = store.list_session_rows(query=SessionQuery(search=hidden_term))
assert [row["id"] for row in sessions] == [session_id]
@pytest.mark.asyncio
async def test_kimi_code_model_probe_error_does_not_mark_successful_session_failed(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="kimi-code", proxy_mode="reverse")
writer = TraceWriter(session_id, store=store, metadata={"client": "kimi-code", "proxy_mode": "reverse"})
try:
await writer.write(
{
"timestamp": "2026-06-09T08:00:00+00:00",
"request": {"method": "GET", "path": "/models", "body": {}},
"response": {"status": 401, "body": {"error": "missing bearer token"}},
}
)
await writer.write(
{
"timestamp": "2026-06-09T08:00:02+00:00",
"request": {
"method": "POST",
"path": "/chat/completions",
"body": {"model": "kimi-code/kimi-for-coding", "messages": [{"role": "user", "content": "ping"}]},
},
"response": {
"status": 200,
"body": {
"model": "kimi-code/kimi-for-coding",
"choices": [{"message": {"content": "pong"}}],
"usage": {"prompt_tokens": 12, "completion_tokens": 3},
},
},
}
)
finally:
writer.close()
conn = store._connect()
row = conn.execute("SELECT status, summary_json FROM sessions WHERE id = ?", (session_id,)).fetchone()
summary = json.loads(row["summary_json"])
payload = load_trace_session(session_id)
assert row["status"] == "complete"
assert summary["status"] == "complete"
assert payload is not None
assert payload["session"]["status"] == "complete"
assert payload["session"]["error"] == ""
assert payload["session"]["first_user"] == "ping"
assert payload["session"]["last_response"] == "pong"
@pytest.mark.asyncio
async def test_kimi_code_model_probe_error_marks_probe_only_session_failed(trace_db) -> None:
store = get_trace_store()
session_id = store.create_session(client="kimi-code", proxy_mode="reverse")
writer = TraceWriter(session_id, store=store, metadata={"client": "kimi-code", "proxy_mode": "reverse"})
try:
await writer.write(
{
"timestamp": "2026-06-09T08:00:00+00:00",
"request": {"method": "GET", "path": "/models", "body": {}},
"response": {"status": 401, "body": {"error": "missing bearer token"}},
}
)
finally:
writer.close()
conn = store._connect()
row = conn.execute("SELECT status, summary_json FROM sessions WHERE id = ?", (session_id,)).fetchone()
summary = json.loads(row["summary_json"])
assert row["status"] == "error"
assert summary["status"] == "error"
conn.execute("UPDATE sessions SET summary_json = NULL WHERE id = ?", (session_id,))
conn.commit()
payload = load_trace_session(session_id)
assert payload is not None
assert payload["session"]["status"] == "error"
assert payload["session"]["error"] == "missing bearer token"