212 lines
13 KiB
TypeScript
212 lines
13 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
|
import { createServer, type ServerResponse } from "node:http";
|
|
import { responseFixtureWriter } from "./support/responses-fixture";
|
|
import { expectThemeColor } from "./support/theme-colors";
|
|
|
|
async function prepare(page: Page) {
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
await mockWorkbenchContext(page);
|
|
await mockAgentApi(page);
|
|
const counts = { sessions: 0, suggestions: 0, projects: 0 };
|
|
page.on("request", (request) => {
|
|
if (request.method() === "OPTIONS") return;
|
|
const path = new URL(request.url()).pathname;
|
|
if (path.endsWith("/projects")) counts.projects++;
|
|
if (path.endsWith("/suggestions")) counts.suggestions++;
|
|
});
|
|
const sessions = Array.from({ length: 65 }, (_, index) => ({
|
|
id: `history-${index}`, session_id: `history-${index}`,
|
|
title: index === 64 ? "城南供水压力与流量证据核查" : `供水监测复核 ${index + 1}`,
|
|
updated_at: Date.now() - index * 60_000, created_at: Date.now() - index * 60_000, purpose: "ordinary_chat", run_status: "completed",
|
|
}));
|
|
await page.route("**/api/v1/sessions?**", (route) => {
|
|
if (route.request().method() === "OPTIONS") return route.fallback();
|
|
counts.sessions++;
|
|
const query = new URL(route.request().url()).searchParams;
|
|
const filtered = sessions.filter((item) => item.title.includes(query.get("q") ?? ""));
|
|
const start = Number(query.get("cursor") ?? 0);
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
|
|
sessions: filtered.slice(start, start + 30), has_more: start + 30 < filtered.length,
|
|
next_cursor: start + 30 < filtered.length ? String(start + 30) : null,
|
|
} });
|
|
});
|
|
const messages = Array.from({ length: 90 }, (_, index) => ({ id: `message-${index}`, role: index % 2 ? "assistant" : "user",
|
|
parts: [{ type: "text", text: `第 ${index + 1} 条:供水监测证据核对。${"压力、流量和监测时窗均需复核。".repeat(5)}` }] }));
|
|
await page.route(/\/api\/v1\/session\/history-[^?]+(?:\?.*)?$/, (route) => {
|
|
if (route.request().method() === "OPTIONS") return route.fallback();
|
|
const url = new URL(route.request().url());
|
|
if (url.pathname.endsWith("/messages")) {
|
|
const end = Number(url.searchParams.get("before"));
|
|
const start = Math.max(0, end - 40);
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
|
|
messages: messages.slice(start, end), message_page: { has_more: start > 0, before_cursor: start > 0 ? String(start) : null },
|
|
} });
|
|
}
|
|
const id = url.pathname.split("/").at(-1);
|
|
const session = sessions.find((item) => item.id === id);
|
|
if (!session) return route.fallback();
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
|
|
...session, messages: messages.slice(50), message_page: { has_more: true, before_cursor: "50" },
|
|
} });
|
|
});
|
|
return counts;
|
|
}
|
|
|
|
test("history searches all titles, loads pages and reuses lists and recommendations", async ({ page }, testInfo) => {
|
|
// Includes ten full focus cycles over 60 retained rows, plus pagination and search.
|
|
test.setTimeout(60_000);
|
|
// Keep the activity snapshot stable while testing cache hits; real status changes have unit coverage.
|
|
await page.clock.setFixedTime(new Date());
|
|
const counts = await prepare(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const trigger = page.getByRole("button", { name: "打开 Agent 历史记录" });
|
|
await expect(page.getByRole("group", { name: "推荐问题" })).toHaveAttribute("aria-busy", "false");
|
|
await expect(page.getByRole("button", { name: "选择 Agent 模型" }).locator("svg.lucide-zap")).toBeVisible();
|
|
const baseline = { ...counts };
|
|
await trigger.click();
|
|
const dialog = page.getByRole("dialog", { name: "历史对话" });
|
|
await expect(dialog).toBeVisible();
|
|
await expect(dialog.getByRole("searchbox")).toBeFocused();
|
|
await expectThemeColor(dialog, "background-color", "--surface-reading");
|
|
await expect(dialog).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
await expect(dialog.locator("[data-agent-history-header]")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
await expect(dialog.getByRole("region", { name: "Agent 历史记录" })).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
await expect(page.locator(".agent-history-overlay")).toHaveCSS("backdrop-filter", "none");
|
|
await expect(dialog.getByRole("button", { name: "关闭历史对话" }).locator("svg.lucide-x")).toHaveCSS("width", "16px");
|
|
await expect(dialog.locator(".agent-history-session-trigger")).toHaveCount(30);
|
|
await dialog.getByRole("button", { name: "加载更多", exact: true }).click();
|
|
await expect(dialog.locator(".agent-history-session-trigger")).toHaveCount(60);
|
|
await dialog.getByRole("searchbox").fill("城南");
|
|
await expect(dialog.locator(".agent-history-session-trigger")).toHaveCount(1);
|
|
await expect(dialog.getByText("城南供水压力与流量证据核查", { exact: true })).toBeVisible();
|
|
await dialog.getByRole("searchbox").fill("");
|
|
await expect(dialog.locator(".agent-history-session-trigger")).toHaveCount(60);
|
|
await page.keyboard.press("Escape");
|
|
await expect(trigger).toBeFocused();
|
|
const loadedCounts = { ...counts };
|
|
const cycleMs: number[] = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
const started = Date.now();
|
|
await trigger.click();
|
|
await expect(dialog).toBeVisible();
|
|
await expect(dialog.locator(".agent-history-session-trigger")).toHaveCount(60);
|
|
await page.keyboard.press("Escape");
|
|
await expect(dialog).toBeHidden();
|
|
await expect(trigger).toBeFocused();
|
|
cycleMs.push(Date.now() - started);
|
|
}
|
|
await testInfo.attach("history-cache-metrics", { contentType: "application/json", body: JSON.stringify({ before: loadedCounts, after: counts, cycleMs }) });
|
|
expect(counts).toEqual(loadedCounts);
|
|
expect(counts.suggestions).toBe(baseline.suggestions);
|
|
expect(counts.projects).toBe(baseline.projects);
|
|
await trigger.click();
|
|
await dialog.screenshot({ path: "test-results/agent-history/desktop.png" });
|
|
});
|
|
|
|
for (const width of [320, 375]) test(`history is full screen and preserves the mobile sheet at ${width}px`, async ({ page }) => {
|
|
await page.setViewportSize({ width, height: 812 });
|
|
await prepare(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
const dialog = page.getByRole("dialog", { name: "历史对话" });
|
|
await expect(dialog).toBeVisible();
|
|
await expect(dialog).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
const box = (await dialog.boundingBox())!;
|
|
expect(box.x).toBe(0); expect(box.y).toBe(0); expect(box.width).toBe(width);
|
|
expect(await dialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
|
await dialog.screenshot({ path: `test-results/agent-history/mobile-${width}.png` });
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(window.visualViewport!, "height", { configurable: true, value: 480 });
|
|
window.visualViewport!.dispatchEvent(new Event("resize"));
|
|
});
|
|
await expect.poll(async () => (await dialog.boundingBox())!.height).toBe(480);
|
|
await expect(dialog.getByRole("button", { name: "关闭历史对话" })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(dialog).toBeHidden();
|
|
await expect(page.getByRole("region", { name: "Agent 工作台抽屉" })).toBeVisible();
|
|
});
|
|
|
|
test("older messages prepend without moving the visible reading anchor", async ({ page }) => {
|
|
await prepare(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).locator(".agent-history-session-trigger").first().click();
|
|
await expect(page.getByRole("dialog", { name: "历史对话" })).toBeHidden();
|
|
await expect(page.locator("[data-agent-message-id]")).toHaveCount(40);
|
|
const scroll = page.locator(".agent-conversation-scroll");
|
|
await scroll.evaluate((element) => { element.scrollTop = 1; element.dispatchEvent(new Event("scroll")); });
|
|
await expect(page.locator("[data-agent-message-id]")).toHaveCount(80);
|
|
expect(await scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(100);
|
|
await expect(page.getByRole("button", { name: "滚动到最新消息" })).toBeVisible();
|
|
const position = await scroll.evaluate((element) => element.scrollTop);
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /供水监测复核 2\s/ }).click();
|
|
await expect(page.locator("[data-agent-message-id]")).toHaveCount(40);
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /供水监测复核 1\s/ }).click();
|
|
await expect(page.locator("[data-agent-message-id]")).toHaveCount(80);
|
|
await expect.poll(async () => Math.abs(await scroll.evaluate((element) => element.scrollTop) - position)).toBeLessThan(2);
|
|
});
|
|
|
|
test("switching conversations keeps the original generation and each draft isolated", async ({ page }) => {
|
|
await prepare(page);
|
|
let output: ServerResponse | undefined;
|
|
let aborts = 0;
|
|
let readsOfRunningSession = 0;
|
|
const write = responseFixtureWriter("background-a");
|
|
const server = createServer((_request, response) => {
|
|
output = response;
|
|
response.writeHead(200, { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
|
|
response.write(write({ type: "start", messageId: "background-answer" }));
|
|
response.write(write({ type: "data-progress", data: { session_id: "background-a", title: "正在复核后台压力", status: "running" } }));
|
|
response.write(write({ type: "data-session_title", data: { session_id: "background-a", title: "后台压力复核 A" } }));
|
|
response.write(write({ type: "text-start", id: "background-text" }));
|
|
response.write(write({ type: "text-delta", id: "background-text", delta: "已开始复核," }));
|
|
});
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") throw new Error("missing fixture server address");
|
|
try {
|
|
await page.route("**/api/v1/responses", (route) => route.request().method() === "OPTIONS" ? route.fallback()
|
|
: route.continue({ url: `http://127.0.0.1:${address.port}/responses` }));
|
|
page.on("request", (request) => {
|
|
if (request.method() === "OPTIONS") return;
|
|
const path = new URL(request.url()).pathname;
|
|
if (path.endsWith("/abort")) aborts++;
|
|
if (path.endsWith("/session/background-a")) readsOfRunningSession++;
|
|
});
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const prompt = page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" });
|
|
await prompt.fill("核对后台压力");
|
|
await page.getByRole("button", { name: "发送 Agent 指令", exact: true }).click();
|
|
await expect(page.getByText("已开始复核,", { exact: true })).toBeVisible();
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /供水监测复核 1\s/ }).click();
|
|
await prompt.fill("保留在另一会话的草稿");
|
|
await expect(page.getByRole("button", { name: /会话正在后台处理/ })).toBeVisible();
|
|
output!.write(write({ type: "text-delta", id: "background-text", delta: "后台证据核对完成。" }));
|
|
output!.write(write({ type: "text-end", id: "background-text" }));
|
|
output!.write(write({ type: "finish" }));
|
|
output!.end();
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /后台压力复核 A\s/ }).click();
|
|
await expect(page.getByText("已开始复核,后台证据核对完成。", { exact: true })).toBeVisible();
|
|
await expect(prompt).toHaveValue("");
|
|
expect(aborts).toBe(0);
|
|
expect(readsOfRunningSession).toBe(0);
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /供水监测复核 1\s/ }).click();
|
|
await expect(prompt).toHaveValue("保留在另一会话的草稿");
|
|
} finally {
|
|
output?.end();
|
|
server.closeAllConnections();
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
}
|
|
});
|