import { responsesFixture } from "./support/responses-fixture"; import { expect, test, type Locator } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; // Product display stays UTC+8 even when the browser itself is configured for UTC. test.use({ timezoneId: "UTC" }); const taskId = "22222222-2222-4222-8222-222222222222"; const runId = "33333333-3333-4333-8333-333333333333"; const projectId = "11111111-1111-4111-8111-111111111111"; const now = "2026-10-13T08:00:00Z"; const run = { run_id: runId, task_id: taskId, generation: 2, execution_request_id: runId, status: "succeeded", revision: 1, agent_session_id: "investigation-run", lease_expires_at: null, heartbeat_at: null, started_at: now, finished_at: now, failure_code: null, failure_detail: null, dispatch_attempts: 1, next_dispatch_at: now, created_at: now, updated_at: now }; const task = { project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint", lifecycle_status: "completed", active_run: null, latest_run: run, status: "completed", priority: "normal", revision: 1, execution_generation: 2, execution_request_id: runId, title: "居民异味投诉调查", summary: "现场记录待复核", source: { kind: "operator", external_id: null, external_revision: null, event_kind: "water_quality_complaint" }, trigger: { kind: "manual", occurred_at: now, detail: "居民异味投诉" }, scheduled_for: null, agent_session_id: "investigation-run", agent_lease_expires_at: null, current_plan_revision: null, current_report_revision: null, created_at: now, updated_at: now, latest_plan: null, latest_report: { revision: 3, execution_generation: 1, source_session_id: "investigation-run", published_at: now, published_by: "agent", report: { title: "居民异味投诉调查报告", current_conclusion: "现场记录待复核", conclusion_boundary: "仅覆盖已有监测记录", evidence_refs: ["monitor-1"], recommendations: [], pending_items: [], stage: "final", outcome: "concluded", stop_reason: null, data_gaps: [], diagram: null } }, latest_progress: null, evidence: [{ evidence_id: "monitor-1", kind: "system_data", summary: "上游监测数据质量已核对", source_label: "SCADA 水质监测", source_ref: "WQ-01", observed_at: now, recorded_at: now, recorded_by: "agent", has_snapshot: false }], actions: [], access_requests: [], work_order_snapshot: null, permissions: { can_publish_artifacts: false, can_propose_actions: false, can_sync_external_feedback: false, can_decide_work_orders: false, can_execute_work_orders: false, can_decide_commands: false, can_execute_commands: false, can_decide_access: false } }; test("证据提问在预览、展开及更多依据中使用统一实体按钮", async ({ page }, testInfo) => { await page.emulateMedia({ reducedMotion: "reduce" }); await page.route("http://127.0.0.1:8787/api/v1/**", (route) => route.fulfill({ json: { items: [] } })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.setContent('
'); await page.evaluate(async ({ fixture, cachePath }) => { const reactModule = await import(`${cachePath}/react.js`); const React = reactModule.default ?? reactModule; const clientModule = await import(`${cachePath}/react-dom_client.js`); const { createRoot } = clientModule.default ?? clientModule; const { InvestigationTaskReport } = await import("/src/features/workbench/components/investigation-task-report.tsx"); const evidence = Array.from({ length: 4 }, (_, index) => ({ ...fixture.evidence[0], evidence_id: `monitor-${index + 1}` })); createRoot(document.getElementById("report")).render(React.createElement(InvestigationTaskReport, { task: { ...fixture, evidence, latest_report: { ...fixture.latest_report, report: { ...fixture.latest_report.report, evidence_refs: evidence.map((item) => item.evidence_id) } } }, onContinue: () => undefined, renderAction: () => null, renderEvidence: (item: { summary: string }) => React.createElement("p", null, item.summary) })); }, { fixture: task, cachePath: `/node_modules/.vite-playwright-${process.env.PLAYWRIGHT_PORT || "5191"}/deps` }); const preview = page.getByRole("region", { name: "主要依据", exact: true }); await expect(page.getByText("任务更新于 2026/10/13 16:00:00", { exact: true })).toBeVisible(); await expect(preview).toContainText("2026/10/13 16:00:00"); for (const width of [1440, 320]) { await page.setViewportSize({ width, height: 900 }); const buttons = preview.getByRole("button", { name: "围绕此证据提问", exact: true }); await expect(buttons).toHaveCount(3); for (const button of await buttons.all()) { await expect(button).toHaveAttribute("data-slot", "button"); await expect(button).toHaveCSS("border-top-width", "1px"); await expect(button).toHaveCSS("height", "32px"); } await preview.screenshot({ path: testInfo.outputPath(`evidence-buttons-${width}.png`) }); await preview.getByRole("button", { name: "展开主要依据" }).click(); const dialog = page.getByRole("dialog"); await dialog.getByText("更多引用依据 · 1 条", { exact: true }).click(); const expandedButtons = dialog.getByRole("button", { name: "围绕此证据提问", exact: true }); await expect(expandedButtons).toHaveCount(4); for (const button of await expandedButtons.all()) { await expect(button).toBeVisible(); await expect(button).toHaveAttribute("data-slot", "button"); await expect(button).toHaveCSS("border-top-width", "1px"); expect(await button.evaluate((element) => element.getBoundingClientRect().right <= innerWidth)).toBe(true); } await dialog.screenshot({ path: testInfo.outputPath(`evidence-buttons-expanded-${width}.png`) }); await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0); } }); async function expectCenteredReference(panel: Locator) { await expect(panel).toHaveAttribute("data-agent-starting", "true"); await expect(panel.getByRole("group", { name: "对话引用" })).toBeVisible(); await expect(panel.getByRole("group", { name: "推荐问题" })).toHaveCount(0); await expect(panel.locator("form")).toHaveCount(1); await expect.poll(async () => { const panelBox = (await panel.boundingBox())!; const header = (await panel.locator("header").boundingBox())!; const composer = (await panel.locator("form").boundingBox())!; // The mobile header extends above the panel to cover the drawer handle. const center = (header.y + header.height + panelBox.y + panelBox.height) / 2; return Math.abs(composer.y + composer.height / 2 - center); }).toBeLessThan(2); } test("单一协作入口展示任务引用,保留草稿且仅手动发送,并适配窄屏", async ({ page }, testInfo) => { await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page, { sessions: [{ session_id: "ordinary-main", title: "供水核查主聊天", purpose: "ordinary_chat", created_at: now, updated_at: now }] }); let submitted = 0; let inboxReads = 0; let incoming = false; page.on("request", (request) => { if (request.method() === "POST" && (new URL(request.url()).pathname.endsWith("/responses") || new URL(request.url()).pathname.endsWith("/investigations"))) submitted += 1; }); const { latest_plan, latest_report, latest_progress, evidence, actions, access_requests, work_order_snapshot, permissions, ...summary } = task; void latest_plan; void latest_report; void latest_progress; void evidence; void actions; void access_requests; void work_order_snapshot; void permissions; await page.route("**/workbench/sync-snapshot", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { schema: "workbench-sync-snapshot@1", event_cursor: 0, runtime: { availability: "not_configured", source: "none", scenario_availability: "not_configured", artifact_availability: "not_configured", schedule_availability: "not_configured", work_order_availability: "not_configured", alert_availability: "not_configured", generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: [] }, tasks: [summary], notifications: [], generated_at: now } })); await page.route("**/operational-tasks/**", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: task })); await page.route("**/agent-messages?**", (route) => { inboxReads += 1; return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { items: incoming ? [{ message_id: "msg1", task_id: taskId, run_id: runId, source_session_id: "investigation-run", target_session_id: "ordinary-main", direction: "to_chat", text: "已核对上游监测,请补充现场采样记录。", status: "pending", in_reply_to: null, created_at: now, failure_detail: null }] : [], next_cursor: null } }); }); await page.route("**/agent-messages/stream", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, contentType: "text/event-stream", body: 'event: agent_message\ndata: {"session_id":"ordinary-main"}\n\n' })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const agent = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(agent.getByRole("heading", { name: "我已就绪,请描述任务" })).toHaveCount(0); await agent.getByRole("button", { name: "打开 Agent 历史记录" }).click(); await agent.getByRole("button", { name: /供水核查主聊天/ }).click(); const prompt = agent.locator("textarea"); await prompt.fill("保留的现场补充草稿"); await page.getByRole("button", { name: "工况任务", exact: true }).click(); const feed = page.getByRole("region", { name: "工况任务", exact: true }); const expand = feed.getByRole("button", { name: "展开工况任务" }); if (await expand.isVisible()) await expand.click(); await feed.getByRole("button", { name: /调查任务/ }).click(); await feed.getByText("居民异味投诉调查", { exact: true }).first().click(); await expect(feed.getByText("16:00", { exact: true })).toBeVisible(); await expect(feed.getByRole("region", { name: "调查任务标识" })).toHaveCount(0); await expect(feed.getByRole("button", { name: "在主聊天中编辑" })).toHaveCount(0); await expect(feed).not.toContainText(taskId); await expect(feed).not.toContainText(runId); await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveCount(1); await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveAttribute("title", "打开调查协作对话,引用此任务,不会直接发送。"); await feed.screenshot({ path: testInfo.outputPath("investigation-entry-desktop.png") }); await feed.getByRole("button", { name: "Agent 协作", exact: true }).click(); await expect(agent.getByText("任务协作问答", { exact: true }).first()).toBeVisible(); const reference = agent.getByRole("group", { name: "对话引用" }); await expect(reference).toContainText("居民异味投诉调查"); await expect(reference).toContainText("第 1 轮 · 报告第 3 版"); await expect(reference).not.toContainText(/当前|第 2 轮|已结束|调查完成|正在调查/); await expect(prompt).toHaveAttribute("placeholder", "可询问调查进展、讨论报告或补充信息"); await expect(prompt).toHaveValue("保留的现场补充草稿"); await expect(prompt).toBeFocused(); expect(submitted).toBe(0); await feed.getByRole("button", { name: "Agent 协作", exact: true }).click(); await expect(prompt).toBeFocused(); await expect(prompt).toHaveValue("保留的现场补充草稿"); await feed.getByLabel("主要依据").getByRole("button", { name: "围绕此证据提问" }).click(); await expect(prompt).toHaveValue("保留的现场补充草稿\n\n请解释“上游监测数据质量已核对”这条证据的范围、来源和判断影响。"); await expect(prompt).toBeFocused(); await expect(reference).toContainText("已引用证据"); await expectCenteredReference(agent); expect(submitted).toBe(0); await agent.screenshot({ path: testInfo.outputPath("investigation-reference-desktop.png") }); await feed.getByRole("button", { name: /定时任务/ }).click(); await page.setViewportSize({ width: 375, height: 900 }); await page.getByRole("button", { name: "打开 Agent 面板", exact: true }).click(); const mobileAgent = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(mobileAgent.getByRole("heading", { name: "我已就绪,请描述任务" })).toHaveCount(0); await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toContainText("居民异味投诉调查"); await expectCenteredReference(mobileAgent); await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-reference-narrow.png") }); await page.setViewportSize({ width: 320, height: 900 }); await expectCenteredReference(mobileAgent); await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-reference-320.png") }); expect(await mobileAgent.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(true); await page.setViewportSize({ width: 375, height: 900 }); let sentContext: unknown; await page.route("**/api/v1/responses", async (route) => { if (route.request().method() !== "POST") return route.fallback(); sentContext = route.request().postDataJSON().tjwater.task_context; expect(route.request().postDataJSON().conversation).toBe(`collaboration-${taskId}`); await route.fulfill({ headers: { "Access-Control-Allow-Origin": "*", "X-Vercel-AI-UI-Message-Stream": "v1" }, contentType: "text/event-stream", body: responsesFixture([{ type: "start", messageId: "reply" }, { type: "finish", finishReason: "stop" }]) }); }); expect(submitted).toBe(0); await mobileAgent.getByRole("button", { name: "发送 Agent 指令" }).click(); await expect.poll(() => sentContext).toEqual({ report_revision: 3, evidence_id: "monitor-1" }); expect(submitted).toBe(1); await expect(mobileAgent).toHaveAttribute("data-agent-starting", "false"); await expect(mobileAgent.locator(".agent-panel-user-message")).toContainText("保留的现场补充草稿"); await expect.poll(async () => { const panelBox = (await mobileAgent.boundingBox())!; const composer = (await mobileAgent.locator("form").boundingBox())!; return panelBox.y + panelBox.height - composer.y - composer.height; }).toBeLessThan(32); await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0); await mobileAgent.getByRole("button", { name: "打开 Agent 历史记录" }).click(); await mobileAgent.getByRole("button", { name: /供水核查主聊天/ }).click(); await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toHaveCount(0); incoming = true; await expect(mobileAgent.getByText("已核对上游监测,请补充现场采样记录。")).toBeVisible(); await expect(mobileAgent.getByText("待接收", { exact: true })).toBeVisible(); await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0); expect(inboxReads).toBeGreaterThan(1); await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-inbox-narrow.png") }); expect(await mobileAgent.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(true); expect(submitted).toBe(1); });