import { expect, test } from "@playwright/test"; import { responseFixtureWriter } from "./support/responses-fixture"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import type { InteractionRequest } from "../../src/features/agent/api/interaction-requests"; for (const width of [1440, 320]) { test(`host question restores draft and requires explicit submit at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" }); await mockAgentApi(page, { sessions: [{ session_id: "host-question", title: "待补充范围", created_at: Date.now(), updated_at: Date.now() }] }); const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, PUT, POST, OPTIONS", "Access-Control-Allow-Headers": "Authorization, Content-Type, X-Project-Id" }; let record: InteractionRequest = { version: "agent-interaction-request@1", interaction_id: "i", message_id: "m", turn_id: "t", call_id: "c", questions: [{ question_id: "q", title: "核查范围", body: "选择本次核查范围", options: [{ id: "north", label: "北侧管网" }, { id: "all", label: "全部管网" }], mode: "single", required: true, allow_custom: true }], revision: 0, draft_revision: 0, draft: [], status: "pending", created_at: new Date().toISOString() }; const writes: string[] = []; const inputs: Array<{ id: string; text: string; delivery: string; status: string }> = []; await page.route("**/api/v1/responses", (route) => { const body = route.request().postDataJSON(); expect(body.stream).toBe(false); const input = { id: body.tjwater.input_id, text: body.input, delivery: body.tjwater.delivery, status: "pending" }; inputs.push(input); return route.fulfill({ headers, status: 202, json: input }); }); await page.route("**/api/v1/session/host-question/inputs", (route) => route.fulfill({ headers, json: { items: inputs } })); await page.route("**/api/v1/session/host-question?**", (route) => route.fulfill({ headers, json: { session_id: "host-question", title: "待补充范围", purpose: "ordinary_chat", status: "active", is_streaming: false, messages: [{ id: "m", role: "assistant", parts: [{ type: "text", text: "请补充核查范围。" }] }] } })); await page.route("**/api/v1/session/host-question/interactions**", async (route) => { const request = route.request(); if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers }); const action = new URL(request.url()).pathname.split("/").at(-1)!; if (request.method() === "GET") return route.fulfill({ headers, json: action === "interactions" ? { items: [record] } : record }); writes.push(action); const body = request.postDataJSON(); if (action === "draft") record = { ...record, draft: body.answers, draft_revision: record.draft_revision + 1 }; if (action === "cancel") record = { ...record, status: "cancelled", revision: 1 }; return route.fulfill({ headers, json: record }); }); 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 card = panel.getByRole("article", { name: "需要补充的信息" }); await expect(card).toBeVisible(); await expect(card.getByRole("button", { name: "提交回答" })).toBeDisabled(); await expect(card.getByRole("button", { name: "跳过", exact: true })).toHaveCount(0); await card.getByRole("radio", { name: "北侧管网" }).check(); await card.getByRole("textbox", { name: "核查范围:补充说明" }).fill("优先检查压力下降区域"); await expect.poll(() => record.draft[0]?.custom_text).toBe("优先检查压力下降区域"); expect(writes.length).toBeGreaterThan(0); expect(writes.every((action) => action === "draft")).toBe(true); await expect(card.getByRole("button", { name: "提交回答" })).toBeEnabled(); await card.screenshot({ path: `/tmp/host-question-${width}.png` }); const bounds = await card.boundingBox(); expect(bounds!.x).toBeGreaterThanOrEqual(0); expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width); await panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("先核对压力测点"); await panel.getByRole("button", { name: "引导当前 Agent 任务", exact: true }).click(); await expect.poll(() => inputs.map((input) => input.delivery)).toEqual(["steer"]); await expect(card.getByText("等待回答", { exact: true })).toBeVisible(); expect(writes.every((action) => action === "draft")).toBe(true); await card.getByRole("button", { name: "取消提问" }).click(); await expect(card.getByText("已取消", { exact: true })).toBeVisible(); await expect(card.getByRole("button", { name: "提交回答" })).toHaveCount(0); }); } for (const responseMode of ["completed_without_event", "pending_conflict"] as const) { test(`collaboration restores a persisted question after ${responseMode} and submits its answer`, async ({ page }, testInfo) => { const width = responseMode === "pending_conflict" ? 320 : 1440; await page.setViewportSize({ width, height: 900 }); await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" }); const sessionId = "collaboration-question"; await mockAgentApi(page, { sessions: [{ session_id: sessionId, title: "公司位置协作", purpose: "task_collaboration", created_at: Date.now(), updated_at: Date.now() }] }); const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, PUT, POST, OPTIONS", "Access-Control-Allow-Headers": "Authorization, Content-Type, X-Project-Id" }; let pending = false; let listReads = 0; let responses = 0; let record: InteractionRequest = { version: "agent-interaction-request@1", interaction_id: "locate-target", message_id: "question-message", turn_id: "question-turn", call_id: "ask-call", questions: [{ question_id: "target", title: "选择定位对象", body: "选择公司上报地址或模拟管段。", options: [{ id: "company", label: "公司上报地址" }, { id: "pipes", label: "模拟管段" }], mode: "single", required: true, allow_custom: true }], revision: 0, draft_revision: 0, draft: [], status: "pending", created_at: new Date().toISOString() }; await page.route(`**/api/v1/session/${sessionId}?**`, (route) => route.fulfill({ headers, json: { session_id: sessionId, title: "公司位置协作", purpose: "task_collaboration", status: "active", is_streaming: false, messages: [{ id: "context-message", role: "assistant", parts: [{ type: "text", text: "调查报告已就绪。" }] }, ...(record.delivery ? [{ id: record.delivery.native_message_id, role: "user", parts: [{ type: "text", text: "用户提交的提问回复:\n选择定位对象:公司上报地址;使用报告中的公司位置" }] }] : [])] } })); await page.route(`**/api/v1/session/${sessionId}/interactions**`, async (route) => { const request = route.request(); if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers }); const action = new URL(request.url()).pathname.split("/").at(-1)!; if (request.method() === "GET") { if (action === "interactions") listReads++; return route.fulfill({ headers, json: action === "interactions" ? { items: pending ? [record] : [] } : record }); } const body = request.postDataJSON(); if (action === "draft") record = { ...record, draft: body.answers, draft_revision: record.draft_revision + 1 }; if (action === "reply") record = { ...record, status: "answered", revision: 1, answers: body.answers.map((answer: { question_id: string; selected_option_ids: string[]; custom_text: string }) => ({ ...answer, selected_options: record.questions[0].options.filter((option) => answer.selected_option_ids.includes(option.id)) })), delivery: { native_message_id: `msg_${"a".repeat(32)}`, status: "queued", revision: 0 } }; if (action === "resume") record = { ...record, delivery: { ...record.delivery!, status: "queued", revision: record.delivery!.revision + 1 } }; return route.fulfill({ headers, json: record }); }); await page.route("**/api/v1/responses", (route) => { responses++; const body = route.request().postDataJSON(); if (responses === 1) { pending = true; if (responseMode === "pending_conflict") return route.fulfill({ headers, status: 409, json: { code: "INTERACTION_PENDING", message: "请先回答或取消当前提问。" } }); } else { expect(body.tjwater.interaction_id).toBe(record.interaction_id); expect(record.answers?.[0]).toMatchObject({ selected_option_ids: responseMode === "pending_conflict" ? ["company"] : [], custom_text: "使用报告中的公司位置" }); if (responseMode === "pending_conflict" && responses === 2) { record = { ...record, delivery: { ...record.delivery!, status: "blocked", revision: 1 } }; return route.fulfill({ headers, status: 503, body: "暂时无法继续,请重试。" }); } record = { ...record, delivery: { ...record.delivery!, status: "delivered", revision: record.delivery!.revision + 1 } }; } const write = responseFixtureWriter(sessionId); return route.fulfill({ headers: { ...headers, "Content-Type": "text/event-stream" }, body: [ { type: "start", messageId: responses === 1 ? "question-message" : "located-message" }, { type: "text-start", id: "text" }, { type: "text-delta", id: "text", delta: responses === 1 ? "请选择要定位的对象。" : "已定位公司上报地址。" }, { type: "text-end", id: "text" }, { type: "finish", finishReason: "stop" } ].map(write).join("") }); }); 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(); await expect.poll(() => listReads).toBeGreaterThan(0); await expect(panel.getByRole("article", { name: "需要补充的信息" })).toHaveCount(0); const readsBeforeSend = listReads; const input = panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }); await input.fill("定位到公司上报地址"); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); const card = panel.getByRole("article", { name: "需要补充的信息" }); await expect(card).toBeVisible(); expect(listReads).toBeGreaterThan(readsBeforeSend); await expect(panel.getByText("离线", { exact: true })).toHaveCount(0); await expect(panel.getByRole("button", { name: "引导当前 Agent 任务", exact: true })).toBeDisabled(); if (responseMode === "pending_conflict") await card.getByRole("radio", { name: "公司上报地址", exact: true }).check(); await card.getByRole("textbox", { name: "选择定位对象:补充说明" }).fill("使用报告中的公司位置"); await card.screenshot({ path: testInfo.outputPath(`recovered-${responseMode}.png`) }); await card.getByRole("button", { name: "提交回答", exact: true }).click(); const reply = panel.getByRole("group", { name: "已提交的回答" }); await expect(reply).toHaveCount(1); await expect(reply.getByText("已回答", { exact: true })).toBeVisible(); await expect(reply.getByText("使用报告中的公司位置", { exact: false })).toBeVisible(); if (responseMode === "pending_conflict") { await expect(reply.getByText("继续失败,可以重试", { exact: true })).toBeVisible(); await reply.getByRole("button", { name: "重试继续", exact: true }).click(); } await expect(reply.getByText("已继续处理", { exact: true })).toBeVisible(); await expect(card).toHaveCount(0); await expect(panel.getByText("用户提交的提问回复", { exact: false })).toHaveCount(0); expect(await reply.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1); await reply.screenshot({ path: testInfo.outputPath(`submitted-${responseMode}.png`) }); await reply.getByText("使用报告中的公司位置", { exact: true }).click({ clickCount: 3 }); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe("使用报告中的公司位置"); await expect(panel.getByText("已定位公司上报地址。", { exact: true })).toBeVisible(); expect(responses).toBe(responseMode === "pending_conflict" ? 3 : 2); await expect(reply).toHaveCount(1); await input.fill("继续核查"); await expect(panel.getByRole("button", { name: "发送 Agent 指令", exact: true })).toBeEnabled(); 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(reply.getByText("使用报告中的公司位置", { exact: false })).toBeVisible(); await expect(panel.getByText("用户提交的提问回复", { exact: false })).toHaveCount(0); await expect(card).toHaveCount(0); }); }