104 lines
6.3 KiB
TypeScript
104 lines
6.3 KiB
TypeScript
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);
|
|
});
|