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
3402 lines
137 KiB
Python
3402 lines
137 KiB
Python
"""Cross-client contract tests for the self-contained HTML viewer.
|
|
|
|
These tests intentionally exercise viewer.html through a real browser instead
|
|
of only checking generated markup. The goal is to keep core semantic sections
|
|
stable across supported trace shapes whenever the large inline JS file changes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from claude_tap.compact_trace import build_compact_trace_bundle
|
|
from claude_tap.viewer import _generate_html_viewer, _generate_html_viewer_from_compact_bundle, _read_viewer_template
|
|
|
|
pw_missing = False
|
|
try:
|
|
from playwright.sync_api import Page, sync_playwright # noqa: F401
|
|
except ImportError:
|
|
pw_missing = True
|
|
Page = Any # type: ignore[assignment,misc]
|
|
|
|
pytestmark = pytest.mark.skipif(pw_missing, reason="playwright not installed")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ViewerContractCase:
|
|
name: str
|
|
records: tuple[dict[str, Any], ...]
|
|
expected_sections: tuple[str, ...]
|
|
expected_system: str | None
|
|
expected_roles: tuple[str, ...]
|
|
expected_tools: tuple[str, ...]
|
|
expected_output_types: tuple[str, ...]
|
|
expected_usage: dict[str, int]
|
|
required_detail_text: tuple[str, ...]
|
|
min_stream_events: int = 0
|
|
entry_index: int = 0
|
|
expected_sidebar_label: str | None = None
|
|
|
|
|
|
def _sse_frame(payload: dict[str, Any]) -> str:
|
|
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
|
|
|
|
|
def _anthropic_messages_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T13:20:00+00:00",
|
|
"request_id": "req_anthropic_contract",
|
|
"turn": 1,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": {},
|
|
"body": {
|
|
"model": "claude-opus-4-6",
|
|
"system": "Claude Code contract system prompt.",
|
|
"messages": [
|
|
{"role": "user", "content": [{"type": "text", "text": "Read pyproject.toml."}]},
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "tool_use",
|
|
"id": "toolu_read",
|
|
"name": "Read",
|
|
"input": {"file_path": "pyproject.toml"},
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "tool_result",
|
|
"tool_use_id": "toolu_read",
|
|
"content": "project metadata",
|
|
}
|
|
],
|
|
},
|
|
],
|
|
"tools": [
|
|
{
|
|
"name": "Read",
|
|
"description": "Read a file.",
|
|
"input_schema": {
|
|
"type": "object",
|
|
"properties": {"file_path": {"type": "string"}},
|
|
"required": ["file_path"],
|
|
},
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"content": [{"type": "text", "text": "Anthropic response OK."}],
|
|
"usage": {"input_tokens": 120, "output_tokens": 9, "cache_read_input_tokens": 40},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _responses_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T13:21:00+00:00",
|
|
"request_id": "req_responses_contract",
|
|
"turn": 1,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"model": "gpt-5.4",
|
|
"instructions": "You are Codex contract system prompt.",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "Run pwd."}],
|
|
}
|
|
],
|
|
"tools": [{"type": "function", "name": "exec_command", "description": "Runs a command."}],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"output": [
|
|
{
|
|
"type": "function_call",
|
|
"name": "exec_command",
|
|
"arguments": '{"cmd":"pwd"}',
|
|
"call_id": "call_pwd",
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "Responses final OK."}],
|
|
},
|
|
],
|
|
"usage": {
|
|
"input_tokens": 130,
|
|
"output_tokens": 14,
|
|
"input_tokens_details": {"cached_tokens": 50},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _responses_empty_input_record() -> dict[str, Any]:
|
|
record = _responses_record()
|
|
record["request_id"] = "req_responses_empty_input"
|
|
record["request"]["body"]["input"] = []
|
|
record["response"]["body"]["output"] = []
|
|
return record
|
|
|
|
|
|
def _codex_websocket_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T13:22:00+00:00",
|
|
"request_id": "req_codex_ws_contract",
|
|
"turn": "1.2",
|
|
"duration_ms": 100,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/backend-api/codex/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"type": "response.create",
|
|
"model": "gpt-5.5",
|
|
"instructions": "You are Codex WebSocket contract system prompt.",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "developer",
|
|
"content": [{"type": "input_text", "text": "Follow repository rules."}],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "Continue after a tool call."}],
|
|
},
|
|
],
|
|
"tools": [{"type": "function", "name": "exec_command", "description": "Runs a command."}],
|
|
"stream": True,
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": None,
|
|
"ws_events": [
|
|
{
|
|
"type": "response.output_item.done",
|
|
"output_index": 0,
|
|
"item": {
|
|
"type": "function_call",
|
|
"name": "exec_command",
|
|
"arguments": '{"cmd":"pwd"}',
|
|
"call_id": "call_pwd",
|
|
},
|
|
},
|
|
{
|
|
"type": "response.output_item.done",
|
|
"output_index": 1,
|
|
"item": {
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "WebSocket final OK."}],
|
|
},
|
|
},
|
|
{
|
|
"type": "response.completed",
|
|
"response": {
|
|
"usage": {"input_tokens": 140, "output_tokens": 8, "total_tokens": 148},
|
|
"output": [],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def _content_block_boundary_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-24T07:20:00+00:00",
|
|
"request_id": "req_content_block_boundary_contract",
|
|
"turn": 99,
|
|
"duration_ms": 120,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"model": "zz-content-block-boundary",
|
|
"system": [
|
|
{"type": "text", "text": "System block one."},
|
|
{"type": "text", "text": "System block two."},
|
|
],
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "developer",
|
|
"content": [
|
|
{"type": "input_text", "text": "Developer block one."},
|
|
{"type": "input_text", "text": "Developer block two."},
|
|
],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "input_text", "text": "User block one."},
|
|
{"type": "input_text", "text": "User block two."},
|
|
{
|
|
"type": "input_image",
|
|
"image_url": (
|
|
"data:image/png;base64,"
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAEklEQVR4nGPQb3j7Hx9mGBkKAKVjpsHoKJzJAAAAAElFTkSuQmCC"
|
|
),
|
|
"detail": "high",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [
|
|
{"type": "output_text", "text": "Assistant text block."},
|
|
{"type": "tool_use", "name": "read_file", "input": {"path": "README.md"}},
|
|
],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "tool",
|
|
"content": [
|
|
{"type": "tool_result", "tool_use_id": "call_one", "content": "Tool result one."},
|
|
{"type": "tool_result", "tool_use_id": "call_two", "content": "Tool result two."},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"content": [{"type": "text", "text": "Content block response OK."}],
|
|
"usage": {"input_tokens": 42, "output_tokens": 5},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _codex_reverse_websocket_record() -> dict[str, Any]:
|
|
record = _codex_websocket_record()
|
|
record["request_id"] = "req_codex_reverse_ws_contract"
|
|
record["request"]["path"] = "/v1/responses"
|
|
return record
|
|
|
|
|
|
def _chat_completions_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T13:23:00+00:00",
|
|
"request_id": "req_chat_contract",
|
|
"turn": 1,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/chat/completions",
|
|
"headers": {},
|
|
"body": {
|
|
"model": "kimi-k2-turbo-preview",
|
|
"messages": [
|
|
{"role": "system", "content": "Kimi contract system prompt."},
|
|
{"role": "user", "content": "Read the project metadata."},
|
|
{
|
|
"role": "assistant",
|
|
"content": "",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_read",
|
|
"type": "function",
|
|
"function": {"name": "read_file", "arguments": '{"path":"pyproject.toml"}'},
|
|
}
|
|
],
|
|
},
|
|
{"role": "tool", "tool_call_id": "call_read", "content": "project metadata"},
|
|
],
|
|
"tools": [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "read_file",
|
|
"description": "Read a file.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"path": {"type": "string"}},
|
|
},
|
|
},
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "chatcmpl-contract",
|
|
"object": "chat.completion",
|
|
"model": "kimi-k2-turbo-preview",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": "",
|
|
"reasoning_content": "Need to inspect metadata before answering.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_response_read",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "inspect_metadata",
|
|
"arguments": '{"path":"pyproject.toml"}',
|
|
},
|
|
}
|
|
],
|
|
},
|
|
"finish_reason": "tool_calls",
|
|
},
|
|
{
|
|
"index": 1,
|
|
"message": {"role": "assistant", "content": "Chat final OK."},
|
|
"finish_reason": "stop",
|
|
},
|
|
],
|
|
"usage": {"prompt_tokens": 150, "completion_tokens": 10, "cached_tokens": 70},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _opencode_chat_completions_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T16:11:10+00:00",
|
|
"request_id": "req_opencode_real_shape_contract",
|
|
"turn": 2,
|
|
"duration_ms": 1800,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/zen/v1/chat/completions",
|
|
"headers": {"Host": "opencode.ai"},
|
|
"body": {
|
|
"model": "deepseek-v4-flash-free",
|
|
"messages": [
|
|
{
|
|
"role": "system",
|
|
"content": (
|
|
"You are opencode, an interactive CLI tool that helps users "
|
|
"with software engineering tasks."
|
|
),
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": "Run printf OPENCODE_TOOL_ONE and pwd.",
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "",
|
|
"reasoning_content": "The user wants me to run a specific command.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_one",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "bash",
|
|
"arguments": (
|
|
'{"command":"printf \'OPENCODE_TOOL_ONE\\\\n\'; pwd",'
|
|
'"description":"Run printf and pwd"}'
|
|
),
|
|
},
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_one",
|
|
"content": "OPENCODE_TOOL_ONE\n/home/liaohch3/src/github.com/liaohch3/claude-tap-3\n",
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "OPENCODE_TOOL_ONE\n/home/liaohch3/src/github.com/liaohch3/claude-tap-3",
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": "Second turn: run printf OPENCODE_TOOL_TWO and ls pyproject.toml.",
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "",
|
|
"reasoning_content": "The user wants a second command and the previous path.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_two",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "bash",
|
|
"arguments": (
|
|
'{"command":"printf \'OPENCODE_TOOL_TWO\\\\n\'; ls pyproject.toml",'
|
|
'"description":"Run printf and ls"}'
|
|
),
|
|
},
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_two",
|
|
"content": "OPENCODE_TOOL_TWO\npyproject.toml\n",
|
|
},
|
|
],
|
|
"tools": [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "bash",
|
|
"description": "Executes a given bash command.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
},
|
|
"required": ["command"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "edit",
|
|
"description": "Edit a file.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"filePath": {"type": "string"}},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
"stream": True,
|
|
"stream_options": {"include_usage": True},
|
|
"tool_choice": "auto",
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"object": "chat.completion",
|
|
"model": "deepseek-v4-flash",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": "The user wants both the path from the previous output and the new output.",
|
|
},
|
|
{
|
|
"type": "text",
|
|
"text": (
|
|
"Path from OPENCODE_TOOL_ONE: "
|
|
"`/home/liaohch3/src/github.com/liaohch3/claude-tap-3`\n\n"
|
|
"New command output:\n```\nOPENCODE_TOOL_TWO\npyproject.toml\n```"
|
|
),
|
|
},
|
|
],
|
|
"usage": {
|
|
"prompt_tokens": 14070,
|
|
"completion_tokens": 109,
|
|
"total_tokens": 14179,
|
|
"prompt_tokens_details": {"cached_tokens": 13952},
|
|
"completion_tokens_details": {"reasoning_tokens": 57},
|
|
"input_tokens": 14070,
|
|
"output_tokens": 109,
|
|
"cache_read_input_tokens": 13952,
|
|
},
|
|
},
|
|
"sse_events": [
|
|
{
|
|
"event": "message",
|
|
"data": {
|
|
"choices": [
|
|
{
|
|
"delta": {
|
|
"reasoning_content": (
|
|
"The user wants both the path from the previous output and the new output."
|
|
)
|
|
}
|
|
}
|
|
]
|
|
},
|
|
},
|
|
{
|
|
"event": "message",
|
|
"data": {
|
|
"choices": [
|
|
{
|
|
"delta": {
|
|
"content": (
|
|
"Path from OPENCODE_TOOL_ONE: "
|
|
"`/home/liaohch3/src/github.com/liaohch3/claude-tap-3`"
|
|
)
|
|
}
|
|
}
|
|
]
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def _opencode_openai_oauth_responses_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T16:45:43+00:00",
|
|
"request_id": "req_opencode_openai_oauth_responses_contract",
|
|
"turn": 3,
|
|
"duration_ms": 6106,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/backend-api/codex/responses",
|
|
"headers": {"Host": "chatgpt.com", "User-Agent": "opencode/1.14.48"},
|
|
"body": {
|
|
"model": "gpt-5.4-mini",
|
|
"instructions": (
|
|
"You are OpenCode, You and the user share the same workspace "
|
|
"and collaborate to achieve the user's goals."
|
|
),
|
|
"input": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "input_text",
|
|
"text": (
|
|
"Use the bash tool to run exactly: printf "
|
|
"'OPENCODE_OPENAI_OAUTH_TOOL_ONE\\n'; pwd ."
|
|
),
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "Running the requested shell command."}],
|
|
},
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_oauth_bash",
|
|
"name": "bash",
|
|
"arguments": (
|
|
'{"command":"printf \'OPENCODE_OPENAI_OAUTH_TOOL_ONE\\\\n\'; pwd .",'
|
|
'"description":"Runs the requested command"}'
|
|
),
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_oauth_bash",
|
|
"output": (
|
|
"OPENCODE_OPENAI_OAUTH_TOOL_ONE\n/home/liaohch3/src/github.com/liaohch3/claude-tap-3\n"
|
|
),
|
|
},
|
|
],
|
|
"tools": [
|
|
{"type": "function", "name": "bash", "description": "Run a shell command."},
|
|
{"type": "function", "name": "read", "description": "Read a file."},
|
|
],
|
|
"stream": True,
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": (
|
|
"OPENCODE_OPENAI_OAUTH_TOOL_ONE\n"
|
|
"/home/liaohch3/src/github.com/liaohch3/claude-tap-3"
|
|
),
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"usage": {
|
|
"input_tokens": 12424,
|
|
"output_tokens": 115,
|
|
"input_tokens_details": {"cached_tokens": 11776},
|
|
},
|
|
},
|
|
"sse_events": [
|
|
{
|
|
"event": "response.output_item.done",
|
|
"data": {
|
|
"type": "response.output_item.done",
|
|
"output_index": 0,
|
|
"item": {
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": (
|
|
"OPENCODE_OPENAI_OAUTH_TOOL_ONE\n"
|
|
"/home/liaohch3/src/github.com/liaohch3/claude-tap-3"
|
|
),
|
|
}
|
|
],
|
|
},
|
|
},
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def _pi_openai_oauth_websocket_record() -> dict[str, Any]:
|
|
system_prompt = (
|
|
"You are an expert coding assistant operating inside pi, a coding agent harness. "
|
|
"You help users by reading files, executing commands, editing code, and writing new files.\n\n"
|
|
"Available tools:\n- bash: Execute bash commands (ls, grep, find, etc.)"
|
|
)
|
|
initial_request = {
|
|
"type": "response.create",
|
|
"model": "gpt-5.3-codex-spark",
|
|
"store": False,
|
|
"stream": True,
|
|
"instructions": system_prompt,
|
|
"input": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "input_text",
|
|
"text": "Use the bash tool to run exactly: printf 'PI_TOOL_ONE\\n'; pwd.",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"tools": [{"type": "function", "name": "bash", "description": "Execute a bash command."}],
|
|
"reasoning": {"effort": "low", "summary": "auto"},
|
|
"prompt_cache_key": "session-pi-contract",
|
|
"tool_choice": "auto",
|
|
"parallel_tool_calls": True,
|
|
}
|
|
continuation_request = {
|
|
**initial_request,
|
|
"previous_response_id": "resp_pi_tool_call",
|
|
"input": [
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_pi_bash",
|
|
"output": "PI_TOOL_ONE\n/home/liaohch3/src/github.com/liaohch3/claude-tap-3\n",
|
|
}
|
|
],
|
|
}
|
|
return {
|
|
"timestamp": "2026-05-14T07:47:21+00:00",
|
|
"request_id": "req_pi_openai_oauth_ws_contract",
|
|
"turn": 1,
|
|
"duration_ms": 2400,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/backend-api/codex/responses",
|
|
"headers": {"Host": "chatgpt.com", "User-Agent": "pi (browser)", "originator": "pi"},
|
|
"body": continuation_request,
|
|
"ws_events": [initial_request, continuation_request],
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_pi_final",
|
|
"status": "completed",
|
|
"instructions": system_prompt,
|
|
"model": "gpt-5.3-codex-spark",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": "PI_TOOL_ONE\n/home/liaohch3/src/github.com/liaohch3/claude-tap-3",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 655, "output_tokens": 29, "total_tokens": 684},
|
|
},
|
|
"ws_events": [
|
|
{
|
|
"type": "response.created",
|
|
"response": {
|
|
"id": "resp_pi_tool_call",
|
|
"status": "in_progress",
|
|
"instructions": system_prompt,
|
|
"model": "gpt-5.3-codex-spark",
|
|
"previous_response_id": None,
|
|
},
|
|
},
|
|
{
|
|
"type": "response.output_item.done",
|
|
"output_index": 0,
|
|
"item": {
|
|
"type": "function_call",
|
|
"name": "bash",
|
|
"call_id": "call_pi_bash",
|
|
"arguments": '{"command":"printf \'PI_TOOL_ONE\\n\'; pwd"}',
|
|
},
|
|
},
|
|
{
|
|
"type": "response.completed",
|
|
"response": {
|
|
"id": "resp_pi_tool_call",
|
|
"status": "completed",
|
|
"instructions": system_prompt,
|
|
"model": "gpt-5.3-codex-spark",
|
|
"previous_response_id": None,
|
|
"output": [
|
|
{
|
|
"type": "function_call",
|
|
"name": "bash",
|
|
"call_id": "call_pi_bash",
|
|
"arguments": '{"command":"printf \'PI_TOOL_ONE\\n\'; pwd"}',
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 530, "output_tokens": 88, "total_tokens": 618},
|
|
},
|
|
},
|
|
{
|
|
"type": "response.created",
|
|
"response": {
|
|
"id": "resp_pi_final",
|
|
"status": "in_progress",
|
|
"instructions": system_prompt,
|
|
"model": "gpt-5.3-codex-spark",
|
|
"previous_response_id": "resp_pi_tool_call",
|
|
},
|
|
},
|
|
{
|
|
"type": "response.output_item.done",
|
|
"output_index": 0,
|
|
"item": {
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": "PI_TOOL_ONE\n/home/liaohch3/src/github.com/liaohch3/claude-tap-3",
|
|
}
|
|
],
|
|
},
|
|
},
|
|
{
|
|
"type": "response.completed",
|
|
"response": {
|
|
"id": "resp_pi_final",
|
|
"status": "completed",
|
|
"instructions": system_prompt,
|
|
"model": "gpt-5.3-codex-spark",
|
|
"previous_response_id": "resp_pi_tool_call",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": "PI_TOOL_ONE\n/home/liaohch3/src/github.com/liaohch3/claude-tap-3",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 655, "output_tokens": 29, "total_tokens": 684},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def _gemini_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T13:24:00+00:00",
|
|
"request_id": "req_gemini_contract",
|
|
"turn": 1,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1internal:streamGenerateContent?alt=sse",
|
|
"headers": {"Host": "cloudcode-pa.googleapis.com"},
|
|
"body": {
|
|
"model": "gemini-3-flash-preview",
|
|
"request": {
|
|
"systemInstruction": {
|
|
"role": "user",
|
|
"parts": [{"text": "You are Gemini CLI contract system prompt."}],
|
|
},
|
|
"contents": [
|
|
{"role": "user", "parts": [{"text": "Use shell to inspect the workspace."}]},
|
|
{
|
|
"role": "model",
|
|
"parts": [{"functionCall": {"name": "run_shell_command", "args": {"command": "pwd"}}}],
|
|
},
|
|
{
|
|
"role": "user",
|
|
"parts": [
|
|
{
|
|
"functionResponse": {
|
|
"id": "run_shell_command_1",
|
|
"name": "run_shell_command",
|
|
"response": {"output": "Output: /repo\nProcess Group PGID: 123"},
|
|
}
|
|
}
|
|
],
|
|
},
|
|
],
|
|
"tools": [
|
|
{
|
|
"functionDeclarations": [
|
|
{
|
|
"name": "run_shell_command",
|
|
"description": "Runs a shell command.",
|
|
"parametersJsonSchema": {
|
|
"type": "object",
|
|
"properties": {"command": {"type": "string"}},
|
|
},
|
|
}
|
|
]
|
|
}
|
|
],
|
|
},
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": (
|
|
_sse_frame(
|
|
{
|
|
"response": {
|
|
"candidates": [
|
|
{
|
|
"content": {
|
|
"role": "model",
|
|
"parts": [
|
|
{"thought": True, "text": "I should run the command."},
|
|
{"functionCall": {"name": "run_shell_command", "args": {"command": "pwd"}}},
|
|
],
|
|
}
|
|
}
|
|
],
|
|
"usageMetadata": {
|
|
"promptTokenCount": 160,
|
|
"candidatesTokenCount": 5,
|
|
"cachedContentTokenCount": 80,
|
|
},
|
|
}
|
|
}
|
|
)
|
|
+ _sse_frame(
|
|
{
|
|
"response": {
|
|
"candidates": [{"content": {"role": "model", "parts": [{"text": "Gemini final OK."}]}}],
|
|
"usageMetadata": {
|
|
"promptTokenCount": 170,
|
|
"candidatesTokenCount": 13,
|
|
"cachedContentTokenCount": 80,
|
|
},
|
|
}
|
|
}
|
|
)
|
|
),
|
|
},
|
|
}
|
|
|
|
|
|
def _bedrock_converse_record() -> dict[str, Any]:
|
|
return {
|
|
"timestamp": "2026-05-13T13:30:00+00:00",
|
|
"request_id": "req_bedrock_converse_contract",
|
|
"turn": 1,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/model/anthropic.claude-sonnet-4-20250514-v1:0/converse",
|
|
"headers": {},
|
|
"body": {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": "Use Bedrock Converse to answer."}],
|
|
}
|
|
],
|
|
"tools": [
|
|
{
|
|
"name": "lookup",
|
|
"description": "Look up a value.",
|
|
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}},
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"output": {
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": [
|
|
{"text": "Bedrock Converse final OK."},
|
|
{
|
|
"reasoningContent": {
|
|
"reasoningText": {
|
|
"text": "Bedrock reasoning OK.",
|
|
"signature": "sig-contract",
|
|
}
|
|
}
|
|
},
|
|
{"toolUse": {"toolUseId": "tool-1", "name": "lookup", "input": {"query": "bedrock"}}},
|
|
],
|
|
}
|
|
},
|
|
"usage": {
|
|
"inputTokens": 180,
|
|
"outputTokens": 18,
|
|
"totalTokens": 198,
|
|
"cacheReadInputTokens": 60,
|
|
"cacheWriteInputTokens": 12,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _contract_cases() -> tuple[ViewerContractCase, ...]:
|
|
return (
|
|
ViewerContractCase(
|
|
name="anthropic_messages",
|
|
records=(_anthropic_messages_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Messages", "Response"),
|
|
expected_system="Claude Code contract system prompt.",
|
|
expected_roles=("user", "assistant", "user"),
|
|
expected_tools=("Read",),
|
|
expected_output_types=("text",),
|
|
expected_usage={"input_tokens": 120, "output_tokens": 9, "cache_read_input_tokens": 40},
|
|
required_detail_text=("Read pyproject.toml.", "project metadata", "Anthropic response OK."),
|
|
),
|
|
ViewerContractCase(
|
|
name="openai_responses",
|
|
records=(_responses_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Messages", "Response"),
|
|
expected_system="You are Codex contract system prompt.",
|
|
expected_roles=("developer", "user"),
|
|
expected_tools=("exec_command",),
|
|
expected_output_types=("tool_use", "text"),
|
|
expected_usage={"input_tokens": 130, "output_tokens": 14, "cache_read_input_tokens": 50},
|
|
required_detail_text=("Run pwd.", "exec_command", "Responses final OK."),
|
|
),
|
|
ViewerContractCase(
|
|
name="codex_websocket",
|
|
records=(_codex_websocket_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Request Context", "Response", "SSE Events"),
|
|
expected_system="You are Codex WebSocket contract system prompt.",
|
|
expected_roles=("developer", "user"),
|
|
expected_tools=("exec_command",),
|
|
expected_output_types=("tool_use", "text"),
|
|
expected_usage={"input_tokens": 140, "output_tokens": 8},
|
|
required_detail_text=("Continue after a tool call.", "exec_command", "WebSocket final OK."),
|
|
min_stream_events=3,
|
|
),
|
|
ViewerContractCase(
|
|
name="codex_reverse_websocket",
|
|
records=(_codex_reverse_websocket_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Request Context", "Response", "SSE Events"),
|
|
expected_system="You are Codex WebSocket contract system prompt.",
|
|
expected_roles=("developer", "user"),
|
|
expected_tools=("exec_command",),
|
|
expected_output_types=("tool_use", "text"),
|
|
expected_usage={"input_tokens": 140, "output_tokens": 8},
|
|
required_detail_text=("Continue after a tool call.", "exec_command", "WebSocket final OK."),
|
|
min_stream_events=3,
|
|
),
|
|
ViewerContractCase(
|
|
name="chat_completions",
|
|
records=(_chat_completions_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Messages", "Response"),
|
|
expected_system="Kimi contract system prompt.",
|
|
expected_roles=("user", "assistant", "tool"),
|
|
expected_tools=("read_file",),
|
|
expected_output_types=("thinking", "tool_use", "text"),
|
|
expected_usage={"input_tokens": 150, "output_tokens": 10, "cache_read_input_tokens": 70},
|
|
required_detail_text=(
|
|
"Read the project metadata.",
|
|
"read_file",
|
|
"Need to inspect metadata before answering.",
|
|
"inspect_metadata",
|
|
"Chat final OK.",
|
|
),
|
|
),
|
|
ViewerContractCase(
|
|
name="opencode_chat_completions",
|
|
records=(_opencode_chat_completions_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Messages", "Response", "SSE Events"),
|
|
expected_system="You are opencode, an interactive CLI tool that helps users with software engineering tasks.",
|
|
expected_roles=("user", "assistant", "tool", "assistant", "user", "assistant", "tool"),
|
|
expected_tools=("bash", "edit"),
|
|
expected_output_types=("thinking", "text"),
|
|
expected_usage={"input_tokens": 14070, "output_tokens": 109, "cache_read_input_tokens": 13952},
|
|
required_detail_text=(
|
|
"OPENCODE_TOOL_ONE",
|
|
"/home/liaohch3/src/github.com/liaohch3/claude-tap-3",
|
|
"OPENCODE_TOOL_TWO",
|
|
"pyproject.toml",
|
|
),
|
|
min_stream_events=2,
|
|
),
|
|
ViewerContractCase(
|
|
name="opencode_openai_oauth_responses",
|
|
records=(_opencode_openai_oauth_responses_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Messages", "Response", "SSE Events"),
|
|
expected_system=(
|
|
"You are OpenCode, You and the user share the same workspace "
|
|
"and collaborate to achieve the user's goals."
|
|
),
|
|
expected_roles=("developer", "user", "assistant", "assistant", "tool"),
|
|
expected_tools=("bash", "read"),
|
|
expected_output_types=("text",),
|
|
expected_usage={"input_tokens": 12424, "output_tokens": 115, "cache_read_input_tokens": 11776},
|
|
required_detail_text=(
|
|
"OPENCODE_OPENAI_OAUTH_TOOL_ONE",
|
|
"/home/liaohch3/src/github.com/liaohch3/claude-tap-3",
|
|
"bash",
|
|
),
|
|
min_stream_events=1,
|
|
),
|
|
ViewerContractCase(
|
|
name="pi_openai_oauth_websocket_final_response",
|
|
records=(_pi_openai_oauth_websocket_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Request Context", "Response", "SSE Events"),
|
|
expected_system=(
|
|
"You are an expert coding assistant operating inside pi, a coding agent harness. "
|
|
"You help users by reading files, executing commands, editing code, and writing new files.\n\n"
|
|
"Available tools:\n- bash: Execute bash commands (ls, grep, find, etc.)"
|
|
),
|
|
expected_roles=("developer", "user", "assistant", "tool"),
|
|
expected_tools=("bash",),
|
|
expected_output_types=("text",),
|
|
expected_usage={"input_tokens": 655, "output_tokens": 29},
|
|
required_detail_text=(
|
|
"PI_TOOL_ONE",
|
|
"/home/liaohch3/src/github.com/liaohch3/claude-tap-3",
|
|
"printf 'PI_TOOL_ONE",
|
|
),
|
|
min_stream_events=2,
|
|
entry_index=1,
|
|
expected_sidebar_label="Pi",
|
|
),
|
|
ViewerContractCase(
|
|
name="gemini",
|
|
records=(_gemini_record(),),
|
|
expected_sections=("Tools", "System Prompt", "Messages", "Response", "SSE Events"),
|
|
expected_system="You are Gemini CLI contract system prompt.",
|
|
expected_roles=("user", "assistant", "tool"),
|
|
expected_tools=("run_shell_command",),
|
|
expected_output_types=("thinking", "tool_use", "text"),
|
|
expected_usage={"input_tokens": 170, "output_tokens": 13, "cache_read_input_tokens": 80},
|
|
required_detail_text=("Use shell to inspect the workspace.", "Output: /repo", "Gemini final OK."),
|
|
min_stream_events=2,
|
|
),
|
|
ViewerContractCase(
|
|
name="bedrock_converse",
|
|
records=(_bedrock_converse_record(),),
|
|
expected_sections=("Tools", "Messages", "Response"),
|
|
expected_system=None,
|
|
expected_roles=("user",),
|
|
expected_tools=("lookup",),
|
|
expected_output_types=("text", "thinking", "tool_use"),
|
|
expected_usage={
|
|
"input_tokens": 180,
|
|
"output_tokens": 18,
|
|
"cache_read_input_tokens": 60,
|
|
"cache_creation_input_tokens": 12,
|
|
},
|
|
required_detail_text=(
|
|
"Use Bedrock Converse to answer.",
|
|
"Bedrock Converse final OK.",
|
|
"Bedrock reasoning OK.",
|
|
"lookup",
|
|
),
|
|
),
|
|
ViewerContractCase(
|
|
name="content_block_boundaries",
|
|
records=(_content_block_boundary_record(),),
|
|
expected_sections=("System Prompt", "Messages", "Response"),
|
|
expected_system="System block one.\n\nSystem block two.",
|
|
expected_roles=("developer", "user", "assistant", "tool"),
|
|
expected_tools=(),
|
|
expected_output_types=("text",),
|
|
expected_usage={"input_tokens": 42, "output_tokens": 5},
|
|
required_detail_text=(
|
|
"System block one.",
|
|
"Developer block one.",
|
|
"User block two.",
|
|
"Assistant text block.",
|
|
"Tool result two.",
|
|
"Content block response OK.",
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def _runtime_smoke_records() -> tuple[dict[str, Any], ...]:
|
|
return (
|
|
{
|
|
"request_id": "req_empty_body",
|
|
"turn": 1,
|
|
"request": {"method": "POST", "path": "/v1/messages", "headers": {}, "body": None},
|
|
"response": {"status": 200, "headers": {}, "body": None},
|
|
},
|
|
{
|
|
"request_id": "req_string_bodies",
|
|
"turn": 2,
|
|
"request": {"method": "POST", "path": "/v1/messages", "headers": {}, "body": "not json"},
|
|
"response": {"status": 200, "headers": {}, "body": "plain text response"},
|
|
},
|
|
)
|
|
|
|
|
|
def _sidebar_order_records() -> tuple[dict[str, Any], ...]:
|
|
base = {
|
|
"duration_ms": 100,
|
|
"request": {"method": "POST", "path": "/v1/messages", "headers": {}, "body": {}},
|
|
"response": {"status": 200, "headers": {}, "body": {"content": [{"type": "text", "text": "OK"}]}},
|
|
}
|
|
rows = [
|
|
("req_order_unknown", 1, "other-model", "First sidebar task"),
|
|
("req_order_sonnet", 2, "aws.claude-sonnet-4.6", "Second sidebar task"),
|
|
("req_order_opus", 3, "aws.claude-opus-4.6", "Second sidebar task"),
|
|
]
|
|
records = []
|
|
for request_id, turn, model, prompt in rows:
|
|
record = json.loads(json.dumps(base))
|
|
record["request_id"] = request_id
|
|
record["turn"] = turn
|
|
record["timestamp"] = f"2026-05-13T13:2{turn}:00+00:00"
|
|
record["request"]["body"]["model"] = model
|
|
record["request"]["body"]["messages"] = [{"role": "user", "content": prompt}]
|
|
records.append(record)
|
|
return tuple(records)
|
|
|
|
|
|
def _claude_code_session_round_records() -> tuple[dict[str, Any], ...]:
|
|
model = "aws.claude-opus-4.6"
|
|
|
|
def make_record(
|
|
request_id: str,
|
|
turn: int,
|
|
messages: list[dict[str, Any]],
|
|
response_content: list[dict[str, Any]],
|
|
*,
|
|
system: str | None = None,
|
|
stop_reason: str = "end_turn",
|
|
) -> dict[str, Any]:
|
|
body: dict[str, Any] = {"model": model, "messages": messages}
|
|
if system is not None:
|
|
body["system"] = system
|
|
return {
|
|
"request_id": request_id,
|
|
"turn": turn,
|
|
"timestamp": f"2026-05-13T13:{20 + turn:02d}:00+00:00",
|
|
"duration_ms": 100,
|
|
"request": {"method": "POST", "path": "/v1/messages", "headers": {}, "body": body},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {"stop_reason": stop_reason, "content": response_content},
|
|
},
|
|
}
|
|
|
|
first_prompt = "Check configured MCP settings"
|
|
second_prompt = "Diagnose portfolio holdings"
|
|
third_prompt = "Add portfolio positions"
|
|
title_system = 'Generate a concise, sentence-case title for the session. Return JSON with a single "title" field.'
|
|
first_tool = {"type": "tool_use", "id": "tool_1", "name": "ListMcpResourcesTool", "input": {}}
|
|
second_tool = {"type": "tool_use", "id": "tool_2", "name": "portfolio", "input": {}}
|
|
third_tool = {"type": "tool_use", "id": "tool_3", "name": "search_stock_by_name", "input": {"query": "ACME"}}
|
|
|
|
return (
|
|
make_record(
|
|
"req_title",
|
|
1,
|
|
[{"role": "user", "content": [{"type": "text", "text": f"<session>\n{first_prompt}\n</session>"}]}],
|
|
[{"type": "text", "text": '{"title": "Check configured MCP settings"}'}],
|
|
system=title_system,
|
|
),
|
|
make_record(
|
|
"req_first_tool",
|
|
2,
|
|
[{"role": "user", "content": first_prompt}],
|
|
[first_tool],
|
|
stop_reason="tool_use",
|
|
),
|
|
make_record(
|
|
"req_first_final",
|
|
3,
|
|
[
|
|
{"role": "user", "content": first_prompt},
|
|
{"role": "assistant", "content": [first_tool]},
|
|
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tool_1", "content": "mcp list"}]},
|
|
],
|
|
[{"type": "text", "text": "Configured MCP server: wyckoff."}],
|
|
),
|
|
make_record(
|
|
"req_second_tool",
|
|
4,
|
|
[
|
|
{"role": "user", "content": first_prompt},
|
|
{"role": "assistant", "content": "Configured MCP server: wyckoff."},
|
|
{"role": "user", "content": second_prompt},
|
|
],
|
|
[second_tool],
|
|
stop_reason="tool_use",
|
|
),
|
|
make_record(
|
|
"req_second_final",
|
|
5,
|
|
[
|
|
{"role": "user", "content": first_prompt},
|
|
{"role": "assistant", "content": "Configured MCP server: wyckoff."},
|
|
{"role": "user", "content": second_prompt},
|
|
{"role": "assistant", "content": [second_tool]},
|
|
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tool_2", "content": "empty"}]},
|
|
],
|
|
[{"type": "text", "text": "No holdings found."}],
|
|
),
|
|
make_record(
|
|
"req_third_tool",
|
|
6,
|
|
[
|
|
{"role": "user", "content": first_prompt},
|
|
{"role": "assistant", "content": "Configured MCP server: wyckoff."},
|
|
{"role": "user", "content": second_prompt},
|
|
{"role": "assistant", "content": "No holdings found."},
|
|
{"role": "user", "content": third_prompt},
|
|
],
|
|
[third_tool],
|
|
stop_reason="tool_use",
|
|
),
|
|
make_record(
|
|
"req_third_suggestion",
|
|
7,
|
|
[
|
|
{"role": "user", "content": first_prompt},
|
|
{"role": "assistant", "content": "Configured MCP server: wyckoff."},
|
|
{"role": "user", "content": second_prompt},
|
|
{"role": "assistant", "content": "No holdings found."},
|
|
{"role": "user", "content": third_prompt},
|
|
{"role": "assistant", "content": "Portfolio diagnosis complete."},
|
|
{"role": "user", "content": "[SUGGESTION MODE: Suggest what the user might naturally type next.]"},
|
|
],
|
|
[{"type": "text", "text": "Give me an action plan."}],
|
|
),
|
|
)
|
|
|
|
|
|
def _codex_app_large_session_records() -> tuple[dict[str, Any], ...]:
|
|
records: list[dict[str, Any]] = []
|
|
session_id = "codex-session-alpha"
|
|
prompts = [
|
|
"Write Codex App runtime wiki",
|
|
"Review live dashboard capture",
|
|
"Fix duplicate Codex App trace rows",
|
|
]
|
|
for turn in range(1, 61):
|
|
prompt_index = (turn - 1) // 20
|
|
prompt = prompts[prompt_index]
|
|
step = (turn - 1) % 20
|
|
hour = 10 + (turn - 1) // 60
|
|
minute = (turn - 1) % 60
|
|
injected_user_messages = [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "input_text", "text": "# AGENTS.md instructions\nSkip maintainer automation notes."}
|
|
],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "<environment_context>\nskip cwd\n</environment_context>"}],
|
|
},
|
|
]
|
|
prior_messages: list[dict[str, Any]] = []
|
|
for prior_prompt in prompts[:prompt_index]:
|
|
prior_messages.extend(
|
|
[
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": prior_prompt}],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": f"Finished {prior_prompt}."}],
|
|
},
|
|
]
|
|
)
|
|
user_message = {
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": prompt}],
|
|
}
|
|
continuation_messages: list[dict[str, Any]] = []
|
|
if step:
|
|
continuation_messages = [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": f"Working on {prompt} step {step}."}],
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": f"call-{turn}",
|
|
"output": f"step {step} output",
|
|
},
|
|
]
|
|
records.append(
|
|
{
|
|
"timestamp": f"2026-06-13T{hour:02d}:{minute:02d}:00+00:00",
|
|
"request_id": f"req_codexapp_{turn}",
|
|
"turn": turn,
|
|
"duration_ms": 0,
|
|
"transport": "codex-app-transcript",
|
|
"request": {
|
|
"method": "CODEX_APP_TRANSCRIPT",
|
|
"path": "/v1/responses",
|
|
"headers": {"x-codex-app-session-id": session_id},
|
|
"body": {
|
|
"model": "gpt-5.5",
|
|
"metadata": {"codex_app_session_id": session_id},
|
|
"input": [
|
|
*injected_user_messages,
|
|
*prior_messages,
|
|
user_message,
|
|
*continuation_messages,
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"id": f"resp_codexapp_{turn}",
|
|
"status": "completed",
|
|
"model": "gpt-5.5",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": f"Answer {turn}."}],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": turn, "output_tokens": 1, "total_tokens": turn + 1},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
return tuple(records)
|
|
|
|
|
|
def _codex_display_turn_records() -> tuple[dict[str, Any], ...]:
|
|
return (
|
|
{
|
|
"timestamp": "2026-06-01T00:57:34.069390+00:00",
|
|
"request_id": "req_models",
|
|
"turn": 1,
|
|
"duration_ms": 4031,
|
|
"request": {
|
|
"method": "GET",
|
|
"path": "/v1/models?client_version=0.134.0",
|
|
"headers": {},
|
|
"body": None,
|
|
},
|
|
"response": {"status": 200, "headers": {}, "body": {"data": []}},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:57:40.306340+00:00",
|
|
"request_id": "req_prefetch",
|
|
"turn": 2,
|
|
"duration_ms": 1643,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {"type": "response.create", "model": "gpt-5.5", "generate": False, "input": []},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_prefetch",
|
|
"model": "gpt-5.5",
|
|
"generate": False,
|
|
"output": [],
|
|
"usage": {"input_tokens": 10088, "output_tokens": 0},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:58:53.101027+00:00",
|
|
"request_id": "req_first_visible",
|
|
"turn": "2.2",
|
|
"duration_ms": 74200,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"type": "response.create",
|
|
"model": "gpt-5.5",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "Clean local branches."}],
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_first_visible",
|
|
"model": "gpt-5.5",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "I will inspect the worktree."}],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 23768, "output_tokens": 407},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:59:02.416139+00:00",
|
|
"request_id": "req_second_visible",
|
|
"turn": "2.3",
|
|
"duration_ms": 83000,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"type": "response.create",
|
|
"model": "gpt-5.5",
|
|
"previous_response_id": "resp_first_visible",
|
|
"input": [{"type": "function_call_output", "call_id": "call_status", "output": "{}"}],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_second_visible",
|
|
"model": "gpt-5.5",
|
|
"previous_response_id": "resp_first_visible",
|
|
"output": [
|
|
{
|
|
"type": "function_call",
|
|
"name": "exec_command",
|
|
"call_id": "call_branch",
|
|
"arguments": '{"cmd":"git branch"}',
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 25076, "output_tokens": 303},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:59:12.416139+00:00",
|
|
"request_id": "req_zero_output_visible",
|
|
"turn": "2.4",
|
|
"duration_ms": 5000,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"type": "response.create",
|
|
"model": "simple",
|
|
"previous_response_id": "resp_second_visible",
|
|
"input": [{"type": "function_call_output", "call_id": "call_branch", "output": "{}"}],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 500,
|
|
"headers": {},
|
|
"body": {
|
|
"error": {"message": "upstream timeout"},
|
|
"usage": {"input_tokens": 0, "output_tokens": 0},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
def _codex_lazy_display_turn_records() -> tuple[dict[str, Any], ...]:
|
|
records = list(_codex_display_turn_records())
|
|
for idx in range(60):
|
|
records.append(
|
|
{
|
|
"timestamp": f"2026-06-01T01:{idx % 60:02d}:00.000000+00:00",
|
|
"request_id": f"req_model_{idx}",
|
|
"turn": 100 + idx,
|
|
"duration_ms": 10,
|
|
"request": {
|
|
"method": "GET",
|
|
"path": "/v1/models",
|
|
"headers": {},
|
|
"body": None,
|
|
},
|
|
"response": {"status": 200, "headers": {}, "body": {"data": []}},
|
|
}
|
|
)
|
|
return tuple(records)
|
|
|
|
|
|
def _codex_direct_generate_false_records() -> tuple[dict[str, Any], ...]:
|
|
visible = json.loads(json.dumps(_responses_record()))
|
|
visible["request_id"] = "req_direct_visible"
|
|
visible["turn"] = 2
|
|
visible["request"]["body"]["model"] = "gpt-5.5"
|
|
visible["response"]["body"]["id"] = "resp_direct_visible"
|
|
visible["response"]["body"]["usage"] = {"input_tokens": 12, "output_tokens": 1}
|
|
|
|
return (
|
|
{
|
|
"timestamp": "2026-06-01T02:00:00.000000+00:00",
|
|
"request_id": "req_direct_prefetch",
|
|
"turn": 1,
|
|
"duration_ms": 20,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {"model": "gpt-5.5", "generate": False, "input": []},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_direct_prefetch",
|
|
"model": "gpt-5.5",
|
|
"generate": False,
|
|
"output": [],
|
|
"usage": {"input_tokens": 9, "output_tokens": 0},
|
|
},
|
|
},
|
|
},
|
|
visible,
|
|
)
|
|
|
|
|
|
def _codex_lazy_event_generate_false_records() -> tuple[dict[str, Any], ...]:
|
|
records: list[dict[str, Any]] = [
|
|
{
|
|
"timestamp": "2026-06-01T02:10:00.000000+00:00",
|
|
"request_id": "req_event_prefetch",
|
|
"turn": 1,
|
|
"duration_ms": 20,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {"type": "response.create", "model": "gpt-5.5", "input": []},
|
|
"ws_events": [
|
|
{
|
|
"type": "response.create",
|
|
"data": {"model": "gpt-5.5", "generate": False, "input": []},
|
|
}
|
|
],
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": None,
|
|
"ws_events": [
|
|
{
|
|
"type": "response.created",
|
|
"data": {
|
|
"response": {
|
|
"id": "resp_event_prefetch",
|
|
"model": "gpt-5.5",
|
|
"generate": False,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"type": "response.completed",
|
|
"data": {
|
|
"response": {
|
|
"id": "resp_event_prefetch",
|
|
"model": "gpt-5.5",
|
|
"generate": False,
|
|
"output": [],
|
|
"usage": {"input_tokens": 11, "output_tokens": 0},
|
|
}
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T02:10:01.000000+00:00",
|
|
"request_id": "req_event_visible",
|
|
"turn": 2,
|
|
"duration_ms": 50,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {"type": "response.create", "model": "gpt-5.5", "input": []},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": None,
|
|
"ws_events": [
|
|
{
|
|
"type": "response.created",
|
|
"data": {"response": {"id": "resp_event_visible", "model": "gpt-5.5"}},
|
|
},
|
|
{
|
|
"type": "response.output_item.done",
|
|
"data": {
|
|
"output_index": 0,
|
|
"item": {
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "event visible"}],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "response.completed",
|
|
"data": {
|
|
"response": {
|
|
"id": "resp_event_visible",
|
|
"model": "gpt-5.5",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "event visible"}],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 12, "output_tokens": 1},
|
|
}
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]
|
|
for idx in range(60):
|
|
records.append(
|
|
{
|
|
"timestamp": f"2026-06-01T02:11:{idx % 60:02d}.000000+00:00",
|
|
"request_id": f"req_event_model_{idx}",
|
|
"turn": 100 + idx,
|
|
"duration_ms": 10,
|
|
"request": {
|
|
"method": "GET",
|
|
"path": "/v1/models",
|
|
"headers": {},
|
|
"body": None,
|
|
},
|
|
"response": {"status": 200, "headers": {}, "body": {"data": []}},
|
|
}
|
|
)
|
|
return tuple(records)
|
|
|
|
|
|
def _write_trace(trace_path: Path, records: tuple[dict[str, Any], ...]) -> None:
|
|
trace_path.write_text(
|
|
"".join(json.dumps(record, ensure_ascii=False) + "\n" for record in records),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _generate_case_html(tmp_path: Path, name: str, records: tuple[dict[str, Any], ...]) -> Path:
|
|
trace_path = tmp_path / f"{name}.jsonl"
|
|
html_path = tmp_path / f"{name}.html"
|
|
_write_trace(trace_path, records)
|
|
_generate_html_viewer(trace_path, html_path)
|
|
return html_path
|
|
|
|
|
|
def _compact_contract_records() -> tuple[dict[str, Any], ...]:
|
|
records = []
|
|
for turn in (1, 2):
|
|
record = json.loads(json.dumps(_responses_record()))
|
|
record["request_id"] = f"req_compact_contract_{turn}"
|
|
record["turn"] = turn
|
|
record["request"]["body"]["instructions"] = "Shared compact system prompt. " * 120
|
|
record["request"]["body"]["tools"] = [
|
|
{
|
|
"type": "function",
|
|
"name": "exec_command",
|
|
"description": "Large repeated tool schema. " * 120,
|
|
"parameters": {"type": "object", "properties": {"cmd": {"type": "string"}}},
|
|
}
|
|
]
|
|
record["request"]["body"]["input"] = [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": f"Compact bundle turn {turn}."}],
|
|
}
|
|
]
|
|
record["response"]["body"]["output"] = [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": f"Compact response {turn}."}],
|
|
}
|
|
]
|
|
records.append(record)
|
|
return tuple(records)
|
|
|
|
|
|
def _open_viewer_with_error_capture(page: Page, html_path: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
page.on("pageerror", lambda exc: errors.append(f"pageerror: {exc}"))
|
|
page.on("console", lambda msg: errors.append(f"console.error: {msg.text}") if msg.type == "error" else None)
|
|
page.goto(html_path.resolve().as_uri(), timeout=10000)
|
|
page.wait_for_selector(".sidebar-item", timeout=5000)
|
|
return errors
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def chromium_browser():
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch(headless=True)
|
|
yield browser
|
|
browser.close()
|
|
|
|
|
|
@pytest.mark.parametrize("case", _contract_cases(), ids=lambda case: case.name)
|
|
def test_viewer_semantic_contracts_across_supported_trace_shapes(
|
|
tmp_path: Path, chromium_browser, case: ViewerContractCase
|
|
) -> None:
|
|
html_path = _generate_case_html(tmp_path, case.name, case.records)
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
page.locator(".sidebar-item").nth(case.entry_index).click()
|
|
page.wait_for_selector("#detail .section", timeout=5000)
|
|
|
|
result = page.evaluate(
|
|
"""(entryIndex) => {
|
|
const entry = entries[entryIndex];
|
|
const body = entry.request.body;
|
|
const output = getResponseOutput(entry);
|
|
const usage = getUsage(entry);
|
|
return {
|
|
sectionTitles: Array.from(document.querySelectorAll('#detail .section .title')).map(el => el.textContent),
|
|
sidebarLabel: document.querySelectorAll('.sidebar-item .si-task')[entryIndex]?.textContent || '',
|
|
system: extractSystem(body) || '',
|
|
roles: getMessages(body).map(message => message.role),
|
|
tools: getRequestTools(body).map(toolDisplayName),
|
|
outputTypes: (output?.content || []).map(block => block.type),
|
|
usage,
|
|
eventCount: getResponseEvents(entry).length,
|
|
detailText: document.querySelector('#detail').innerText,
|
|
};
|
|
}""",
|
|
case.entry_index,
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert "Full JSON" in result["sectionTitles"]
|
|
assert any(title != "Full JSON" for title in result["sectionTitles"])
|
|
for section in case.expected_sections:
|
|
assert section in result["sectionTitles"]
|
|
if case.expected_system is not None:
|
|
assert result["system"] == case.expected_system
|
|
if case.expected_sidebar_label is not None:
|
|
assert result["sidebarLabel"] == case.expected_sidebar_label
|
|
assert result["roles"] == list(case.expected_roles)
|
|
assert result["tools"] == list(case.expected_tools)
|
|
assert result["outputTypes"] == list(case.expected_output_types)
|
|
for key, value in case.expected_usage.items():
|
|
assert result["usage"][key] == value
|
|
assert result["eventCount"] >= case.min_stream_events
|
|
for text in case.required_detail_text:
|
|
assert text in result["detailText"]
|
|
|
|
|
|
def test_viewer_detail_tabs_keep_default_view_and_expose_trace_mode(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "detail_tabs", (_responses_record(),))
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
page.locator(".sidebar-item").first.click()
|
|
page.wait_for_selector('#detail .detail-tab[data-tab="default"].active', timeout=5000)
|
|
|
|
default_state = page.evaluate(
|
|
"""() => ({
|
|
tabs: Array.from(document.querySelectorAll('#detail .detail-tab')).map(el => ({
|
|
mode: el.dataset.tab,
|
|
label: el.querySelector('span:not(.tab-count)')?.textContent || '',
|
|
active: el.classList.contains('active'),
|
|
})),
|
|
sectionTitles: Array.from(document.querySelectorAll('#detail .section .title')).map(el => el.textContent),
|
|
text: document.querySelector('#detail')?.innerText || '',
|
|
})"""
|
|
)
|
|
|
|
page.locator('#detail .detail-tab[data-tab="trace"]').click()
|
|
page.wait_for_selector('#detail .detail-tab[data-tab="trace"].active', timeout=5000)
|
|
trace_state = page.evaluate(
|
|
"""() => ({
|
|
sectionCount: document.querySelectorAll('#detail .section').length,
|
|
blockTitles: Array.from(document.querySelectorAll('#detail .trace-block-title .trace-title')).map(el => el.textContent),
|
|
copyButtons: Array.from(document.querySelectorAll('#detail .trace-copy-btn')).map(el => el.textContent),
|
|
formats: Array.from(document.querySelectorAll('#detail .trace-format-btn')).map(el => ({
|
|
format: el.dataset.format,
|
|
label: el.textContent,
|
|
active: el.classList.contains('active'),
|
|
})),
|
|
text: document.querySelector('#detail')?.innerText || '',
|
|
})"""
|
|
)
|
|
page.evaluate(
|
|
"""() => {
|
|
window.__copiedTraceText = '';
|
|
window.copyToClipboard = (text, btn) => {
|
|
window.__copiedTraceText = text;
|
|
if (btn) btn.textContent = t('copied');
|
|
return Promise.resolve();
|
|
};
|
|
}"""
|
|
)
|
|
page.locator("#detail .trace-copy-btn").first.click()
|
|
page.wait_for_function("window.__copiedTraceText.includes('Run pwd.')")
|
|
copied_trace_text = page.evaluate("window.__copiedTraceText")
|
|
|
|
page.locator('#detail .trace-format-btn[data-format="yaml"]').click()
|
|
page.wait_for_selector('#detail .trace-format-btn[data-format="yaml"].active', timeout=5000)
|
|
yaml_state = page.evaluate(
|
|
"""() => ({
|
|
text: document.querySelector('#detail')?.innerText || '',
|
|
codeFormat: document.querySelector('#detail .trace-code')?.dataset.format || '',
|
|
prettyCount: document.querySelectorAll('#detail .trace-pretty').length,
|
|
})"""
|
|
)
|
|
|
|
page.locator('#detail .trace-format-btn[data-format="pretty"]').click()
|
|
page.wait_for_selector('#detail .trace-format-btn[data-format="pretty"].active', timeout=5000)
|
|
pretty_state = page.evaluate(
|
|
"""() => ({
|
|
text: document.querySelector('#detail')?.innerText || '',
|
|
codeCount: document.querySelectorAll('#detail .trace-code').length,
|
|
prettyCount: document.querySelectorAll('#detail .trace-pretty').length,
|
|
})"""
|
|
)
|
|
|
|
remaining_tabs = page.evaluate(
|
|
"() => Array.from(document.querySelectorAll('#detail .detail-tab')).map(el => el.dataset.tab)"
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert default_state["tabs"] == [
|
|
{"mode": "default", "label": "Default", "active": True},
|
|
{"mode": "trace", "label": "Trace", "active": False},
|
|
]
|
|
assert default_state["sectionTitles"] == ["Tools", "System Prompt", "Messages", "Response", "Full JSON"]
|
|
assert "Responses final OK." in default_state["text"]
|
|
assert "Diff with Prev" in default_state["text"]
|
|
assert trace_state["sectionCount"] == 0
|
|
assert trace_state["blockTitles"] == ["Input", "Output", "Metadata"]
|
|
assert trace_state["copyButtons"] == ["Copy", "Copy", "Copy"]
|
|
assert '"messages"' in copied_trace_text
|
|
assert "Run pwd." in copied_trace_text
|
|
assert trace_state["formats"] == [
|
|
{"format": "json", "label": "JSON", "active": True},
|
|
{"format": "yaml", "label": "YAML", "active": False},
|
|
{"format": "pretty", "label": "Pretty", "active": False},
|
|
]
|
|
assert '"messages"' in trace_state["text"]
|
|
assert "req_responses_contract" in trace_state["text"]
|
|
assert "Responses final OK." in trace_state["text"]
|
|
assert yaml_state["codeFormat"] == "yaml"
|
|
assert yaml_state["prettyCount"] == 0
|
|
assert "messages:" in yaml_state["text"]
|
|
assert "req_responses_contract" in yaml_state["text"]
|
|
assert pretty_state["codeCount"] == 0
|
|
assert pretty_state["prettyCount"] == 3
|
|
assert "messages" in pretty_state["text"]
|
|
assert "req_responses_contract" in pretty_state["text"]
|
|
assert remaining_tabs == ["default", "trace"]
|
|
|
|
|
|
def test_viewer_tool_call_params_can_expand_escaped_string_newlines(tmp_path: Path, chromium_browser) -> None:
|
|
record = json.loads(json.dumps(_responses_record()))
|
|
decoded_command = "python - <<'PY'\nprint(\"hello\")\nPY"
|
|
record["response"]["body"]["output"][0]["arguments"] = json.dumps(
|
|
{"cmd": decoded_command, "yield_time_ms": 1000},
|
|
ensure_ascii=False,
|
|
)
|
|
html_path = _generate_case_html(tmp_path, "tool_call_param_escapes", (record,))
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
page.locator(".sidebar-item").first.click()
|
|
page.wait_for_selector("#detail .section", timeout=5000)
|
|
page.wait_for_selector("#detail .tool-input-toggle", timeout=5000)
|
|
page.evaluate(
|
|
"""() => {
|
|
window.__copiedToolInput = '';
|
|
window.copyToClipboard = (text) => {
|
|
window.__copiedToolInput = text;
|
|
return Promise.resolve();
|
|
};
|
|
}"""
|
|
)
|
|
|
|
input_view = page.locator("#detail .tool-input-view").first
|
|
raw_text = input_view.inner_text()
|
|
input_view_count = page.locator("#detail .tool-input-view").count()
|
|
toggle_text_before = page.locator("#detail .tool-input-toggle").first.inner_text()
|
|
copy_button_count = page.locator("#detail .tool-input-copy").count()
|
|
page.locator("#detail .tool-input-copy").first.click()
|
|
copied_raw_text = page.evaluate("window.__copiedToolInput")
|
|
page.locator("#detail .tool-input-toggle").first.click()
|
|
page.wait_for_selector("#detail .tool-input-view.expanded", timeout=5000)
|
|
decoded_text = input_view.inner_text()
|
|
toggle_text_expanded = page.locator("#detail .tool-input-toggle").first.inner_text()
|
|
page.locator("#detail .tool-input-copy").first.click()
|
|
copied_decoded_text = page.evaluate("window.__copiedToolInput")
|
|
expanded_view_count = page.locator("#detail .tool-input-view").count()
|
|
decoded_box_count = page.locator("#detail .tool-input-decoded").count()
|
|
decoded_copy_button_count = page.locator("#detail .tool-input-copy-decoded").count()
|
|
page.locator("#detail .tool-input-toggle").first.click()
|
|
restored_text = input_view.inner_text()
|
|
full_json_buttons = page.locator("#detail .json-view .tool-input-toggle").count()
|
|
full_json_copy_buttons = page.locator("#detail .json-view .tool-input-copy").count()
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert "\\nprint" in raw_text
|
|
assert "python - <<'PY'\nprint(\"hello\")\nPY" in decoded_text
|
|
assert copied_raw_text == raw_text
|
|
assert copied_decoded_text == decoded_text
|
|
assert toggle_text_before == "\u21b5"
|
|
assert toggle_text_expanded == "Raw"
|
|
assert restored_text == raw_text
|
|
assert input_view_count == expanded_view_count == 1
|
|
assert decoded_box_count == 0
|
|
assert copy_button_count == 1
|
|
assert decoded_copy_button_count == 0
|
|
assert full_json_buttons == 0
|
|
assert full_json_copy_buttons == 0
|
|
|
|
|
|
def test_viewer_renders_embedded_and_dropped_compact_trace_bundle(tmp_path: Path, chromium_browser) -> None:
|
|
records = _compact_contract_records()
|
|
bundle = build_compact_trace_bundle(list(records))
|
|
html_path = tmp_path / "compact.html"
|
|
bundle_path = tmp_path / "compact.ctap.json"
|
|
blank_html_path = tmp_path / "blank.html"
|
|
|
|
_generate_html_viewer_from_compact_bundle(
|
|
bundle,
|
|
html_path,
|
|
display_trace_path=bundle_path,
|
|
display_html_path=html_path,
|
|
)
|
|
bundle_path.write_text(json.dumps(bundle, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
|
|
blank_html_path.write_text(_read_viewer_template(), encoding="utf-8")
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
page.locator(".sidebar-item").nth(1).click()
|
|
page.wait_for_selector("#detail .section", timeout=5000)
|
|
embedded_state = page.evaluate(
|
|
"""() => ({
|
|
entryCount: entries.length,
|
|
hasBlobRef: JSON.stringify(EMBEDDED_TRACE_COMPACT_DATA).includes('__claude_tap_blob_ref__'),
|
|
detailText: document.querySelector('#detail')?.innerText || '',
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert embedded_state["entryCount"] == 2
|
|
assert embedded_state["hasBlobRef"] is True
|
|
assert "Compact bundle turn 2." in embedded_state["detailText"]
|
|
assert "Compact response 2." in embedded_state["detailText"]
|
|
|
|
drop_page = chromium_browser.new_page()
|
|
drop_errors: list[str] = []
|
|
drop_page.on("pageerror", lambda exc: drop_errors.append(f"pageerror: {exc}"))
|
|
drop_page.on(
|
|
"console", lambda msg: drop_errors.append(f"console.error: {msg.text}") if msg.type == "error" else None
|
|
)
|
|
try:
|
|
drop_page.goto(blank_html_path.resolve().as_uri(), timeout=10000)
|
|
drop_page.set_input_files("#file-input", str(bundle_path))
|
|
drop_page.wait_for_selector(".sidebar-item", timeout=5000)
|
|
dropped_state = drop_page.evaluate(
|
|
"""() => ({
|
|
entryCount: entries.length,
|
|
sidebarText: document.querySelector('#sidebar')?.innerText || '',
|
|
detailText: document.querySelector('#detail')?.innerText || '',
|
|
})"""
|
|
)
|
|
finally:
|
|
drop_page.close()
|
|
|
|
assert drop_errors == []
|
|
assert dropped_state["entryCount"] == 2
|
|
assert "Compact bundle turn 1." in dropped_state["detailText"]
|
|
assert "gpt-5.4" in dropped_state["sidebarText"]
|
|
|
|
|
|
def test_viewer_does_not_synthesize_messages_for_empty_responses_input(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "responses_empty_input", (_responses_empty_input_record(),))
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
page.locator(".sidebar-item").first.click()
|
|
page.wait_for_selector("#detail .section", timeout=5000)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
roles: getMessages(entries[0].request.body).map(message => message.role),
|
|
sectionTitles: Array.from(document.querySelectorAll('#detail .section .title')).map(el => el.textContent),
|
|
detailText: document.querySelector('#detail')?.innerText || '',
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["roles"] == []
|
|
assert "System Prompt" in state["sectionTitles"]
|
|
assert "Tools" in state["sectionTitles"]
|
|
assert "Messages" not in state["sectionTitles"]
|
|
assert "You are Codex contract system prompt." in state["detailText"]
|
|
|
|
|
|
def test_viewer_sidebar_order_can_switch_between_model_turn_and_session_sequence(
|
|
tmp_path: Path, chromium_browser
|
|
) -> None:
|
|
html_path = _generate_case_html(tmp_path, "sidebar_order", _sidebar_order_records())
|
|
|
|
page = chromium_browser.new_page()
|
|
page.add_init_script("localStorage.setItem('claude-tap-sidebar-order', 'model')")
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
model_state = page.evaluate(
|
|
"""() => ({
|
|
label: document.querySelector('#sidebar-sort-label')?.textContent || '',
|
|
buttons: Array.from(document.querySelectorAll('.sidebar-sort-btn')).map(el => ({
|
|
mode: el.dataset.sortMode,
|
|
label: el.textContent,
|
|
active: el.classList.contains('active'),
|
|
})),
|
|
groups: Array.from(document.querySelectorAll('.sidebar-group-header .group-name')).map(el => el.textContent),
|
|
turns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
})"""
|
|
)
|
|
|
|
page.locator('.sidebar-sort-btn[data-sort-mode="turn"]').click()
|
|
page.wait_for_selector('.sidebar-sort-btn[data-sort-mode="turn"].active', timeout=5000)
|
|
turn_state = page.evaluate(
|
|
"""() => ({
|
|
buttons: Array.from(document.querySelectorAll('.sidebar-sort-btn')).map(el => ({
|
|
mode: el.dataset.sortMode,
|
|
label: el.textContent,
|
|
active: el.classList.contains('active'),
|
|
})),
|
|
groupCount: document.querySelectorAll('.sidebar-group-header').length,
|
|
turns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
})"""
|
|
)
|
|
|
|
page.locator('.sidebar-sort-btn[data-sort-mode="session"]').click()
|
|
page.wait_for_selector('.sidebar-sort-btn[data-sort-mode="session"].active', timeout=5000)
|
|
session_state = page.evaluate(
|
|
"""() => ({
|
|
buttons: Array.from(document.querySelectorAll('.sidebar-sort-btn')).map(el => ({
|
|
mode: el.dataset.sortMode,
|
|
label: el.textContent,
|
|
active: el.classList.contains('active'),
|
|
})),
|
|
groups: Array.from(document.querySelectorAll('.sidebar-group-header .group-name')).map(el => el.textContent),
|
|
counts: Array.from(document.querySelectorAll('.sidebar-group-header .group-count')).map(el => el.textContent),
|
|
turns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert model_state["label"] == "Order"
|
|
assert model_state["buttons"] == [
|
|
{"mode": "model", "label": "Model", "active": True},
|
|
{"mode": "turn", "label": "Turn", "active": False},
|
|
{"mode": "session", "label": "Query", "active": False},
|
|
]
|
|
assert model_state["groups"] == ["aws.claude-opus-4.6", "aws.claude-sonnet-4.6", "other-model"]
|
|
assert model_state["turns"] == ["Turn 3", "Turn 2", "Turn 1"]
|
|
assert turn_state["buttons"] == [
|
|
{"mode": "model", "label": "Model", "active": False},
|
|
{"mode": "turn", "label": "Turn", "active": True},
|
|
{"mode": "session", "label": "Query", "active": False},
|
|
]
|
|
assert turn_state["groupCount"] == 0
|
|
assert turn_state["turns"] == ["Turn 1", "Turn 2", "Turn 3"]
|
|
assert session_state["buttons"] == [
|
|
{"mode": "model", "label": "Model", "active": False},
|
|
{"mode": "turn", "label": "Turn", "active": False},
|
|
{"mode": "session", "label": "Query", "active": True},
|
|
]
|
|
assert session_state["groups"] == [
|
|
"Query 1 - First sidebar task",
|
|
"Query 2 - Second sidebar task",
|
|
"Query 3 - Second sidebar task",
|
|
]
|
|
assert session_state["counts"] == ["1", "1", "1"]
|
|
assert session_state["turns"] == ["Turn 1", "Turn 2", "Turn 3"]
|
|
|
|
|
|
def test_viewer_session_order_groups_claude_code_tool_loop_rounds(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "claude_code_session_rounds", _claude_code_session_round_records())
|
|
|
|
page = chromium_browser.new_page()
|
|
page.add_init_script("localStorage.setItem('claude-tap-sidebar-order', 'session')")
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
groups: Array.from(document.querySelectorAll('.sidebar-group-header .group-name')).map(el => el.textContent),
|
|
counts: Array.from(document.querySelectorAll('.sidebar-group-header .group-count')).map(el => el.textContent),
|
|
turns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
headerText: document.querySelector('#sidebar')?.innerText || '',
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["groups"] == [
|
|
"Query 1 - Check configured MCP settings",
|
|
"Query 2 - Diagnose portfolio holdings",
|
|
"Query 3 - Add portfolio positions",
|
|
]
|
|
assert state["counts"] == ["3", "2", "2"]
|
|
assert state["turns"] == ["Turn 1", "Turn 2", "Turn 3", "Turn 4", "Turn 5", "Turn 6", "Turn 7"]
|
|
assert "SUGGESTION MODE" not in state["headerText"]
|
|
|
|
|
|
def test_viewer_session_order_groups_large_codex_app_sessions_in_virtual_mode(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "codex_app_large_sessions", _codex_app_large_session_records())
|
|
|
|
page = chromium_browser.new_page()
|
|
page.add_init_script("localStorage.setItem('claude-tap-sidebar-order', 'session')")
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
virtualMode,
|
|
groups: Array.from(document.querySelectorAll('.sidebar-group-header .group-name')).map(el => el.textContent),
|
|
counts: Array.from(document.querySelectorAll('.sidebar-group-header .group-count')).map(el => el.textContent),
|
|
virtualGroups: vsFilteredItems
|
|
.filter(row => row.type === 'group')
|
|
.map(row => row.group.userText),
|
|
virtualCounts: vsFilteredItems
|
|
.filter(row => row.type === 'group')
|
|
.map(row => String(row.group.items.length)),
|
|
rowCount: vsFilteredItems.length,
|
|
visualCount: visualOrder.length,
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["virtualMode"] is True
|
|
assert state["groups"] == ["Query 1 - Write Codex App runtime wiki"]
|
|
assert state["counts"] == ["20"]
|
|
assert state["virtualGroups"] == [
|
|
"Write Codex App runtime wiki",
|
|
"Review live dashboard capture",
|
|
"Fix duplicate Codex App trace rows",
|
|
]
|
|
assert state["virtualCounts"] == ["20", "20", "20"]
|
|
assert state["rowCount"] == 63
|
|
assert state["visualCount"] == 60
|
|
|
|
|
|
def test_viewer_codex_display_turns_skip_capture_control_records(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "codex_display_turns", _codex_display_turn_records())
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
sidebarTurns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
codexEntries: entries
|
|
.filter(entry => entry.request?.path === '/v1/responses')
|
|
.map(entry => ({
|
|
turn: entry.turn,
|
|
displayTurn: entry.display_turn,
|
|
captureTurn: entry.capture_turn,
|
|
requestId: entry.request_id,
|
|
})),
|
|
resetTurns: (() => {
|
|
const extra = JSON.parse(JSON.stringify(entries.find(entry => entry.request?.path === '/v1/responses' && entry.display_turn === 3)));
|
|
delete extra.display_turn;
|
|
extra.turn = '2.5';
|
|
extra.capture_turn = '2.5';
|
|
extra.request_id = 'req_after_reset';
|
|
return normalizeDisplayTurns([...entries, extra], true)
|
|
.filter(entry => entry.request?.path === '/v1/responses' && entry.display_turn !== undefined)
|
|
.map(entry => entry.display_turn);
|
|
})(),
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["sidebarTurns"] == ["Turn 1", "Turn 2", "Turn 3"]
|
|
assert state["codexEntries"] == [
|
|
{
|
|
"turn": "2.2",
|
|
"displayTurn": 1,
|
|
"captureTurn": "2.2",
|
|
"requestId": "req_first_visible",
|
|
},
|
|
{
|
|
"turn": "2.3",
|
|
"displayTurn": 2,
|
|
"captureTurn": "2.3",
|
|
"requestId": "req_second_visible",
|
|
},
|
|
{
|
|
"turn": "2.4",
|
|
"displayTurn": 3,
|
|
"captureTurn": "2.4",
|
|
"requestId": "req_zero_output_visible",
|
|
},
|
|
]
|
|
assert state["resetTurns"] == [1, 2, 3, 4]
|
|
|
|
|
|
def test_viewer_direct_responses_generate_false_is_not_navigable(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(
|
|
tmp_path,
|
|
"codex_direct_generate_false",
|
|
_codex_direct_generate_false_records(),
|
|
)
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
sidebarTurns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
filteredRequestIds: filtered.map(entry => entry.request_id),
|
|
entriesById: Object.fromEntries(entries.map(entry => [entry.request_id, {
|
|
displayTurn: entry.display_turn ?? null,
|
|
captureTurn: entry.capture_turn ?? null,
|
|
navigable: isNavigableTraceEntry(entry),
|
|
}])),
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["sidebarTurns"] == ["Turn 1"]
|
|
assert state["filteredRequestIds"] == ["req_direct_visible"]
|
|
assert state["entriesById"]["req_direct_prefetch"] == {
|
|
"displayTurn": None,
|
|
"captureTurn": 1,
|
|
"navigable": False,
|
|
}
|
|
assert state["entriesById"]["req_direct_visible"] == {
|
|
"displayTurn": 1,
|
|
"captureTurn": 2,
|
|
"navigable": True,
|
|
}
|
|
|
|
|
|
def test_viewer_codex_lazy_display_turns_skip_capture_control_records(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "codex_lazy_display_turns", _codex_lazy_display_turn_records())
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
usesCompactBundle: typeof EMBEDDED_TRACE_COMPACT_DATA !== 'undefined',
|
|
usesMetadataMode: typeof EMBEDDED_TRACE_META !== 'undefined',
|
|
sidebarTurns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
sidebarPaths: Array.from(document.querySelectorAll('.sidebar-item .si-path')).map(el => el.textContent),
|
|
filteredRequestIds: filtered.map(entry => entry.request_id),
|
|
codexEntries: entries
|
|
.filter(entry => entry.request?.path === '/v1/responses')
|
|
.map(entry => ({
|
|
requestId: entry.request_id,
|
|
turn: entry.turn,
|
|
displayTurn: entry.display_turn ?? null,
|
|
captureTurn: entry.capture_turn ?? null,
|
|
responseOutputCount: entry.response?.body?.output?.length ?? 0,
|
|
outputTokens: entry.response?.body?.usage?.output_tokens ?? 0,
|
|
navigable: isNavigableTraceEntry(entry),
|
|
})),
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["usesCompactBundle"] is True
|
|
assert state["usesMetadataMode"] is False
|
|
assert state["sidebarTurns"] == ["Turn 1", "Turn 2", "Turn 3"]
|
|
assert state["sidebarPaths"] == ["WEBSOCKET /v1/responses", "WEBSOCKET /v1/responses", "WEBSOCKET /v1/responses"]
|
|
assert state["filteredRequestIds"] == ["req_first_visible", "req_second_visible", "req_zero_output_visible"]
|
|
assert state["codexEntries"] == [
|
|
{
|
|
"requestId": "req_first_visible",
|
|
"turn": "2.2",
|
|
"displayTurn": 1,
|
|
"captureTurn": "2.2",
|
|
"responseOutputCount": 1,
|
|
"outputTokens": 407,
|
|
"navigable": True,
|
|
},
|
|
{
|
|
"requestId": "req_second_visible",
|
|
"turn": "2.3",
|
|
"displayTurn": 2,
|
|
"captureTurn": "2.3",
|
|
"responseOutputCount": 1,
|
|
"outputTokens": 303,
|
|
"navigable": True,
|
|
},
|
|
{
|
|
"requestId": "req_zero_output_visible",
|
|
"turn": "2.4",
|
|
"displayTurn": 3,
|
|
"captureTurn": "2.4",
|
|
"responseOutputCount": 0,
|
|
"outputTokens": 0,
|
|
"navigable": True,
|
|
},
|
|
]
|
|
|
|
|
|
def test_viewer_codex_lazy_stubs_read_generate_from_websocket_events(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(
|
|
tmp_path,
|
|
"codex_lazy_event_generate_false",
|
|
_codex_lazy_event_generate_false_records(),
|
|
)
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
usesCompactBundle: typeof EMBEDDED_TRACE_COMPACT_DATA !== 'undefined',
|
|
usesMetadataMode: typeof EMBEDDED_TRACE_META !== 'undefined',
|
|
sidebarTurns: Array.from(document.querySelectorAll('.sidebar-item .si-turn')).map(el => el.textContent),
|
|
filteredRequestIds: filtered.map(entry => entry.request_id),
|
|
eventEntries: entries
|
|
.filter(entry => entry.request_id.startsWith('req_event_') && entry.request?.path === '/v1/responses')
|
|
.map(entry => ({
|
|
requestId: entry.request_id,
|
|
displayTurn: entry.display_turn ?? null,
|
|
captureTurn: entry.capture_turn ?? null,
|
|
requestGenerate: entry.request?.body?.generate ?? null,
|
|
responseGenerate: entry.response?.body?.generate ?? null,
|
|
responseOutputCount: entry.response?.body?.output?.length ?? 0,
|
|
outputTokens: entry.response?.body?.usage?.output_tokens ?? 0,
|
|
navigable: isNavigableTraceEntry(entry),
|
|
})),
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["usesCompactBundle"] is True
|
|
assert state["usesMetadataMode"] is False
|
|
assert state["sidebarTurns"] == ["Turn 1"]
|
|
assert state["filteredRequestIds"] == ["req_event_visible"]
|
|
assert state["eventEntries"] == [
|
|
{
|
|
"requestId": "req_event_visible",
|
|
"displayTurn": 1,
|
|
"captureTurn": 2,
|
|
"requestGenerate": None,
|
|
"responseGenerate": None,
|
|
"responseOutputCount": 1,
|
|
"outputTokens": 1,
|
|
"navigable": True,
|
|
},
|
|
]
|
|
|
|
|
|
def test_viewer_codex_lazy_trace_tab_preserves_display_turn_metadata(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(
|
|
tmp_path,
|
|
"codex_lazy_trace_display_turns",
|
|
_codex_lazy_display_turn_records(),
|
|
)
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => {
|
|
function metadataText() {
|
|
const blocks = Array.from(document.querySelectorAll('#detail .trace-block'));
|
|
const block = blocks.find(el => el.querySelector('.trace-title')?.textContent === 'Metadata');
|
|
return block?.innerText || '';
|
|
}
|
|
const idx = filtered.findIndex(entry => entry.request?.path === '/v1/responses' && entry.display_turn === 1);
|
|
selectEntry(idx);
|
|
document.querySelector('#detail .detail-tab[data-tab="trace"]').click();
|
|
const jsonMetadata = metadataText();
|
|
document.querySelector('#detail .trace-format-btn[data-format="yaml"]').click();
|
|
const yamlMetadata = metadataText();
|
|
document.querySelector('#detail .trace-format-btn[data-format="pretty"]').click();
|
|
const prettyMetadata = metadataText();
|
|
return {
|
|
selectedIdx: idx,
|
|
activeDisplayTurn: filtered[activeIdx]?.display_turn ?? null,
|
|
jsonMetadata,
|
|
yamlMetadata,
|
|
prettyMetadata,
|
|
};
|
|
}"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["selectedIdx"] >= 0
|
|
assert state["activeDisplayTurn"] == 1
|
|
assert '"display_turn": 1' in state["jsonMetadata"]
|
|
assert '"capture_turn": "2.2"' in state["jsonMetadata"]
|
|
assert "display_turn: 1" in state["yamlMetadata"]
|
|
assert "capture_turn: 2.2" in state["yamlMetadata"]
|
|
assert "display_turn" in state["prettyMetadata"]
|
|
assert "capture_turn" in state["prettyMetadata"]
|
|
|
|
|
|
def test_viewer_session_group_hover_shows_full_truncated_user_input(tmp_path: Path, chromium_browser) -> None:
|
|
long_prompt = (
|
|
"Investigate why the dashboard session group title is truncated, then preserve this full original "
|
|
"user input in a hover tooltip so maintainers can read the complete request without opening the turn."
|
|
)
|
|
record = {
|
|
"request_id": "req_long_session_prompt",
|
|
"turn": 1,
|
|
"timestamp": "2026-05-13T13:21:00+00:00",
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": {},
|
|
"body": {
|
|
"model": "aws.claude-sonnet-4.6",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "<system-reminder>\nskip injected context\n</system-reminder>"},
|
|
{
|
|
"type": "text",
|
|
"text": "<local-command-caveat>\nskip local command context\n</local-command-caveat>",
|
|
},
|
|
{"type": "text", "text": long_prompt},
|
|
{"type": "text", "text": "[Image: source: /tmp/screenshot.png]"},
|
|
],
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {"content": [{"type": "text", "text": "OK"}]},
|
|
},
|
|
}
|
|
injected_continuation = json.loads(json.dumps(record))
|
|
injected_continuation["request_id"] = "req_injected_context_only"
|
|
injected_continuation["turn"] = 2
|
|
injected_continuation["request"]["body"]["messages"] = [
|
|
{"role": "user", "content": "<system-reminder>\nThe following skills are available...\n</system-reminder>"}
|
|
]
|
|
json_title_record = json.loads(json.dumps(record))
|
|
json_title_record["request_id"] = "req_json_title"
|
|
json_title_record["turn"] = 3
|
|
json_title_record["request"]["body"]["messages"] = [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": '{"title":"Fix login button on mobile"}'},
|
|
{"type": "text", "text": "<system-reminder>\nInjected context\n</system-reminder>"},
|
|
],
|
|
}
|
|
]
|
|
html_path = _generate_case_html(
|
|
tmp_path,
|
|
"session_hover_tooltip",
|
|
(record, injected_continuation, json_title_record),
|
|
)
|
|
|
|
page = chromium_browser.new_page()
|
|
page.add_init_script("localStorage.setItem('claude-tap-sidebar-order', 'session')")
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
header = page.locator(".sidebar-group-header").nth(0)
|
|
assert header.locator(".group-name").inner_text().endswith("...")
|
|
assert "<system-reminder>" not in header.locator(".group-name").inner_text()
|
|
assert header.locator(".group-count").inner_text() == "2"
|
|
json_title_header = page.locator(".sidebar-group-header").nth(1)
|
|
json_title_name = json_title_header.locator(".group-name").inner_text()
|
|
assert "fix login button on mobile" in json_title_name.lower()
|
|
assert "{" not in json_title_name
|
|
|
|
header.hover()
|
|
page.wait_for_selector(".session-hover-tooltip.visible", timeout=5000)
|
|
tooltip_text = page.locator(".session-hover-tooltip.visible").inner_text()
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert tooltip_text == long_prompt
|
|
|
|
|
|
def test_viewer_recovers_session_title_image_placeholders(tmp_path: Path, chromium_browser) -> None:
|
|
prompt = "My local TNTCloud nodes all time out, but the same account works on another computer."
|
|
image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
|
|
title_record = {
|
|
"timestamp": "2026-05-13T13:21:00+00:00",
|
|
"request_id": "req_title_image_placeholder",
|
|
"turn": 1,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": {},
|
|
"body": {
|
|
"model": "aws.claude-sonnet-4.6",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": f"<session>\n[Image #1] {prompt}\n</session>"}],
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {"content": [{"type": "text", "text": '{"title":"TNTCloud timeout"}'}]},
|
|
},
|
|
}
|
|
actual_record = json.loads(json.dumps(title_record))
|
|
actual_record["request_id"] = "req_actual_image"
|
|
actual_record["turn"] = 2
|
|
actual_record["request"]["body"]["messages"] = [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": f"[Image #1] {prompt}"},
|
|
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_data}},
|
|
],
|
|
}
|
|
]
|
|
|
|
html_path = _generate_case_html(tmp_path, "image-placeholder-recovery", (title_record, actual_record))
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
page.locator(".sidebar-item").nth(0).click()
|
|
page.wait_for_selector("#detail .msg.user img.message-image", timeout=5000)
|
|
result = page.evaluate(
|
|
"""() => ({
|
|
imageCount: document.querySelectorAll('#detail .msg.user img.message-image').length,
|
|
imageSrc: document.querySelector('#detail .msg.user img.message-image')?.getAttribute('src') || '',
|
|
text: document.querySelector('#detail .msg.user')?.textContent || ''
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert result["imageCount"] == 1
|
|
assert result["imageSrc"].startswith("data:image/png;base64,")
|
|
assert "[Image #1]" in result["text"]
|
|
|
|
|
|
def test_viewer_runtime_smoke_handles_degenerate_records_without_js_errors(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "runtime_smoke", _runtime_smoke_records())
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
sidebar_count = page.locator(".sidebar-item").count()
|
|
for index in range(sidebar_count):
|
|
page.locator(".sidebar-item").nth(index).click()
|
|
page.wait_for_selector("#detail .section", timeout=5000)
|
|
assert "Full JSON" in page.locator("#detail").inner_text()
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert sidebar_count == len(_runtime_smoke_records())
|
|
|
|
|
|
def test_viewer_empty_embedded_trace_renders_explicit_no_api_calls_state(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "empty_trace", ())
|
|
|
|
page = chromium_browser.new_page()
|
|
errors: list[str] = []
|
|
page.on("pageerror", lambda exc: errors.append(f"pageerror: {exc}"))
|
|
page.on("console", lambda msg: errors.append(f"console.error: {msg.text}") if msg.type == "error" else None)
|
|
try:
|
|
page.goto(html_path.resolve().as_uri(), timeout=10000)
|
|
page.wait_for_selector(".empty-trace-state", timeout=5000)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
title: document.querySelector('#empty-trace-title')?.textContent || '',
|
|
description: document.querySelector('#empty-trace-desc')?.textContent || '',
|
|
count: document.querySelector('#empty-trace-count')?.textContent || '',
|
|
hint: document.querySelector('#empty-trace-hint')?.textContent || '',
|
|
sidebarItemCount: document.querySelectorAll('.sidebar-item').length,
|
|
sidebarDisplay: getComputedStyle(document.querySelector('#sidebar-wrap')).display,
|
|
detailDisplay: getComputedStyle(document.querySelector('#detail')).display,
|
|
pathText: document.querySelector('#trace-path-bar')?.innerText || '',
|
|
oldDropTitlePresent: Boolean(document.querySelector('#drop-title')),
|
|
fileInputPresent: Boolean(document.querySelector('#file-input')),
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["title"] == "No API calls captured"
|
|
assert "generated" in state["description"]
|
|
assert state["count"] == "Captured API calls: 0"
|
|
assert "real empty run" in state["hint"]
|
|
assert state["sidebarItemCount"] == 0
|
|
assert state["sidebarDisplay"] == "none"
|
|
assert state["detailDisplay"] == "none"
|
|
assert "empty_trace.jsonl" in state["pathText"]
|
|
assert "empty_trace.html" in state["pathText"]
|
|
assert state["oldDropTitlePresent"] is False
|
|
assert state["fileInputPresent"] is True
|
|
|
|
|
|
def test_viewer_trace_path_copy_handles_apostrophes(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "user's_trace", (_anthropic_messages_record(),))
|
|
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
copy_button = page.locator("#trace-path-bar .tp-copy").first
|
|
assert copy_button.get_attribute("onclick") is None
|
|
assert "user's_trace.jsonl" in (copy_button.get_attribute("data-copy-path") or "")
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
|
|
|
|
def test_viewer_iframe_embed_query_hides_chrome_and_keeps_trace_loaded(tmp_path: Path, chromium_browser) -> None:
|
|
html_path = _generate_case_html(tmp_path, "embed_trace", (_anthropic_messages_record(),))
|
|
query = "embed=1&hideHeader=1&hidePath=1&hideHistory=1&hideControls=1&density=compact&theme=light"
|
|
|
|
page = chromium_browser.new_page()
|
|
errors: list[str] = []
|
|
page.on("pageerror", lambda exc: errors.append(f"pageerror: {exc}"))
|
|
page.on("console", lambda msg: errors.append(f"console.error: {msg.text}") if msg.type == "error" else None)
|
|
try:
|
|
page.goto(
|
|
f"{html_path.resolve().as_uri()}?{query}",
|
|
timeout=10000,
|
|
)
|
|
page.wait_for_selector(".sidebar-item", timeout=5000)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
embedMode: document.documentElement.dataset.embedMode || '',
|
|
bodyClasses: Array.from(document.body.classList),
|
|
headerDisplay: getComputedStyle(document.querySelector('.header')).display,
|
|
pathDisplay: getComputedStyle(document.querySelector('#trace-path-bar')).display,
|
|
themeToggleDisplay: getComputedStyle(document.querySelector('#theme-toggle')).display,
|
|
langSelectDisplay: getComputedStyle(document.querySelector('#lang-select')).display,
|
|
searchBarDisplay: getComputedStyle(document.querySelector('#search-bar')).display,
|
|
sidebarSortDisplay: getComputedStyle(document.querySelector('#sidebar-sort')).display,
|
|
detailTabsDisplay: getComputedStyle(document.querySelector('.detail-inspector-bar')).display,
|
|
actionBarDisplay: getComputedStyle(document.querySelector('.action-bar')).display,
|
|
sidebarWidth: Math.round(document.querySelector('#sidebar-wrap').getBoundingClientRect().width),
|
|
theme: document.documentElement.dataset.theme || 'light',
|
|
sidebarItemCount: document.querySelectorAll('.sidebar-item').length,
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["embedMode"] == "true"
|
|
assert "embed-mode" in state["bodyClasses"]
|
|
assert "embed-compact" in state["bodyClasses"]
|
|
assert state["headerDisplay"] == "none"
|
|
assert state["pathDisplay"] == "none"
|
|
assert state["themeToggleDisplay"] == "none"
|
|
assert state["langSelectDisplay"] == "none"
|
|
assert state["searchBarDisplay"] == "none"
|
|
assert state["sidebarSortDisplay"] == "none"
|
|
assert state["detailTabsDisplay"] == "none"
|
|
assert state["actionBarDisplay"] == "none"
|
|
assert state["sidebarWidth"] <= 280
|
|
assert state["theme"] == "light"
|
|
assert state["sidebarItemCount"] == 1
|
|
|
|
sandbox_page = chromium_browser.new_page()
|
|
sandbox_errors: list[str] = []
|
|
sandbox_page.on("pageerror", lambda exc: sandbox_errors.append(f"pageerror: {exc}"))
|
|
sandbox_page.on(
|
|
"console",
|
|
lambda msg: sandbox_errors.append(f"console.error: {msg.text}") if msg.type == "error" else None,
|
|
)
|
|
harness_path = tmp_path / "embed_harness.html"
|
|
harness_path.write_text(
|
|
f'<iframe sandbox="allow-scripts" src="{html_path.resolve().as_uri()}?{query}" '
|
|
'style="width:1200px;height:800px"></iframe>',
|
|
encoding="utf-8",
|
|
)
|
|
try:
|
|
sandbox_page.goto(harness_path.resolve().as_uri(), timeout=10000)
|
|
frame = sandbox_page.frame_locator("iframe")
|
|
frame.locator(".sidebar-item").first.wait_for(state="attached", timeout=5000)
|
|
sandbox_state = frame.locator("body").evaluate(
|
|
"""() => ({
|
|
embedMode: document.documentElement.dataset.embedMode || '',
|
|
headerDisplay: getComputedStyle(document.querySelector('.header')).display,
|
|
pathDisplay: getComputedStyle(document.querySelector('#trace-path-bar')).display,
|
|
sidebarItemCount: document.querySelectorAll('.sidebar-item').length,
|
|
dropZoneDisplay: getComputedStyle(document.querySelector('#drop-zone')).display,
|
|
})"""
|
|
)
|
|
finally:
|
|
sandbox_page.close()
|
|
|
|
assert sandbox_errors == []
|
|
assert sandbox_state["embedMode"] == "true"
|
|
assert sandbox_state["headerDisplay"] == "none"
|
|
assert sandbox_state["pathDisplay"] == "none"
|
|
assert sandbox_state["sidebarItemCount"] == 1
|
|
assert sandbox_state["dropZoneDisplay"] == "none"
|
|
|
|
|
|
def test_viewer_v8_coverage_exercises_core_inline_js_functions(tmp_path: Path, chromium_browser) -> None:
|
|
records = tuple(record for case in _contract_cases() for record in case.records)
|
|
html_path = _generate_case_html(tmp_path, "v8_coverage", records)
|
|
required_functions = {
|
|
"renderDetail",
|
|
"extractSystem",
|
|
"getMessages",
|
|
"getRequestTools",
|
|
"getUsage",
|
|
"getResponseEvents",
|
|
"getResponseOutput",
|
|
"geminiMessages",
|
|
"geminiResponseOutput",
|
|
"renderTools",
|
|
"renderImageBlock",
|
|
"sessionTurnDiscriminator",
|
|
"showSessionTooltip",
|
|
}
|
|
|
|
page = chromium_browser.new_page()
|
|
page.add_init_script(
|
|
"window.__TRACE_SESSION_EXPORTS__ = {compact: 'coverage.json', log: 'coverage.log', html: 'coverage.html'};"
|
|
)
|
|
try:
|
|
session = page.context.new_cdp_session(page)
|
|
session.send("Profiler.enable")
|
|
session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True})
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
|
|
export_items = page.locator("#viewer-actions .export-menu-item")
|
|
assert export_items.count() == 2
|
|
assert export_items.all_text_contents() == ["Export JSON", "Export HTML"]
|
|
assert page.locator('#viewer-actions a[href="coverage.log"]').count() == 0
|
|
|
|
entry_count = page.evaluate("entries.length")
|
|
for index in range(entry_count):
|
|
page.evaluate("entryIndex => renderDetail(entries[entryIndex])", index)
|
|
page.wait_for_selector("#detail .section", timeout=5000)
|
|
page.evaluate(
|
|
"""(entryIndex) => {
|
|
const entry = entries[entryIndex];
|
|
const body = entry.request.body;
|
|
getMessages(body);
|
|
getRequestTools(body);
|
|
extractSystem(body);
|
|
getUsage(entry);
|
|
getResponseEvents(entry);
|
|
getResponseOutput(entry);
|
|
const jsonSection = Array.from(document.querySelectorAll('#detail .section'))
|
|
.find(el => el.querySelector('.title')?.textContent === t('section_json'));
|
|
const jsonToggle = jsonSection?.querySelector('.jt-toggle');
|
|
if (jsonToggle) {
|
|
jsonToggle.click();
|
|
jsonToggle.click();
|
|
}
|
|
}""",
|
|
index,
|
|
)
|
|
|
|
page.evaluate(
|
|
"""() => {
|
|
const imageBlock = {
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: 'image/png',
|
|
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
|
|
}
|
|
};
|
|
imageLookupKey('<session>[Image #1] coverage prompt</session>');
|
|
isInlineImageUrl('data:image/png;base64,abc');
|
|
imageSourceFromBlock(imageBlock);
|
|
imageBlocksForContent([{ type: 'text', text: '[Image #1] coverage prompt' }, imageBlock]);
|
|
imageSourceKey(imageBlock);
|
|
buildSessionImageRegistry();
|
|
naturalTextFromPromptPayload({ prompt: 'coverage prompt' });
|
|
if (entries.length) {
|
|
sessionTurnDiscriminator(entries[0]);
|
|
sessionKeyForEntry(entries[0], null);
|
|
matchSearch(entries[0], '1');
|
|
const originalPrompt = window.prompt;
|
|
window.prompt = () => '1';
|
|
promptJumpToTurn();
|
|
window.prompt = originalPrompt;
|
|
_buildDiffTargetOptions(Math.min(1, filtered.length - 1));
|
|
if (filtered.length > 1) showDiffForIdx(1, null, 0);
|
|
}
|
|
const stubEntry = buildStubEntry({
|
|
turn: '2.2',
|
|
transport: 'websocket',
|
|
method: 'WEBSOCKET',
|
|
path: '/v1/responses',
|
|
model: 'gpt-5.5',
|
|
request_generate: true,
|
|
response_output_count: 1,
|
|
output_tokens: 1,
|
|
}, 0);
|
|
normalizeDisplayTurns([stubEntry], true);
|
|
renderImageElement('data:image/png;base64,abc', 'coverage image');
|
|
renderImageElementForBlock(imageBlock);
|
|
document.body.insertAdjacentHTML('beforeend', renderImageBlock(imageBlock, 0, 1, { frameBlocks: true }));
|
|
renderViewerActions();
|
|
valueHasReadableEscapes({ cmd: 'printf "coverage\\\\n"' });
|
|
decodeEscapedTextForView('line1\\\\nline2\\\\t\\\\u4e00');
|
|
document.body.insertAdjacentHTML(
|
|
'beforeend',
|
|
renderToolInput({ cmd: 'printf "coverage\\n"', yield_time_ms: 1000 })
|
|
);
|
|
document.querySelector('.tool-input-toggle')?.click();
|
|
const tooltipTrigger = document.querySelector('.sidebar-group-header') || document.createElement('div');
|
|
if (!tooltipTrigger.isConnected) document.body.appendChild(tooltipTrigger);
|
|
tooltipTrigger.dataset.fullUserInput = 'coverage tooltip prompt';
|
|
sessionTooltip();
|
|
showSessionTooltip(tooltipTrigger);
|
|
hideSessionTooltip(tooltipTrigger);
|
|
}"""
|
|
)
|
|
|
|
coverage = session.send("Profiler.takePreciseCoverage")
|
|
session.send("Profiler.stopPreciseCoverage")
|
|
session.send("Profiler.disable")
|
|
finally:
|
|
page.close()
|
|
|
|
covered_names: set[str] = set()
|
|
used_main_script_bytes = 0
|
|
for script in coverage["result"]:
|
|
if not script.get("url", "").endswith("v8_coverage.html"):
|
|
continue
|
|
functions = script.get("functions", [])
|
|
if len(functions) < 50:
|
|
continue
|
|
for function in functions:
|
|
ranges = function.get("ranges", [])
|
|
if any(item.get("count", 0) > 0 for item in ranges):
|
|
name = function.get("functionName")
|
|
if name:
|
|
covered_names.add(name)
|
|
used_main_script_bytes += sum(
|
|
item.get("endOffset", 0) - item.get("startOffset", 0) for item in ranges if item.get("count", 0) > 0
|
|
)
|
|
|
|
assert errors == []
|
|
assert required_functions <= covered_names
|
|
assert used_main_script_bytes > 50_000
|
|
|
|
|
|
def test_viewer_visual_layout_contracts_cover_css_modes(tmp_path: Path, chromium_browser) -> None:
|
|
records = tuple(record for case in _contract_cases() for record in case.records)
|
|
html_path = _generate_case_html(tmp_path, "visual_contract", records)
|
|
|
|
page = chromium_browser.new_page(viewport={"width": 1440, "height": 1000})
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
page.evaluate(
|
|
"requestId => renderDetail(entries.find(entry => entry.request_id === requestId))", "req_gemini_contract"
|
|
)
|
|
page.evaluate(
|
|
"""() => {
|
|
const toolsSection = Array.from(document.querySelectorAll('#detail .section'))
|
|
.find(section => section.querySelector('.title')?.textContent === 'Tools');
|
|
if (toolsSection && !toolsSection.querySelector('.section-body')?.classList.contains('open')) {
|
|
toolsSection.querySelector('.section-header').click();
|
|
}
|
|
}"""
|
|
)
|
|
|
|
def snapshot(width: int, height: int, theme: str, mobile: bool = False) -> dict:
|
|
page.set_viewport_size({"width": width, "height": height})
|
|
page.evaluate("theme => document.documentElement.setAttribute('data-theme', theme)", theme)
|
|
if mobile:
|
|
page.evaluate("mobileShowDetail()")
|
|
return page.evaluate(
|
|
"""() => {
|
|
const rect = selector => {
|
|
const el = document.querySelector(selector);
|
|
if (!el) return null;
|
|
const r = el.getBoundingClientRect();
|
|
return { width: r.width, height: r.height, left: r.left, right: r.right, top: r.top, bottom: r.bottom };
|
|
};
|
|
const color = selector => getComputedStyle(document.querySelector(selector)).backgroundColor;
|
|
return {
|
|
overflowX: document.documentElement.scrollWidth - window.innerWidth,
|
|
bodyBg: getComputedStyle(document.body).backgroundColor,
|
|
userMsgBg: color('.msg.user'),
|
|
sidebar: rect('#sidebar-wrap'),
|
|
detail: rect('#detail'),
|
|
sectionHeader: rect('.section-header'),
|
|
tokenBar: rect('.token-bar'),
|
|
message: rect('.msg'),
|
|
toolBlock: rect('.tool-block'),
|
|
response: rect('.section-body.open .content-block'),
|
|
sectionCount: document.querySelectorAll('#detail .section').length,
|
|
};
|
|
}"""
|
|
)
|
|
|
|
desktop_light = snapshot(1440, 1000, "light")
|
|
desktop_dark = snapshot(1440, 1000, "dark")
|
|
page.evaluate(
|
|
"requestId => renderDetail(entries.find(entry => entry.request_id === requestId))",
|
|
"req_content_block_boundary_contract",
|
|
)
|
|
dark_content_block = page.evaluate(
|
|
"""() => {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
const block = document.querySelector('.content-block.block-framed');
|
|
if (!block) return null;
|
|
const style = getComputedStyle(block);
|
|
return {
|
|
backgroundColor: style.backgroundColor,
|
|
borderLeftWidth: style.borderLeftWidth,
|
|
};
|
|
}"""
|
|
)
|
|
page.evaluate(
|
|
"requestId => renderDetail(entries.find(entry => entry.request_id === requestId))", "req_gemini_contract"
|
|
)
|
|
mobile_dark = snapshot(390, 900, "dark", mobile=True)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
for result in (desktop_light, desktop_dark, mobile_dark):
|
|
assert result["overflowX"] <= 2
|
|
assert result["sectionCount"] >= 5
|
|
assert result["sectionHeader"]["height"] >= 28
|
|
assert result["tokenBar"]["width"] > 250
|
|
assert result["message"]["height"] > 40
|
|
assert result["toolBlock"]["width"] > 250
|
|
assert result["response"]["height"] > 20
|
|
|
|
assert desktop_light["sidebar"]["width"] >= 280
|
|
assert desktop_light["detail"]["width"] >= 1000
|
|
assert desktop_dark["sidebar"]["width"] >= 280
|
|
assert desktop_dark["detail"]["width"] >= 1000
|
|
assert desktop_light["bodyBg"] != desktop_dark["bodyBg"]
|
|
assert desktop_light["userMsgBg"] != desktop_dark["userMsgBg"]
|
|
assert dark_content_block is not None
|
|
assert dark_content_block["backgroundColor"] != "rgba(0, 0, 0, 0)"
|
|
assert dark_content_block["borderLeftWidth"] == "1px"
|
|
|
|
assert mobile_dark["sidebar"]["width"] == 0
|
|
assert mobile_dark["detail"]["width"] == 390
|
|
assert mobile_dark["detail"]["left"] == 0
|
|
|
|
|
|
def test_viewer_session_identical_prompts_image_tags_and_early_title_generation(
|
|
tmp_path: Path, chromium_browser
|
|
) -> None:
|
|
model = "aws.claude-opus-4.6"
|
|
title_system = 'Generate a concise, sentence-case title for the session. Return JSON with a single "title" field.'
|
|
|
|
def make_record(
|
|
request_id: str,
|
|
turn: int,
|
|
messages: list[dict[str, Any]],
|
|
response_content: list[dict[str, Any]],
|
|
*,
|
|
system: str | None = None,
|
|
) -> dict[str, Any]:
|
|
body: dict[str, Any] = {"model": model, "messages": messages}
|
|
if system is not None:
|
|
body["system"] = system
|
|
return {
|
|
"request_id": request_id,
|
|
"turn": turn,
|
|
"timestamp": f"2026-05-13T13:{20 + turn:02d}:00+00:00",
|
|
"duration_ms": 100,
|
|
"request": {"method": "POST", "path": "/v1/messages", "headers": {}, "body": body},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {"stop_reason": "end_turn", "content": response_content},
|
|
},
|
|
}
|
|
|
|
records = (
|
|
# Turn 1
|
|
make_record(
|
|
"req_t1",
|
|
1,
|
|
[{"role": "user", "content": "继续"}],
|
|
[{"type": "text", "text": "Turn 1 done"}],
|
|
),
|
|
# Turn 1 title-gen
|
|
make_record(
|
|
"req_t1_title",
|
|
2,
|
|
[{"role": "user", "content": "继续"}],
|
|
[{"type": "text", "text": '{"title": "Group1"}'}],
|
|
system=title_system,
|
|
),
|
|
# Turn 2 title-gen arrives before the real request with the same prompt.
|
|
make_record(
|
|
"req_t2_title",
|
|
3,
|
|
[{"role": "user", "content": "继续"}],
|
|
[{"type": "text", "text": '{"title": "Group2"}'}],
|
|
system=title_system,
|
|
),
|
|
# Turn 2 (identical prompt)
|
|
make_record(
|
|
"req_t2",
|
|
4,
|
|
[{"role": "user", "content": "继续"}],
|
|
[{"type": "text", "text": "Turn 2 done"}],
|
|
),
|
|
# Turn 3 (image wrappers)
|
|
make_record(
|
|
"req_t3",
|
|
5,
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "<image name=[Image #1]>"},
|
|
{
|
|
"type": "image",
|
|
"source": {
|
|
"type": "base64",
|
|
"media_type": "image/png",
|
|
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=",
|
|
},
|
|
},
|
|
{"type": "text", "text": "</image>"},
|
|
{"type": "text", "text": "Analyze the flowchart"},
|
|
],
|
|
}
|
|
],
|
|
[{"type": "text", "text": "Turn 3 done"}],
|
|
),
|
|
)
|
|
|
|
html_path = _generate_case_html(tmp_path, "identical_prompts_and_image_tags", records)
|
|
page = chromium_browser.new_page()
|
|
page.add_init_script("localStorage.setItem('claude-tap-sidebar-order', 'session')")
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
state = page.evaluate(
|
|
"""() => ({
|
|
groups: Array.from(document.querySelectorAll('.sidebar-group-header .group-name')).map(el => el.textContent),
|
|
counts: Array.from(document.querySelectorAll('.sidebar-group-header .group-count')).map(el => el.textContent),
|
|
})"""
|
|
)
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert state["groups"] == [
|
|
"Query 1 - 继续",
|
|
"Query 2 - 继续",
|
|
"Query 3 - Analyze the flowchart",
|
|
]
|
|
assert state["counts"] == ["2", "2", "1"]
|
|
|
|
|
|
def test_viewer_codex_global_search_skips_non_navigable_and_orders_by_capture_turn(
|
|
tmp_path: Path, chromium_browser
|
|
) -> None:
|
|
records = (
|
|
{
|
|
"timestamp": "2026-06-01T00:57:34.000Z",
|
|
"request_id": "req_models",
|
|
"turn": 1,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "GET",
|
|
"path": "/v1/models",
|
|
"headers": {},
|
|
"body": None,
|
|
},
|
|
"response": {"status": 200, "headers": {}, "body": {"data": []}},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:57:40.000Z",
|
|
"request_id": "req_prefetch",
|
|
"turn": 2,
|
|
"duration_ms": 100,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"type": "response.create",
|
|
"model": "gpt-hidden-model",
|
|
"generate": False,
|
|
"input": [],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_prefetch",
|
|
"model": "gpt-hidden-model",
|
|
"generate": False,
|
|
"output": [],
|
|
"usage": {"input_tokens": 10, "output_tokens": 0},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:58:53.000Z",
|
|
"request_id": "req_response_2",
|
|
"turn": 2,
|
|
"duration_ms": 100,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"type": "response.create",
|
|
"model": "gpt-5.5",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": "hello",
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_response_2",
|
|
"model": "gpt-5.5",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": "hi",
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 10, "output_tokens": 10},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:59:00.000Z",
|
|
"request_id": "req_mcp_between",
|
|
"turn": 3,
|
|
"duration_ms": 100,
|
|
"request": {
|
|
"method": "POST",
|
|
"path": "/v1/mcp/list",
|
|
"headers": {},
|
|
"body": {"query": "mcp-query"},
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {},
|
|
"body": {"tools": []},
|
|
},
|
|
},
|
|
{
|
|
"timestamp": "2026-06-01T00:59:02.000Z",
|
|
"request_id": "req_response_4",
|
|
"turn": 4,
|
|
"duration_ms": 100,
|
|
"transport": "websocket",
|
|
"request": {
|
|
"method": "WEBSOCKET",
|
|
"path": "/v1/responses",
|
|
"headers": {},
|
|
"body": {
|
|
"type": "response.create",
|
|
"model": "gpt-5.5",
|
|
"previous_response_id": "resp_response_2",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": "then what",
|
|
}
|
|
],
|
|
},
|
|
},
|
|
"response": {
|
|
"status": 101,
|
|
"headers": {},
|
|
"body": {
|
|
"id": "resp_response_4",
|
|
"model": "gpt-5.5",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": "that is it",
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 10, "output_tokens": 10},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
html_path = _generate_case_html(tmp_path, "codex_search_sort", records)
|
|
page = chromium_browser.new_page()
|
|
try:
|
|
errors = _open_viewer_with_error_capture(page, html_path)
|
|
# Verify global search skips the hidden model
|
|
page.evaluate("() => openGlobalSearch()")
|
|
page.evaluate("() => { $('#global-search-input').value = 'gpt-hidden-model'; recalcGlobalSearchMatches(); }")
|
|
search_state = page.evaluate("() => ({ totalMatches: globalSearchState.totalMatches })")
|
|
|
|
# Expand paths and check sorting (should be chronological: turn 2 -> 3 -> 4)
|
|
page.evaluate("() => { closeGlobalSearch(); activePaths.add('/v1/mcp/list'); applyFilter(); }")
|
|
debug_state = page.evaluate(
|
|
"""() => ({
|
|
entries: entries.map(e => ({
|
|
request_id: e.request_id,
|
|
turn: e.turn,
|
|
capture_turn: e.capture_turn,
|
|
display_turn: e.display_turn,
|
|
captureTurnValue: captureTurnValue(e),
|
|
displayTurnValue: displayTurnValue(e),
|
|
isNavigable: isNavigableTraceEntry(e),
|
|
})),
|
|
filtered: filtered.map(e => e.request_id),
|
|
})"""
|
|
)
|
|
sorted_ids = [rid for rid in debug_state["filtered"] if rid != "req_models"]
|
|
finally:
|
|
page.close()
|
|
|
|
assert errors == []
|
|
assert search_state["totalMatches"] == 0
|
|
assert sorted_ids == ["req_response_2", "req_mcp_between", "req_response_4"]
|