| #!/usr/bin/env bash |
| set -euo pipefail |
|
|
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" |
| DESKTOP_DIR="$ROOT_DIR/desktop" |
|
|
| if ! command -v agent-browser >/dev/null 2>&1; then |
| echo "agent-browser is required but was not found in PATH" >&2 |
| exit 1 |
| fi |
|
|
| PORTS="$(bun -e 'import { createServer } from "node:net"; const getPort = () => new Promise((resolve) => { const server = createServer(); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; server.close(() => resolve(port)); }); }); const apiPort = await getPort(); const webPort = await getPort(); console.log(`${apiPort} ${webPort}`);')" |
| API_PORT="${PORTS%% *}" |
| WEB_PORT="${PORTS##* }" |
| BASE_URL="http://127.0.0.1:${API_PORT}" |
| WEB_URL="http://127.0.0.1:${WEB_PORT}/?serverUrl=${BASE_URL}" |
|
|
| RUN_ID="$(date +%s)-$RANDOM" |
| SESSION_NAME="cc-haha-context-live-${RUN_ID}" |
| ARTIFACT_DIR="${ARTIFACT_DIR:-$(mktemp -d "/tmp/cc-haha-context-live-${RUN_ID}-XXXX")}" |
| PROJECT_DIR="${ARTIFACT_DIR}/project" |
| SERVER_LOG="${ARTIFACT_DIR}/server.log" |
| WEB_LOG="${ARTIFACT_DIR}/web.log" |
| BROWSER_LOG="${ARTIFACT_DIR}/browser.log" |
| RESULT_FILE="${PROJECT_DIR}/context-live-result.txt" |
|
|
| cleanup() { |
| local exit_code=$? |
| if [[ -n "${PREVIOUS_PERMISSION_MODE:-}" ]]; then |
| curl -fsS -X PUT "${BASE_URL}/api/permissions/mode" \ |
| -H 'Content-Type: application/json' \ |
| -d "{\"mode\":\"${PREVIOUS_PERMISSION_MODE}\"}" >/dev/null 2>&1 || true |
| fi |
| if [[ -n "${SESSION_ID:-}" && "${KEEP_SESSION:-0}" != "1" ]]; then |
| curl -fsS -X DELETE "${BASE_URL}/api/sessions/${SESSION_ID}" >/dev/null 2>&1 || true |
| fi |
| agent-browser --session "${SESSION_NAME}" close >/dev/null 2>&1 || true |
| if [[ -n "${SERVER_PID:-}" ]]; then |
| kill "${SERVER_PID}" >/dev/null 2>&1 || true |
| wait "${SERVER_PID}" >/dev/null 2>&1 || true |
| fi |
| if [[ -n "${WEB_PID:-}" ]]; then |
| kill "${WEB_PID}" >/dev/null 2>&1 || true |
| wait "${WEB_PID}" >/dev/null 2>&1 || true |
| fi |
| if [[ $exit_code -ne 0 || "${KEEP_ARTIFACTS:-0}" == "1" ]]; then |
| echo "Artifacts kept at: ${ARTIFACT_DIR}" >&2 |
| else |
| rm -rf "${ARTIFACT_DIR}" |
| fi |
| } |
| trap cleanup EXIT |
|
|
| mkdir -p "${PROJECT_DIR}" |
| cat > "${PROJECT_DIR}/README.md" <<'EOF' |
| |
|
|
| This throwaway project is used by a real-model desktop WebUI test. |
| EOF |
|
|
| echo "Starting backend on ${BASE_URL}" |
| ( |
| cd "${ROOT_DIR}" |
| SERVER_PORT="${API_PORT}" bun run src/server/index.ts --host 127.0.0.1 --port "${API_PORT}" |
| ) >"${SERVER_LOG}" 2>&1 & |
| SERVER_PID=$! |
|
|
| echo "Starting web UI on http://127.0.0.1:${WEB_PORT}" |
| ( |
| cd "${DESKTOP_DIR}" |
| bun run dev -- --host 127.0.0.1 --port "${WEB_PORT}" --strictPort |
| ) >"${WEB_LOG}" 2>&1 & |
| WEB_PID=$! |
|
|
| wait_for_http() { |
| local url="$1" |
| for _ in $(seq 1 120); do |
| if curl --max-time 2 -fsS "$url" >/dev/null 2>&1; then |
| return 0 |
| fi |
| sleep 0.5 |
| done |
| echo "Timed out waiting for ${url}" >&2 |
| return 1 |
| } |
|
|
| fetch_json_field() { |
| local url="$1" |
| local expression="$2" |
| curl -fsS "$url" | bun -e "const input = await Bun.stdin.text(); const data = JSON.parse(input); ${expression}" |
| } |
|
|
| AB=(agent-browser --session "${SESSION_NAME}") |
|
|
| browser_text() { |
| "${AB[@]}" get text body 2>>"${BROWSER_LOG}" |
| } |
|
|
| wait_for_text() { |
| local needle="$1" |
| local attempts="${2:-120}" |
| for _ in $(seq 1 "${attempts}"); do |
| if browser_text | grep -Fq "$needle"; then |
| return 0 |
| fi |
| sleep 1 |
| done |
| echo "Timed out waiting for page text: ${needle}" >&2 |
| return 1 |
| } |
|
|
| session_contains() { |
| local needle="$1" |
| curl -fsS "${BASE_URL}/api/sessions/${SESSION_ID}/messages" | NEEDLE="${needle}" bun -e ' |
| const input = await Bun.stdin.text() |
| const data = JSON.parse(input) |
| const haystack = JSON.stringify(data) |
| process.exit(haystack.includes(process.env.NEEDLE ?? "") ? 0 : 1) |
| ' |
| } |
|
|
| wait_for_session_text() { |
| local needle="$1" |
| local attempts="${2:-180}" |
| for _ in $(seq 1 "${attempts}"); do |
| if session_contains "${needle}"; then |
| return 0 |
| fi |
| sleep 1 |
| done |
| echo "Timed out waiting for transcript text: ${needle}" >&2 |
| return 1 |
| } |
|
|
| context_label() { |
| "${AB[@]}" eval "document.querySelector('[aria-label^=\"Context usage\"], [aria-label^=\"上下文用量\"]')?.getAttribute('aria-label') || ''" 2>>"${BROWSER_LOG}" || true |
| } |
|
|
| wait_for_context_indicator() { |
| for _ in $(seq 1 120); do |
| if context_label | grep -Fq "Context usage"; then |
| return 0 |
| fi |
| sleep 1 |
| done |
| echo "Timed out waiting for context usage indicator" >&2 |
| return 1 |
| } |
|
|
| show_context_details() { |
| "${AB[@]}" eval "(() => { const el = document.querySelector('[aria-label^=\"Context usage\"], [aria-label^=\"上下文用量\"]'); if (el) { el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); el.focus(); } })()" >>"${BROWSER_LOG}" 2>&1 || true |
| } |
|
|
| wait_for_context_window_text() { |
| for _ in $(seq 1 80); do |
| show_context_details |
| if browser_text | grep -Eq "Window|Input context|Free space|上下文|空闲"; then |
| return 0 |
| fi |
| sleep 0.5 |
| done |
| echo "Context usage detail popover text was not detected; continuing with indicator and screenshot evidence" >&2 |
| return 0 |
| } |
|
|
| wait_for_result_file() { |
| for _ in $(seq 1 240); do |
| if [[ -f "${RESULT_FILE}" ]] && grep -Fq "context-live-ok" "${RESULT_FILE}"; then |
| return 0 |
| fi |
| sleep 2 |
| done |
| echo "Timed out waiting for real model tool execution" >&2 |
| return 1 |
| } |
|
|
| wait_for_compact_boundary() { |
| for _ in $(seq 1 240); do |
| if session_contains "compact_boundary" || session_contains "Context compacted" || session_contains "Compacted"; then |
| return 0 |
| fi |
| sleep 2 |
| done |
| echo "Timed out waiting for real /compact boundary" >&2 |
| return 1 |
| } |
|
|
| wait_for_http "${BASE_URL}/health" |
| wait_for_http "http://127.0.0.1:${WEB_PORT}" |
|
|
| PREVIOUS_PERMISSION_MODE="$(fetch_json_field "${BASE_URL}/api/permissions/mode" 'console.log(data.mode)')" |
| curl -fsS -X PUT "${BASE_URL}/api/permissions/mode" \ |
| -H 'Content-Type: application/json' \ |
| -d '{"mode":"bypassPermissions"}' >/dev/null |
|
|
| SESSION_ID="$(curl -fsS -X POST "${BASE_URL}/api/sessions" \ |
| -H 'Content-Type: application/json' \ |
| -d "{\"workDir\":\"${PROJECT_DIR//\"/\\\"}\"}" \ |
| | bun -e 'const input = await Bun.stdin.text(); const data = JSON.parse(input); console.log(data.sessionId);')" |
| UNIQUE_TITLE="Context Live ${RUN_ID}" |
| curl -fsS -X PATCH "${BASE_URL}/api/sessions/${SESSION_ID}" \ |
| -H 'Content-Type: application/json' \ |
| -d "{\"title\":\"${UNIQUE_TITLE//\"/\\\"}\"}" >/dev/null |
|
|
| CURRENT_MODEL="$(fetch_json_field "${BASE_URL}/api/models/current" 'console.log(data.model?.name || data.model?.id || "current")')" |
| TAB_STATE="{\"openTabs\":[{\"sessionId\":\"${SESSION_ID}\",\"title\":\"${UNIQUE_TITLE}\",\"type\":\"session\"}],\"activeTabId\":\"${SESSION_ID}\"}" |
|
|
| "${AB[@]}" open "${WEB_URL}" >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" wait 1200 >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" eval "localStorage.setItem('cc-haha-locale', 'en'); localStorage.setItem('cc-haha-open-tabs', '${TAB_STATE}'); location.reload();" >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" wait 1800 >>"${BROWSER_LOG}" 2>&1 |
|
|
| "${AB[@]}" fill '#sidebar-search' "${UNIQUE_TITLE}" >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" eval "Array.from(document.querySelectorAll('button')).find((el) => el.textContent?.includes('Context Live'))?.click()" >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" wait 1200 >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" wait 'textarea' >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" screenshot "${ARTIFACT_DIR}/01-session-open.png" >/dev/null |
|
|
| wait_for_context_indicator |
| show_context_details |
| wait_for_context_window_text |
| "${AB[@]}" screenshot "${ARTIFACT_DIR}/02-initial-real-context.png" >/dev/null |
|
|
| PROMPT="Use Bash to run exactly: sleep 18 && printf context-live-ok > context-live-result.txt . After the command finishes, reply exactly CONTEXT_LIVE_DONE. Do not edit any other file." |
| "${AB[@]}" fill 'textarea' "${PROMPT}" >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" press 'Enter' >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" wait 3500 >>"${BROWSER_LOG}" 2>&1 |
|
|
| wait_for_context_indicator |
| show_context_details |
| wait_for_context_window_text |
| "${AB[@]}" screenshot "${ARTIFACT_DIR}/03-running-real-model-context.png" >/dev/null |
|
|
| wait_for_result_file |
| wait_for_session_text "CONTEXT_LIVE_DONE" 180 |
| "${AB[@]}" wait 1000 >>"${BROWSER_LOG}" 2>&1 |
| show_context_details |
| "${AB[@]}" screenshot "${ARTIFACT_DIR}/04-after-real-model-turn-context.png" >/dev/null |
|
|
| COMPACT_PROMPT="/compact Keep a concise summary that mentions context-live-ok and the Bash command result." |
| "${AB[@]}" fill 'textarea' "${COMPACT_PROMPT}" >>"${BROWSER_LOG}" 2>&1 |
| "${AB[@]}" press 'Enter' >>"${BROWSER_LOG}" 2>&1 |
| wait_for_compact_boundary |
| show_context_details |
| wait_for_context_window_text |
| "${AB[@]}" screenshot "${ARTIFACT_DIR}/05-after-real-compact-context.png" >/dev/null |
|
|
| INSPECTION_SUMMARY="$(curl -fsS "${BASE_URL}/api/sessions/${SESSION_ID}/inspection?includeContext=1" | bun -e 'const input = await Bun.stdin.text(); const data = JSON.parse(input); const ctx = data.context || data.contextEstimate; console.log(JSON.stringify({ model: ctx?.model, totalTokens: ctx?.totalTokens, rawMaxTokens: ctx?.rawMaxTokens, percentage: ctx?.percentage, categories: ctx?.categories?.slice(0, 5)?.map((c) => ({ name: c.name, tokens: c.tokens })) }, null, 2));')" |
|
|
| echo "Context usage live WebUI E2E passed" |
| echo "Model: ${CURRENT_MODEL}" |
| echo "Session: ${SESSION_ID}" |
| echo "API port: ${API_PORT}" |
| echo "Web port: ${WEB_PORT}" |
| echo "Artifacts: ${ARTIFACT_DIR}" |
| echo "Inspection:" |
| echo "${INSPECTION_SUMMARY}" |
|
|