import { expect, test, type Page } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { responseFixtureWriter } from "./support/responses-fixture"; async function setupTopics(page: Page) { await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page); const topics = [{ id: "10000000-0000-4000-8000-000000000001", name: "压力异常事件", createdAt: "2026-10-22T01:00:00Z", updatedAt: "2026-10-22T01:00:00Z" }, { id: "10000000-0000-4000-8000-000000000002", name: "阀门检修问题", createdAt: "2026-10-22T01:00:00Z", updatedAt: "2026-10-22T01:00:00Z" }]; const sessions = [{ id: "topic-chat-a", title: "夜间压力复核", topic_id: topics[0].id, updated_at: Date.now(), created_at: Date.now(), purpose: "ordinary_chat" }, { id: "topic-chat-b", title: "检修记录核对", topic_id: topics[1].id, updated_at: Date.now() - 1000, created_at: Date.now() - 1000, purpose: "ordinary_chat" }]; const metrics = { topicReads: 0, sentTopic: "" }; await page.route(/\/api\/v1\/topics(?:\/[^?]+)?$/, (route) => { const request = route.request(); if (request.method() === "OPTIONS") return route.fallback(); const id = new URL(request.url()).pathname.split("/").at(-1); const headers = { "Access-Control-Allow-Origin": "*" }; if (request.method() === "GET") { metrics.topicReads++; return route.fulfill({ headers, json: { topics } }); } if (request.method() === "POST") { const topic = { id: "10000000-0000-4000-8000-000000000003", name: request.postDataJSON().name, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; topics.push(topic); return route.fulfill({ headers, status: 201, json: topic }); } if (request.method() === "PATCH") { const topic = topics.find((entry) => entry.id === id)!; topic.name = request.postDataJSON().name; return route.fulfill({ headers, json: topic }); } const index = topics.findIndex((entry) => entry.id === id); topics.splice(index, 1); return route.fulfill({ headers, status: 204 }); }); await page.route("**/api/v1/sessions?**", (route) => { if (route.request().method() === "OPTIONS") return route.fallback(); const query = new URL(route.request().url()).searchParams; const q = query.get("q") ?? ""; const topicId = query.get("topic_id"); return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { sessions: sessions.map((session) => ({ ...session, topic_id: topics.some((topic) => topic.id === session.topic_id) ? session.topic_id : null, topic_name: topics.find((topic) => topic.id === session.topic_id)?.name, search_snippet: q && "南桥阀门夜间巡检记录".includes(q) ? "正文命中:南桥阀门夜间巡检记录" : undefined, })).filter((session) => (!q || session.title.includes(q) || session.search_snippet) && (!topicId || (topicId === "unclassified" ? !session.topic_id : session.topic_id === topicId))), has_more: false, next_cursor: null, } }); }); await page.route(/\/api\/v1\/session\/topic-chat-[ab]\/topic$/, (route) => { if (route.request().method() === "OPTIONS") return route.fallback(); const id = new URL(route.request().url()).pathname.split("/").at(-2); const topicId = route.request().postDataJSON().topic_id; sessions.find((session) => session.id === id)!.topic_id = topicId; return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { session_id: id, topic_id: topicId } }); }); await page.route("**/api/v1/responses", (route) => { if (route.request().method() === "OPTIONS") return route.fallback(); metrics.sentTopic = route.request().postDataJSON().tjwater.topic_id; const write = responseFixtureWriter("new-topic-conversation"); return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, contentType: "text/event-stream", body: [ { type: "start", messageId: "topic-answer" }, { type: "text-start", id: "topic-text" }, { type: "text-delta", id: "topic-text", delta: "已按当前主题整理问题。" }, { type: "text-end", id: "topic-text" }, { type: "finish" }, ].map(write).join("") }); }); return metrics; } test("topics organize chats while full-text history searches across all topics", async ({ page }) => { test.setTimeout(60_000); const metrics = await setupTopics(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const topicTrigger = page.getByRole("button", { name: "选择主题" }); await topicTrigger.click(); await page.getByRole("menuitem", { name: "新建主题", exact: true }).click(); const create = page.getByRole("dialog", { name: "新建主题", exact: true }); await expect(create.getByRole("textbox", { name: "主题名称" })).toBeFocused(); await expect(create).toHaveCSS("background-color", "rgb(255, 255, 255)"); await expect(page.locator(".agent-history-overlay")).toHaveCSS("backdrop-filter", "none"); await create.screenshot({ path: "test-results/agent-topics/create.png" }); await create.getByRole("textbox", { name: "主题名称" }).fill("城南供水复盘"); await create.getByRole("button", { name: "创建主题", exact: true }).click(); await expect(create).toBeHidden(); await expect(topicTrigger).toHaveText("城南供水复盘"); await topicTrigger.click(); await page.getByRole("menuitemradio", { name: "压力异常事件", exact: true }).click(); await expect(topicTrigger).toHaveText("压力异常事件"); await topicTrigger.click(); await page.getByRole("menuitemradio", { name: "城南供水复盘", exact: true }).click(); await expect(topicTrigger).toHaveText("城南供水复盘"); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await panel.locator("textarea").fill("整理相关事件与待查问题"); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); await expect(panel.getByText("已按当前主题整理问题。", { exact: true })).toBeVisible(); expect(metrics.sentTopic).toBe("10000000-0000-4000-8000-000000000003"); await panel.locator("textarea").fill("这段草稿稍后继续整理"); await panel.getByRole("button", { name: "新建 Agent 对话" }).click(); await expect(panel.locator("textarea")).toHaveValue(""); await expect(topicTrigger).toHaveText("城南供水复盘"); await page.getByRole("button", { name: "打开 Agent 历史记录" }).click(); await page.getByRole("dialog", { name: "历史对话", exact: true }).locator(".agent-history-session-trigger").filter({ hasText: "新对话" }).click(); await expect(page.getByRole("dialog", { name: "历史对话", exact: true })).toBeHidden(); await expect(panel.locator("textarea")).toHaveValue("这段草稿稍后继续整理"); await page.getByRole("button", { name: "打开 Agent 历史记录" }).click(); const history = page.getByRole("dialog", { name: "历史对话", exact: true }); await history.getByRole("searchbox").fill("南桥阀门"); await expect(history.locator(".agent-history-session-trigger")).toHaveCount(2); await expect(history.getByText("压力异常事件", { exact: true })).toBeVisible(); await expect(history.getByText("阀门检修问题", { exact: true })).toBeVisible(); await expect(history.getByText("正文命中:南桥阀门夜间巡检记录", { exact: true })).toHaveCount(2); await history.getByRole("button", { name: "会话操作:夜间压力复核" }).click(); await page.getByRole("menuitem", { name: "移入主题", exact: true }).click(); await history.getByRole("button", { name: "城南供水复盘", exact: true }).click(); await expect(history.getByText("城南供水复盘", { exact: true })).toBeVisible(); await history.getByRole("button", { name: "筛选历史主题" }).click(); await expect(page.getByRole("menu")).toHaveCSS("background-color", "rgb(255, 255, 255)"); await page.getByRole("menuitemradio", { name: "阀门检修问题", exact: true }).click(); await expect(history.locator(".agent-history-session-trigger")).toHaveCount(1); await history.getByRole("button", { name: "筛选历史主题" }).click(); await page.getByRole("menuitemradio", { name: "全部主题", exact: true }).click(); await expect(history.locator(".agent-history-session-trigger")).toHaveCount(2); await history.screenshot({ path: "test-results/agent-topics/cross-topic-search.png" }); await page.keyboard.press("Escape"); await topicTrigger.click(); await page.getByRole("menuitem", { name: "管理主题", exact: true }).click(); const manage = page.getByRole("dialog", { name: "管理主题", exact: true }); await expect(manage.getByRole("button", { name: "新建主题", exact: true })).toBeFocused(); await manage.screenshot({ path: "test-results/agent-topics/manage.png" }); await manage.getByRole("button", { name: "重命名主题:城南供水复盘" }).click(); await expect(page.getByRole("dialog", { name: "重命名主题", exact: true }).getByRole("textbox", { name: "主题名称" })).toBeFocused(); await page.getByRole("dialog", { name: "重命名主题", exact: true }).getByRole("textbox", { name: "主题名称" }).fill("城南复盘与跟进"); await page.getByRole("dialog", { name: "重命名主题", exact: true }).getByRole("button", { name: "保存主题" }).click(); await expect(manage.getByText("城南复盘与跟进", { exact: true })).toBeVisible(); await manage.getByRole("button", { name: "关闭主题窗口" }).click(); await expect(topicTrigger).toHaveText("城南复盘与跟进"); await topicTrigger.click(); await page.getByRole("menuitem", { name: "管理主题", exact: true }).click(); await manage.getByRole("button", { name: "删除主题:城南复盘与跟进" }).click(); await page.getByRole("dialog", { name: "删除主题", exact: true }).screenshot({ path: "test-results/agent-topics/delete.png" }); await page.getByRole("dialog", { name: "删除主题", exact: true }).getByRole("button", { name: "确认删除主题" }).click(); await expect(manage.getByText("城南复盘与跟进", { exact: true })).toHaveCount(0); await expect(manage.getByRole("button", { name: "关闭主题窗口" })).toBeEnabled(); await page.keyboard.press("Escape"); await expect(manage).toBeHidden(); await expect(topicTrigger).toHaveText("未分类"); const reads = metrics.topicReads; for (let i = 0; i < 3; i++) { await topicTrigger.click(); await expect(page.getByRole("menuitem", { name: "新建主题", exact: true })).toBeVisible(); await page.keyboard.press("Escape"); } expect(metrics.topicReads).toBe(reads); await page.getByRole("button", { name: "打开 Agent 历史记录" }).click(); await expect(history.getByText("夜间压力复核", { exact: true })).toBeVisible(); await expect(history.getByText("未分类", { exact: true })).toBeVisible(); }); test("a topic can be created on a 320px mobile sheet", async ({ page }) => { await page.setViewportSize({ width: 320, height: 812 }); await setupTopics(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: "打开 Agent 面板" }).click(); await page.getByRole("button", { name: "选择主题" }).click(); await page.getByRole("menuitem", { name: "新建主题", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "新建主题", exact: true }); await expect(dialog.getByRole("textbox", { name: "主题名称" })).toBeFocused(); await dialog.getByRole("textbox", { name: "主题名称" }).fill("供水异常事件与问题跟踪"); expect(await dialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true); await dialog.screenshot({ path: "test-results/agent-topics/mobile-create.png" }); await dialog.getByRole("button", { name: "创建主题", exact: true }).click(); await expect(dialog).toBeHidden(); await expect(page.getByRole("button", { name: "选择主题" })).toHaveText("供水异常事件与问题跟踪"); }); test("a topic created during a slow menu read remains available for switching", async ({ page }) => { await setupTopics(page); let release!: () => void; const gate = new Promise((resolve) => { release = resolve; }); let writes = 0; await page.route("**/api/v1/topics", async (route) => { if (route.request().method() === "GET") await gate; if (route.request().method() === "POST") writes++; await route.fallback(); }); try { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const trigger = page.getByRole("button", { name: "选择主题", exact: true }); await trigger.click(); await page.getByRole("menuitem", { name: "新建主题", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "新建主题", exact: true }); await dialog.getByRole("textbox", { name: "主题名称" }).fill("新建后持续跟踪"); await dialog.getByRole("button", { name: "创建主题", exact: true }).click(); await expect(dialog.locator('button[type="submit"]')).toHaveAttribute("aria-busy", "true"); expect(writes).toBe(0); release(); await expect(dialog).toBeHidden(); await expect(trigger).toHaveText("新建后持续跟踪"); await expect(page.locator("body")).not.toHaveCSS("pointer-events", "none"); await trigger.click(); await page.getByRole("menuitemradio", { name: "压力异常事件", exact: true }).click(); await expect(trigger).toHaveText("压力异常事件"); await trigger.click(); await page.getByRole("menuitemradio", { name: "新建后持续跟踪", exact: true }).click(); await expect(trigger).toHaveText("新建后持续跟踪"); expect(writes).toBe(1); } finally { release(); } }); test("a failed topic creation keeps the name and allows retry", async ({ page }) => { await setupTopics(page); let failed = false; await page.route("**/api/v1/topics", (route) => { if (route.request().method() !== "POST" || failed) return route.fallback(); failed = true; return route.fulfill({ status: 503, headers: { "Access-Control-Allow-Origin": "*" }, json: { message: "主题暂时无法保存,请重试" } }); }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const trigger = page.getByRole("button", { name: "选择主题", exact: true }); await trigger.click(); await page.getByRole("menuitem", { name: "新建主题", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "新建主题", exact: true }); await dialog.getByRole("textbox", { name: "主题名称" }).fill("保留输入并重试"); await dialog.getByRole("button", { name: "创建主题", exact: true }).click(); await expect(dialog.getByRole("alert")).toContainText("主题暂时无法保存"); await expect(dialog.getByRole("textbox", { name: "主题名称" })).toHaveValue("保留输入并重试"); await expect(dialog.getByRole("button", { name: "关闭主题窗口" })).toBeEnabled(); await dialog.getByRole("button", { name: "创建主题", exact: true }).click(); await expect(dialog).toBeHidden(); await expect(trigger).toHaveText("保留输入并重试"); await expect(trigger).toBeFocused(); });