import { expect, test } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { responsesFixture } from "./support/responses-fixture"; test.setTimeout(60_000); test("service detection recovers without a reload and retains the draft", async ({ page }) => { await page.clock.install(); await mockWorkbenchContext(page); await mockAgentApi(page); let available = false; let healthReads = 0; const headers = { "Access-Control-Allow-Origin": "*" }; await page.route("**/health", (route) => { healthReads++; return route.fulfill({ headers, status: available ? 200 : 503, json: { ok: available, ready: available } }); }); await page.route("**/api/v1/models", (route) => available ? route.fallback() : route.fulfill({ headers, status: 503, json: { message: "temporarily unavailable" } })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const state = panel.locator("header").getByRole("status"); await expect(state).toHaveText("离线"); const prompt = panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }); await prompt.fill("恢复后继续核查压力"); available = true; await page.clock.runFor(5_000); await expect(state).toHaveText("在线"); await expect(prompt).toHaveValue("恢复后继续核查压力"); available = false; await page.clock.runFor(15_000); await expect(state).toHaveText("离线"); available = true; await page.evaluate(() => window.dispatchEvent(new Event("focus"))); await expect(state).toHaveText("在线"); expect(healthReads).toBeGreaterThanOrEqual(4); }); test("a completed business failure does not mark a healthy Agent offline", async ({ page }) => { await mockWorkbenchContext(page); await mockAgentApi(page); await page.route("**/health", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { ok: true, ready: true } })); await page.route("**/api/v1/responses", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" }, body: responsesFixture([{ type: "start", messageId: "business-failure" }, { type: "error", errorText: "本次数据查询失败" }, { type: "finish", finishReason: "error" }]) })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const state = panel.locator("header").getByRole("status"); await expect(state).toHaveText("在线"); await panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("核对压力"); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); await expect(page.getByText("本次数据查询失败", { exact: true }).first()).toBeVisible(); await expect(state).toHaveText("在线"); }); test("a disconnected session resubscribes without replaying input or moving an older reader", async ({ page }) => { await page.clock.install(); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); const sessionId = "recover-stream"; await mockAgentApi(page, { sessions: [{ session_id: sessionId, title: "断线中的压力分析", created_at: Date.now(), updated_at: Date.now() }] }); const headers = { "Access-Control-Allow-Origin": "*" }; const history = Array.from({ length: 18 }, (_, i) => ({ id: `older-${i}`, role: "assistant", parts: [{ type: "text", text: `历史核查记录 ${i}:需要保留这段已读内容和阅读位置。` }] })); const messages = (text: string) => [...history, { id: "stream-result", role: "assistant", parts: [{ type: "text", text }] }]; let subscriptions = 0; let generationRequests = 0; await page.route("**/health", (route) => route.fulfill({ headers, json: { ok: true, ready: true } })); await page.route(`**/api/v1/session/${sessionId}?**`, (route) => route.fulfill({ headers, json: { session_id: sessionId, title: "断线中的压力分析", purpose: "ordinary_chat", status: "active", is_streaming: true, run_status: "running", messages: messages("已收到第一段结果。") } })); await page.route(`**/api/v1/session/${sessionId}/stream**`, (route) => { subscriptions++; const finished = subscriptions > 1; return route.fulfill({ headers: { ...headers, "Content-Type": "text/event-stream" }, body: `event: state\ndata: ${JSON.stringify({ session_id: sessionId, messages: messages(finished ? "断线期间的结果已恢复。" : "已收到第一段结果。"), is_streaming: !finished, run_status: finished ? "completed" : "running" })}\n\n` }); }); await page.route("**/api/v1/responses", (route) => { generationRequests++; return route.abort(); }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await panel.getByRole("button", { name: /历史/ }).click(); await page.locator("button.agent-history-session-trigger").filter({ hasText: "断线中的压力分析" }).click(); await expect.poll(() => subscriptions).toBe(1); const prompt = panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }); await prompt.fill("保留未发送的补充问题"); const scroll = panel.locator(".agent-conversation-scroll"); await page.clock.runFor(100); await scroll.hover(); await page.mouse.wheel(0, -2_400); await page.clock.runFor(100); await expect.poll(() => scroll.evaluate((element) => element.scrollTop)).toBeLessThanOrEqual(2); await page.clock.runFor(2_000); await expect.poll(() => subscriptions).toBe(2); await expect(panel.locator('[data-agent-message-id="stream-result"]')).toContainText("断线期间的结果已恢复。"); await expect(panel.locator('[data-agent-message-id="older-0"]')).toHaveCount(1); await expect(prompt).toHaveValue("保留未发送的补充问题"); expect(await scroll.evaluate((element) => element.scrollTop)).toBeLessThanOrEqual(2); expect(generationRequests).toBe(0); });