import { expect, test } from "@playwright/test"; import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; for (const status of ["completed", "error", "aborted"] as const) { test(`tool progress settles after reconnect and reopening (${status})`, async ({ page }, testInfo) => { await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); const sessionId = `tool-terminal-${status}`; const title = `工具终态核查 ${status}`; await mockAgentApi(page, { sessions: [{ session_id: sessionId, title, created_at: Date.now(), updated_at: Date.now() }] }); const headers = { "Access-Control-Allow-Origin": "*" }; const messages = [{ id: "tool-answer", role: "assistant", parts: [ { type: "text", text: "已查到部分压力记录。" }, { type: "data-progress", data: { id: "tool", phase: "tool", title: "正在处理", status: "running", tool: { name: "query_pressure", input: { node_id: 12 } } } }, ] }]; let settled = false; let finish: (() => void) | undefined; const server = createServer((_request, response) => { response.writeHead(200, { ...headers, "Content-Type": "text/event-stream" }); response.write(`event: state\ndata: ${JSON.stringify({ session_id: sessionId, messages, is_streaming: true, run_status: "running" })}\n\n`); finish = () => { settled = true; const type = status === "completed" ? "done" : status === "aborted" ? "aborted" : "error"; const data = { session_id: sessionId, status, message: status === "error" ? "压力查询连接失败" : "请求已中断" }; // Legacy subscriptions may terminate without a separate run_status event. response.end(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); }; }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); await page.route(`**/api/v1/session/${sessionId}?**`, (route) => route.fulfill({ headers, json: { session_id: sessionId, title, purpose: "ordinary_chat", status: "active", is_streaming: !settled, run_status: settled ? status : "running", messages, } })); await page.route(`**/api/v1/session/${sessionId}/stream**`, (route) => route.continue({ url: `http://127.0.0.1:${(server.address() as AddressInfo).port}` })); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const answer = panel.locator('[data-agent-message-id="tool-answer"]'); const icon = answer.locator('[data-agent-tool] [data-progress-status]'); const open = async () => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click(); await page.locator("button.agent-history-session-trigger").filter({ hasText: title }).click(); }; try { await open(); await expect(icon).toHaveAttribute("data-progress-status", "running"); await expect.poll(() => Boolean(finish)).toBe(true); finish!(); await answer.getByRole("button", { name: "查看处理过程", exact: true }).click(); const expectedStatus = status === "aborted" ? "cancelled" : status; await expect(icon).toHaveAttribute("data-progress-status", expectedStatus); await expect(answer.locator('[data-progress-status="running"]')).toHaveCount(0); if (status === "error") await expect(answer.getByRole("status").filter({ hasText: "压力查询连接失败" })).toBeVisible(); await answer.screenshot({ path: testInfo.outputPath(`${status}-settled.png`), animations: "disabled" }); await open(); await answer.getByRole("button", { name: "查看处理过程", exact: true }).click(); await expect(icon).toHaveAttribute("data-progress-status", expectedStatus); await expect(answer.locator('[data-progress-status="running"]')).toHaveCount(0); if (status === "aborted") await expect(icon).toHaveAttribute("aria-label", "已中止"); await answer.screenshot({ path: testInfo.outputPath(`${status}-reopened.png`), animations: "disabled" }); } finally { server.closeAllConnections(); await new Promise((resolve) => server.close(() => resolve())); } }); }