import { expect, test } from "@playwright/test"; import type { OperationalTask, OperationalTaskSummary } from "../../src/features/workbench/api/operational-task-client"; import { mockAgentApi } from "./support/mock-agent-api"; const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e"; const taskId = "82000000-0000-4000-8000-000000000001"; const actionId = "83000000-0000-4000-8000-000000000001"; const accessId = "84000000-0000-4000-8000-000000000001"; const now = "2026-09-24T08:00:00Z"; test("动态调查显示选定路线与嵌套并行,并完成授权和工单草稿确认", async ({ page }, testInfo) => { test.setTimeout(90_000); await mockAgentApi(page); await page.route("**/runtime-config.js", (route) => route.fulfill({ contentType: "text/javascript", body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({ TJWATER_AUTH_MODE: "disabled", TJWATER_SERVER_API_BASE_URL: "http://server.test", TJWATER_MAP_URL: "http://map.test/geoserver", TJWATER_GEOSERVER_WORKSPACE: "tjwater_next" })}` })); await page.route("http://server.test/api/v1/projects**", (route) => route.fulfill({ json: { items: [{ project_id: projectId, gs_workspace: "tjwater_next", status: "active" }], total: 1, limit: 1000, offset: 0 } })); await page.route("http://server.test/api/v1/scada-devices**", (route) => route.fulfill({ json: { items: [], total: 0, limit: 1000, offset: 0 } })); await page.route("http://map.test/**", (route) => route.fulfill({ status: 204 })); const planNode = (id: string, title: string, kind: "activity" | "decision" | "parallel_split" | "parallel_join" | "gate", status: "completed" | "waiting" | "skipped") => ({ id, title, owner: "agent" as const, status, detail: `${title}的执行记录`, kind, evidence_refs: [], action_refs: [], selected_edge_id: null, gate_ref: null, revisits_node_id: null }); const plan = { schema: "execution-plan@2" as const, title: "水质投诉动态调查", reason: "按投诉位置和风险规划", nodes: [ { ...planNode("risk", "判断紧急风险", "decision", "completed"), selected_edge_id: "urgent-route" }, planNode("urgent", "立即现场核查", "activity", "completed"), planNode("routine", "常规现场核查", "activity", "skipped"), planNode("split", "同步核对现场与数据", "parallel_split", "completed"), planNode("field", "现场采样", "activity", "waiting"), planNode("data-split", "多源数据核对", "parallel_split", "completed"), planNode("upstream", "上游监测", "activity", "completed"), planNode("network", "管网运行", "activity", "completed"), planNode("data-join", "数据证据汇合", "parallel_join", "completed"), planNode("join", "现场与数据汇合", "parallel_join", "waiting"), { ...planNode("gate", "确认采样工单", "gate", "waiting"), gate_ref: actionId } ], edges: [ { id: "urgent-route", from: "risk", to: "urgent", condition: "有紧急风险" }, { id: "routine-route", from: "risk", to: "routine", condition: "无紧急风险" }, { id: "e1", from: "urgent", to: "split", condition: null }, { id: "e2", from: "split", to: "field", condition: "现场" }, { id: "e3", from: "split", to: "data-split", condition: "Agent 数据" }, { id: "e4", from: "field", to: "join", condition: null }, { id: "e5", from: "data-split", to: "upstream", condition: "上游" }, { id: "e6", from: "data-split", to: "network", condition: "管网" }, { id: "e7", from: "upstream", to: "data-join", condition: null }, { id: "e8", from: "network", to: "data-join", condition: null }, { id: "e9", from: "data-join", to: "join", condition: null }, { id: "e10", from: "join", to: "gate", condition: null } ] }; let task: OperationalTask = { project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint", lifecycle_status: "waiting_human", active_run: null, status: "waiting_human", priority: "urgent", revision: 2, execution_generation: 1, execution_request_id: "85000000-0000-4000-8000-000000000001", 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: "dynamic-flow-session", agent_lease_expires_at: null, current_plan_revision: 1, current_report_revision: null, created_at: now, updated_at: now, latest_plan: { revision: 1, execution_generation: 1, source_session_id: "dynamic-flow-session", published_at: now, published_by: "agent", plan }, latest_report: null, evidence: [], access_requests: [{ access_request_id: accessId, task_id: taskId, node_id: "data-split", scopes: ["scada_devices", "scada_readings"], reason: "需要核对上游设备和读数", status: "pending", approved_scopes: [], version: 1, requested_by: "agent", requested_at: now, decided_by: null, decided_at: null, comment: null }], actions: [{ action_id: actionId, task_id: taskId, version: 1, kind: "create_work_order", node_id: "gate", reason: "现场采样需要派单", payload: { title: "旧采样草稿", description: "现场采样", work_order_type: "water_quality_complaint", priority: "urgent", location: "演示小区" }, evidence_refs: [], required_approval_permission: "operational_action.work_order.approve", required_execution_permission: "operational_action.work_order.execute", approval_status: "pending", execution_status: "not_submitted", execution_request_id: null, proposed_by: "agent", proposed_at: now, decided_by: null, decided_at: null, decision_comment: null, executed_by: null, executed_at: null, receipt: null, execution_detail: null }], permissions: { can_publish_artifacts: false, can_propose_actions: false, can_sync_external_feedback: false, can_decide_work_orders: true, can_execute_work_orders: true, can_decide_commands: false, can_execute_commands: false, can_decide_access: true }, work_order_snapshot: null }; const summary = (item: OperationalTask): OperationalTaskSummary => { const { latest_plan, latest_report, evidence, actions, access_requests, permissions, work_order_snapshot, ...value } = item; void latest_plan; void latest_report; void evidence; void actions; void access_requests; void permissions; void work_order_snapshot; return value; }; await page.route("http://server.test/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ 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(task)], notifications: [], generated_at: now } })); await page.route("http://server.test/api/v1/workbench/events**", (route) => route.fulfill({ contentType: "text/event-stream", body: ": keepalive\n\n" })); await page.route("http://server.test/api/v1/operational-task-notifications**", (route) => route.fulfill({ json: { items: [], limit: 100, offset: 0, next_offset: null } })); await page.route("http://server.test/api/v1/operational-tasks**", async (route) => { const path = new URL(route.request().url()).pathname; const method = route.request().method(); if (path.endsWith("/operational-tasks/schedules")) return route.fulfill({ json: { items: [] } }); if (method === "GET" && path.endsWith("/operational-tasks")) return route.fulfill({ json: { items: [summary(task)], limit: 50, offset: 0, next_offset: null } }); if (method === "GET" && path.endsWith(`/operational-tasks/${taskId}`)) return route.fulfill({ json: task }); if (method === "POST" && path.endsWith(`/access-requests/${accessId}/decision`)) { const body = route.request().postDataJSON(); expect(body.approved_scopes).toEqual(["scada_devices"]); const access = { ...task.access_requests[0], version: 2, status: "approved" as const, approved_scopes: body.approved_scopes, decided_by: "reviewer", decided_at: now }; task = { ...task, access_requests: [access] }; return route.fulfill({ json: access }); } if (method === "PATCH" && path.endsWith(`/actions/${actionId}/draft`)) { const body = route.request().postDataJSON(); expect(body.payload.title).toBe("现场采样修订稿"); const action = { ...task.actions[0], version: 2, payload: body.payload, edited_by: "reviewer", edited_at: now }; task = { ...task, actions: [action] }; return route.fulfill({ json: action }); } if (method === "POST" && path.endsWith(`/actions/${actionId}/confirm-dispatch`)) { const body = route.request().postDataJSON(); expect(body.expected_version).toBe(2); const action = { ...task.actions[0], version: 4, approval_status: "approved" as const, execution_status: "succeeded" as const, decided_by: "reviewer", decided_at: now, executed_by: "reviewer", executed_at: now, execution_request_id: "86000000-0000-4000-8000-000000000001", receipt: { adapter: "work-order", receipt_id: "WO-20260924-1", submitted_at: now, completed_at: now, external_status: "completed", detail: "采样工单已创建" } }; task = { ...task, actions: [action] }; return route.fulfill({ json: action }); } return route.fulfill({ status: 404, json: {} }); }); await page.goto("/"); await page.getByRole("button", { name: "工况任务", exact: true }).click(); const panel = page.getByRole("region", { name: "工况任务", exact: true }); const expand = panel.getByRole("button", { name: "展开工况任务" }); if (await expand.isVisible()) await expand.click(); await panel.getByRole("button", { name: /调查任务/ }).click(); await panel.getByText("水质投诉动态调查", { exact: true }).first().click(); await expect(panel.getByText(/路线选择:/).first().locator("..")).toContainText("有紧急风险"); await expect(panel.getByLabel("并行处理 2 路")).toHaveCount(2); await expect(panel.locator('[data-execution-node="data-join"]')).toHaveCount(1); await expect(panel.getByText("其他规划路线 1 项")).toBeVisible(); await testInfo.attach("dynamic-investigation-desktop", { body: await panel.screenshot({ path: testInfo.outputPath("dynamic-investigation-desktop.png") }), contentType: "image/png" }); await panel.getByRole("button", { name: "Agent 协作" }).click(); const agentPanel = page.getByRole("complementary", { name: "Agent 命令面板" }); const review = agentPanel.getByRole("region", { name: "当前任务审核" }); await expect(review).toContainText("需要核对上游设备和读数"); await page.setViewportSize({ width: 375, height: 720 }); const mobileAgentSheet = page.getByRole("region", { name: "Agent 工作台抽屉" }); if (!await mobileAgentSheet.isVisible()) await page.getByRole("button", { name: "打开 Agent 面板" }).click(); await expect(mobileAgentSheet.getByRole("region", { name: "当前任务审核" })).toBeVisible(); await page.setViewportSize({ width: 1280, height: 720 }); await expect(review).toBeVisible(); await review.getByLabel("SCADA 监测读数").uncheck(); await review.getByRole("button", { name: "批准所选范围" }).click(); await expect(panel.getByText("已授权:SCADA 设备")).toBeVisible(); await review.getByRole("button", { name: "编辑草稿" }).click(); await review.getByRole("textbox", { name: "工单标题" }).fill("现场采样修订稿"); await review.getByRole("button", { name: "保存草稿" }).click(); await expect(panel.getByText("现场采样修订稿").first()).toBeVisible(); await review.getByRole("button", { name: "确认并下发工单" }).click(); await expect(panel.getByRole("region", { name: "执行回执" })).toContainText("WO-20260924-1"); for (const width of [375, 320]) { await page.setViewportSize({ width, height: 720 }); const mobilePanel = page.getByRole("region", { name: "工况任务", exact: true }); if (!await mobilePanel.isVisible()) await page.getByRole("button", { name: "打开工况任务", exact: true }).click(); const sheet = page.getByRole("region", { name: "工况任务抽屉" }); if (await sheet.getAttribute("data-snap") === "half") { await page.getByRole("button", { name: /工况任务抽屉高度/ }).click(); } await expect(sheet).toHaveAttribute("data-snap", "full"); const mobileExpand = mobilePanel.getByRole("button", { name: "展开工况任务" }); if (await mobileExpand.isVisible()) await mobileExpand.click(); await mobilePanel.getByRole("button", { name: /调查任务/ }).click(); await expect(mobilePanel.getByLabel("并行处理 2 路")).toHaveCount(2); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await mobilePanel.locator('[data-execution-node="split"]').scrollIntoViewIfNeeded(); await testInfo.attach(`dynamic-investigation-${width}`, { body: await sheet.screenshot({ path: testInfo.outputPath(`dynamic-investigation-${width}.png`) }), contentType: "image/png" }); } });