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"; for (const width of [1440, 320]) { test(`restored clarification process belongs to its continuation at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 900 }); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); const sessionId = "clarification-process"; const nativeReplyId = `msg_${"b".repeat(32)}`; const continuationId = `answer_${nativeReplyId}`; const progress = (id: string, title: string) => ({ type: "data-progress", data: { id, title, phase: "tool", status: "completed", detail: "合成演练记录。" } }); const messages: Array<{ id: string; role: string; parts: Array<{ type: string; [key: string]: unknown }> }> = [ { id: "original-user", role: "user", parts: [{ type: "text", text: "分别模拟三根管道。" }] }, { id: "clarifying-message", role: "assistant", parts: [progress("query", "核对管道与时段")] }, { id: nativeReplyId, role: "user", parts: [{ type: "text", text: "用户提交的提问回复:按顺序对应时段" }] }, { id: continuationId, role: "assistant", parts: [ progress("query", "完成对应时段模拟"), { type: "text", text: "三根管道的模拟结果已整理,仅供演练。" } ] } ]; const interaction = { version: "agent-interaction-request@1", interaction_id: "clarification", message_id: "clarifying-message", turn_id: "clarification-turn", call_id: "ask", status: "answered", revision: 1, draft_revision: 0, draft: [], created_at: "2026-10-28T10:00:00Z", questions: [{ question_id: "mapping", title: "管道与时段对应", body: "请确认对应顺序。", mode: "single", required: true, allow_custom: false, options: [{ id: "ordered", label: "按顺序对应时段" }] }], answers: [{ question_id: "mapping", selected_option_ids: ["ordered"], custom_text: "", selected_options: [{ id: "ordered", label: "按顺序对应时段" }] }], delivery: { native_message_id: nativeReplyId, revision: 2, status: "blocked", error_code: "CONTINUATION_UNCONFIRMED" } }; messages[2].parts.push({ type: "data-interaction_reply", data: interaction }); let releaseInteractions!: () => void; let interactionGate = new Promise((resolve) => { releaseInteractions = resolve; }); await mockAgentApi(page, { sessions: [{ session_id: sessionId, title: "补充问题后继续模拟", created_at: Date.now(), updated_at: Date.now() }] }); const headers = { "Access-Control-Allow-Origin": "*" }; let writes = 0; let finishBeforeNextInput = false; const queued: Array<{ id: string; text: string; delivery: string; status: string }> = []; await page.route(`**/api/v1/session/${sessionId}?**`, (route) => route.fulfill({ headers, json: { session_id: sessionId, title: "补充问题后继续模拟", purpose: "ordinary_chat", status: "active", is_streaming: false, messages } })); await page.route(`**/api/v1/session/${sessionId}/interactions**`, async (route) => { if (route.request().method() !== "GET") writes++; await interactionGate; return route.fulfill({ headers, json: { items: [interaction] } }); }); await page.route(`**/api/v1/session/${sessionId}/inputs`, (route) => route.fulfill({ headers, json: { items: queued } })); await page.route("**/api/v1/responses", (route) => { writes++; const body = route.request().postDataJSON(); if (finishBeforeNextInput) { expect(typeof body.input === "string" ? body.input : body.input[0].content[0].text).toBe("继续检查模拟结果"); if (body.stream === false) { interaction.delivery.status = "delivered"; return route.fulfill({ headers, status: 409, json: { code: "RESPONSE_FINISHED", message: "Finished before admission" } }); } return route.fulfill({ headers: { ...headers, "Content-Type": "text/event-stream" }, body: responsesFixture([ { type: "start", messageId: "next-response" }, { type: "text-start", id: "text" }, { type: "text-delta", id: "text", delta: "已开始新一轮核查。" }, { type: "text-end", id: "text" }, { type: "finish", finishReason: "stop" } ]) }); } expect(body.stream).toBe(false); expect(body.conversation).toBe(sessionId); const receipt = { id: body.tjwater.input_id, text: body.input, delivery: body.tjwater.delivery, status: "pending" }; queued.push(receipt); return route.fulfill({ headers, status: 202, json: receipt }); }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 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(); const continuation = panel.locator(`[data-agent-message-id="${continuationId}"]`); await expect(continuation.getByText("三根管道的模拟结果已整理,仅供演练。", { exact: true })).toBeVisible(); const reply = panel.getByRole("group", { name: "已提交的回答" }); await expect(reply).toHaveCount(1); await expect(panel.getByText(/用户提交的提问回复/)).toHaveCount(0); await expect(reply.getByText("按顺序对应时段", { exact: true })).toBeVisible(); releaseInteractions(); await expect(panel.locator('[data-agent-message-id="clarifying-message"]')).toHaveCount(0); await expect(panel.getByRole("button", { name: "查看处理过程", exact: true })).toHaveCount(1); const toggle = continuation.getByRole("button", { name: "查看处理过程", exact: true }); await toggle.click(); const process = continuation.locator("details[data-agent-process]"); await expect(process.getByText("核对管道与时段", { exact: true })).toBeVisible(); await expect(process.getByText("完成对应时段模拟", { exact: true })).toBeVisible(); await expect(process.locator("[data-agent-progress]")).toHaveCount(1); await expect(process.getByRole("list", { name: "全部执行进度" }).locator("li")).toHaveCount(2); await expect(process.getByRole("list", { name: "全部执行进度" }).locator("[data-tool-name]")).toHaveText(["核对管道与时段", "完成对应时段模拟"]); await expect(reply.getByText("继续失败,可以重试", { exact: true })).toBeVisible(); await expect(reply.getByRole("button", { name: "重试继续", exact: true })).toBeEnabled(); const reportBox = (await continuation.locator("[data-answer-parts]").first().boundingBox())!; const processBox = (await process.boundingBox())!; expect(processBox.y).toBeGreaterThanOrEqual(reportBox.y + reportBox.height); await panel.screenshot({ path: testInfo.outputPath(`continued-process-${width}.png`) }); // A hard reload must keep the same human card while metadata is delayed again. interactionGate = new Promise((resolve) => { releaseInteractions = resolve; }); await page.reload({ waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); await panel.getByRole("button", { name: /历史/ }).click(); await page.locator("button.agent-history-session-trigger").filter({ hasText: "补充问题后继续模拟" }).click(); await expect(reply).toHaveCount(1); await expect(panel.getByText(/用户提交的提问回复/)).toHaveCount(0); await expect(panel.locator('[data-agent-message-id="clarifying-message"]')).toHaveCount(0); await expect(toggle).toHaveCount(1); releaseInteractions(); await expect(reply.getByRole("button", { name: "重试继续", exact: true })).toBeEnabled(); expect(writes).toBe(0); const prompt = panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }); await prompt.fill("先核查压力数据"); await panel.getByRole("button", { name: "消息发送方式", exact: true }).click(); await page.getByRole("menuitemradio", { name: "排队处理", exact: true }).click(); await panel.getByRole("button", { name: "将消息加入 Agent 队列", exact: true }).click(); await expect(panel.getByRole("region", { name: "待处理消息队列" }).getByText("先核查压力数据", { exact: true })).toBeVisible(); await prompt.fill("优先检查第二根管道"); await panel.getByRole("button", { name: "消息发送方式", exact: true }).click(); await page.getByRole("menuitemradio", { name: "引导当前任务", exact: true }).click(); await panel.getByRole("button", { name: "引导当前 Agent 任务", exact: true }).click(); await expect.poll(() => queued.map((item) => item.delivery)).toEqual(["queue", "steer"]); expect(writes).toBe(2); finishBeforeNextInput = true; await prompt.fill("继续检查模拟结果"); await panel.getByRole("button", { name: "引导当前 Agent 任务", exact: true }).click(); await expect(panel.getByText("已开始新一轮核查。", { exact: true })).toBeVisible(); expect(writes).toBe(4); }); }