import { expect, test } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; test.describe("外围工单测试", () => { test.beforeEach(async ({ page }) => { 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: "324ff7ee-c2d4-5184-a84b-48fb14a2933e", 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 })); }); test("外围面板只读取工单能力,不再请求监测回放与测试数据", async ({ page }) => { const requestedPaths: string[] = []; 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_PERIPHERAL_SAMPLE_API_BASE_URL: "http://peripheral.test", TJWATER_PERIPHERAL_SAMPLE_TIME_MODE: "demo" })};` }) ); await page.route("http://server.test/api/v1/projects**", (route) => route.fulfill({ json: { items: [ { project_id: "324ff7ee-c2d4-5184-a84b-48fb14a2933e", gs_workspace: "tjwater_next", status: "active" } ], total: 1, limit: 1000, offset: 0 } }) ); const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e"; await page.route("http://peripheral.test/api/sample/v1/**", async (route) => { const url = new URL(route.request().url()); requestedPaths.push(url.pathname); if (url.pathname.endsWith("/context")) return route.fulfill({ json: { protocol_version: "tjwater-peripheral-sample@1", origin: "synthetic", project_id: projectId, user_id: "50000000-0000-4000-8000-000000000001", can_write: true, scenarios: ["normal"], managed_resources: { work_orders: "available", test_data: "available" }, state_transitions: { pending: ["processing"], processing: ["pending_review"], pending_review: ["processing", "completed"], completed: [] } } }); if (url.pathname.endsWith("/work-orders")) return route.fulfill({ json: { items: createSourceWorkOrders(projectId), next_cursor: null } }); return route.fulfill({ status: 404, json: { code: "NOT_FOUND", message: "not found", effect: "none", request_id: null } }); }); await page.goto("/"); await page.getByRole("button", { name: "外围样板管理" }).click(); const panel = page.getByRole("complementary", { name: "外围工单测试" }); await expect(panel).toBeVisible(); await expect(panel.getByText("工单接口测试", { exact: true })).toBeVisible(); await expect(panel.getByText("历史监测回放", { exact: true })).toHaveCount(0); await expect(panel.getByText("测试数据", { exact: true })).toHaveCount(0); const workOrderList = page.getByLabel("工单列表"); await expect(workOrderList).toContainText("合成通信核查"); await expect(workOrderList).toContainText("合成压力偏差复核"); await expect(workOrderList).toContainText("合成零流量样本确认"); await expect(workOrderList).toContainText("待处理"); await expect(workOrderList).toContainText("处理中"); await expect(workOrderList).toContainText("已完成"); await expect(workOrderList.getByText("未提供过程", { exact: true })).toHaveCount(3); expect(requestedPaths).toEqual(expect.arrayContaining([ "/api/sample/v1/context", "/api/sample/v1/work-orders" ])); expect(requestedPaths.some((path) => path.includes("/monitoring/") || path.endsWith("/test-data") || path.endsWith("/demo-data"))).toBe(false); }); test("未配置时从顶栏进入且不暗连外围服务", async ({ page }) => { const peripheralRequests: string[] = []; page.on("request", (request) => { if (request.url().includes("/api/sample/v1")) peripheralRequests.push(request.url()); }); await page.goto("/"); await page.getByRole("button", { name: "外围样板管理" }).click(); await expect(page.getByRole("complementary", { name: "外围工单测试" })).toBeVisible(); await expect( page .getByRole("complementary", { name: "外围工单测试" }) .getByText("仅用于验证外围工单接口与同步过程,不代表真实现场处置", { exact: true }) .last() ).toBeVisible(); await expect(page.getByText(/当前不会连接外围工单服务/)).toBeVisible(); expect(peripheralRequests).toEqual([]); }); test("管理面板打开时阻止地图接收鼠标行为", async ({ page }) => { await page.goto("/"); await page.locator(".maplibregl-canvas-container").evaluate((element) => { const probeWindow = window as Window & { __mapPointerEvents?: string[] }; probeWindow.__mapPointerEvents = []; for (const type of ["pointerdown", "wheel"] as const) { element.addEventListener(type, () => probeWindow.__mapPointerEvents?.push(type), true); } }); const expectMapInteractionBlocked = async (panelName: string) => { const panel = page.getByRole("complementary", { name: panelName }); const [panelBox, canvasBox] = await Promise.all([ panel.boundingBox(), page.locator(".maplibregl-canvas").boundingBox() ]); expect(panelBox).not.toBeNull(); expect(canvasBox).not.toBeNull(); const x = panelBox!.x - 24; const y = panelBox!.y + 120; expect(x).toBeGreaterThan(canvasBox!.x); expect(y).toBeLessThan(canvasBox!.y + canvasBox!.height); await expect .poll(() => page.evaluate( ({ x: pointX, y: pointY }) => document.elementFromPoint(pointX, pointY)?.hasAttribute("data-map-interaction-shield"), { x, y } ) ) .toBe(true); await page.mouse.click(x, y); await page.mouse.wheel(0, 120); await expect .poll(() => page.evaluate( () => (window as Window & { __mapPointerEvents?: string[] }).__mapPointerEvents ) ) .toEqual([]); }; await page.getByRole("button", { name: "外围样板管理" }).click(); await expect(page.getByRole("complementary", { name: "外围工单测试" })).toBeVisible(); await expectMapInteractionBlocked("外围工单测试"); await page.getByRole("button", { name: "外围样板管理" }).click(); await page.getByRole("button", { name: "项目分析成果" }).click(); await expect(page.getByRole("complementary", { name: "项目分析成果" })).toBeVisible(); await expectMapInteractionBlocked("项目分析成果"); }); test("增量存储未供给时保留原有外围能力且不请求缺表资源", async ({ page }) => { const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e"; const managedRequests: string[] = []; 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_PERIPHERAL_SAMPLE_API_BASE_URL: "http://peripheral.test" })}` })); await page.route("http://peripheral.test/api/sample/v1/**", (route) => { const path = new URL(route.request().url()).pathname; if (path.includes("/work-orders") || path.includes("/test-data")) { managedRequests.push(path); return route.fulfill({ status: 503, json: {} }); } if (path.endsWith("/context")) return route.fulfill({ json: { protocol_version: "tjwater-peripheral-sample@1", origin: "synthetic", project_id: projectId, user_id: "50000000-0000-4000-8000-000000000001", can_write: true, scenarios: ["normal"], managed_resources: { work_orders: "not_configured", test_data: "not_configured" }, state_transitions: { pending: [], processing: [], pending_review: [], completed: [] } } }); if (path.endsWith("/scenario")) return route.fulfill({ json: { scenario: "normal", revision: 1 } }); if (path.endsWith("/tasks")) return route.fulfill({ json: { items: [], next_cursor: null } }); if (path.endsWith("/monitoring/points")) return route.fulfill({ json: { items: [], next_cursor: null, origin: "historical_replay", source_id: "tjwater_v2" } }); if (path.endsWith("/monitoring/alerts")) return route.fulfill({ json: { items: [], availability: "not_configured", reason: "告警源未配置", origin: "historical_replay", source_id: "tjwater_v2" } }); return route.fulfill({ status: 404, json: {} }); }); await page.goto("/"); await page.getByRole("button", { name: "外围样板管理" }).click(); const panel = page.getByRole("complementary", { name: "外围工单测试" }); await expect(panel.getByText("测试工单存储待供给", { exact: true })).toBeVisible(); await expect(panel.getByText("测试数据存储待供给", { exact: true })).toHaveCount(0); await expect(panel.getByText("历史监测回放", { exact: true })).toHaveCount(0); expect(managedRequests).toEqual([]); }); test("创建编辑真实过程、刷新外部更新并软删除预约工单", async ({ page }, testInfo) => { test.setTimeout(60_000); const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e"; let workOrders: Record[] = []; const writes: Record[] = []; 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_PERIPHERAL_SAMPLE_API_BASE_URL: "http://peripheral.test" })}` })); await page.route("http://peripheral.test/api/sample/v1/**", async (route) => { const request = route.request(); const url = new URL(request.url()); const path = url.pathname; const now = "2026-09-18T00:00:00Z"; if (path.endsWith("/context")) return route.fulfill({ json: { protocol_version: "tjwater-peripheral-sample@1", origin: "synthetic", project_id: projectId, user_id: "50000000-0000-4000-8000-000000000001", can_write: true, scenarios: ["normal"], managed_resources: { work_orders: "available", test_data: "available" }, state_transitions: { pending: ["processing"], processing: ["pending_review"], pending_review: ["processing", "completed"], completed: [] } } }); if (path.endsWith("/work-orders") && request.method() === "GET") return route.fulfill({ json: { items: workOrders, next_cursor: null } }); if (path.endsWith("/work-orders") && request.method() === "POST") { const body = request.postDataJSON(); writes.push(body); const item = { ...body, work_order_id: "50000000-0000-4000-8000-000000000001", project_id: projectId, revision: 1, origin: "synthetic", created_at: now, updated_at: now, created_by: "tester", updated_by: "tester" }; delete item.request_id; workOrders = [item]; return route.fulfill({ status: 201, json: item }); } if (path.includes("/work-orders/") && request.method() === "PATCH") { const body = request.postDataJSON(); writes.push(body); const next = { ...workOrders[0], ...body, revision: 2, updated_at: "2026-09-18T01:00:00Z" }; if (body.clear_assignee) next.assignee = null; if (body.clear_location) next.location = null; if (body.clear_scenario_code) next.scenario_code = null; if (body.clear_scheduled_at) next.scheduled_at = null; if (body.clear_subtasks) next.subtasks = null; for (const key of ["request_id", "expected_revision", "clear_assignee", "clear_location", "clear_scenario_code", "clear_scheduled_at", "clear_subtasks"]) delete next[key]; workOrders = [next]; return route.fulfill({ json: workOrders[0] }); } if (path.includes("/work-orders/") && request.method() === "DELETE") { workOrders = []; return route.fulfill({ json: { resource_type: "work_order", resource_id: path.split("/").at(-1), revision: 3, deleted: true } }); } return route.fulfill({ status: 404, json: { code: "NOT_FOUND", message: "not found", effect: "none", request_id: null } }); }); await page.goto("/"); await page.getByRole("button", { name: "外围样板管理" }).click(); const panel = page.getByRole("complementary", { name: "外围工单测试" }); await expect(panel.getByText("工单接口测试", { exact: true })).toBeVisible(); await expect(panel.getByLabel("工单列表")).toContainText("暂无测试工单"); await expect(panel.getByRole("textbox", { name: "标题" })).toBeHidden(); await panel.getByRole("button", { name: "新增预约工单" }).click(); let editor = panel.getByRole("region", { name: "新增预约工单" }); await editor.getByRole("textbox", { name: "标题" }).fill("合成预约维修"); await editor.getByRole("textbox", { name: "说明", exact: true }).fill("验证外围过程结构同步"); await editor.getByRole("textbox", { name: "负责人", exact: true }).fill("测试班组甲"); await editor.getByRole("textbox", { name: "位置", exact: true }).fill("联调测试点西侧"); await editor.getByRole("textbox", { name: "场景编号" }).fill("APPOINTMENT-TEST-001"); await editor.getByRole("textbox", { name: "责任班组" }).fill("测试班组甲"); await editor.getByRole("textbox", { name: "处置位置" }).fill("联调测试点西侧"); await editor.getByRole("textbox", { name: "阶段说明", exact: true }).first().fill("确认班组甲和西侧作业窗口。"); await editor.getByRole("button", { name: "创建预约工单" }).click(); await expect(panel.getByText("合成预约维修", { exact: true })).toBeVisible(); expect(writes[0]).toMatchObject({ work_order_type: "general", assignee: "测试班组甲", location: "联调测试点西侧", scenario_code: "APPOINTMENT-TEST-001", subtasks: expect.arrayContaining([expect.objectContaining({ assignee: "测试班组甲", location: "联调测试点西侧", stages: expect.arrayContaining([expect.objectContaining({ description: "确认班组甲和西侧作业窗口。" })]) })]) }); await panel.getByRole("button", { name: "编辑工单 合成预约维修" }).click(); editor = panel.getByRole("region", { name: "编辑工单 合成预约维修" }); await editor.getByRole("textbox", { name: "标题" }).fill("合成预约维修(已对齐)"); await editor.getByRole("textbox", { name: "负责人", exact: true }).fill("测试班组乙"); await editor.getByRole("textbox", { name: "位置", exact: true }).fill("联调测试点东侧"); await editor.getByRole("textbox", { name: "责任班组" }).fill("测试班组乙"); await editor.getByRole("textbox", { name: "处置位置" }).fill("联调测试点东侧"); await editor.getByRole("textbox", { name: "阶段说明", exact: true }).first().fill("确认班组乙和东侧作业窗口,等待真实反馈。"); await editor.getByRole("combobox", { name: "工单状态" }).click(); await page.getByRole("option", { name: "待复令", exact: true }).click(); await editor.getByRole("button", { name: "保存工单与过程" }).click(); await expect(panel.getByText("合成预约维修(已对齐)", { exact: true })).toBeVisible(); expect(writes[1]).toMatchObject({ status: "awaiting_reply", assignee: "测试班组乙", location: "联调测试点东侧", clear_assignee: false, clear_location: false, clear_subtasks: false, subtasks: expect.arrayContaining([expect.objectContaining({ assignee: "测试班组乙", location: "联调测试点东侧", stages: expect.arrayContaining([expect.objectContaining({ description: "确认班组乙和东侧作业窗口,等待真实反馈。" })]) })]) }); expect(workOrders[0]).toMatchObject({ work_order_type: "general", scenario_code: "APPOINTMENT-TEST-001" }); workOrders = [{ ...workOrders[0], title: "外部系统更新后的预约维修", revision: 3 }]; await panel.getByRole("button", { name: "刷新外围工单" }).click(); await expect(panel.getByText("外部系统更新后的预约维修", { exact: true })).toBeVisible(); await panel.getByRole("button", { name: "编辑工单 外部系统更新后的预约维修" }).click(); const refreshedEditor = panel.getByRole("region", { name: "编辑工单 外部系统更新后的预约维修" }); await expect(refreshedEditor.getByRole("textbox", { name: "负责人", exact: true })).toHaveValue("测试班组乙"); await expect(refreshedEditor.getByRole("textbox", { name: "处置位置" })).toHaveValue("联调测试点东侧"); await expect(refreshedEditor.getByRole("textbox", { name: "阶段说明", exact: true }).first()).toHaveValue("确认班组乙和东侧作业窗口,等待真实反馈。"); for (const width of [1280, 375, 320]) { await page.setViewportSize({ width, height: 780 }); await refreshedEditor.scrollIntoViewIfNeeded(); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await testInfo.attach(`peripheral-work-order-process-${width}`, { body: await page.screenshot({ path: `/tmp/tjwater-operational-ui-review/peripheral-work-order-edit-${width}.png` }), contentType: "image/png" }); } await page.setViewportSize({ width: 1280, height: 780 }); await refreshedEditor.getByRole("button", { name: "关闭编辑表单" }).click(); await panel.getByRole("button", { name: "删除工单 外部系统更新后的预约维修" }).click(); await expect(panel.getByText("外部系统更新后的预约维修", { exact: true })).toBeHidden(); await page.route("http://server.test/api/v1/operational-tasks", (route) => route.fulfill({ status: 503, json: { detail: "operational task test stub" } }) ); await panel.getByRole("button", { name: "投诉与爆管上报" }).click(); await panel.getByRole("combobox", { name: "事件类型" }).click(); await page.getByRole("option", { name: "爆管上报", exact: true }).click(); await panel.getByRole("textbox", { name: "事件位置" }).fill("演示路与样板路交叉口"); await panel.getByRole("textbox", { name: "现场描述" }).fill("路面积水并伴随压力突降"); await panel.getByRole("button", { name: "创建事件工单" }).click(); await expect(panel.getByText("爆管上报", { exact: true }).last()).toBeVisible(); expect(workOrders[0]).toMatchObject({ work_order_type: "burst_report", scenario_code: "BURST-REPORT-001", title: "爆管上报:演示路与样板路交叉口", status: "open", priority: "urgent" }); }); test("创建水质投诉工单后进入结构化工况任务并由用户打开 Agent", async ({ page }, testInfo) => { test.setTimeout(90_000); const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e"; const workOrderId = "50000000-0000-4000-8000-000000000099"; const agentRequests: Array> = []; let workOrders: Record[] = []; const plan = { schema: "execution-plan@1", title: "水质投诉调查计划", reason: "先完成四路并行核查,汇合后再分两路核实", nodes: [ { id: "accept", title: "接收投诉工单", status: "completed", owner: "agent", detail: "已关联合成工单", evidence_refs: ["e-1"], action_refs: [] }, { id: "upstream", title: "校验上游水质监测", status: "completed", owner: "agent", detail: "上游三个点位指标正常", evidence_refs: ["e-1"], action_refs: [] }, { id: "upstream-review", title: "复核上游趋势", status: "completed", owner: "agent", detail: "过去两小时趋势稳定", evidence_refs: ["e-1"], action_refs: [] }, { id: "nearby", title: "核对邻近投诉", status: "running", owner: "agent", detail: "检查投诉空间分布", evidence_refs: [], action_refs: [] }, { id: "pressure", title: "比对压力波动", status: "completed", owner: "agent", detail: "泵房出水压力稳定", evidence_refs: [], action_refs: [] }, { id: "hydraulic", title: "校核水力路径", status: "running", owner: "agent", detail: "追踪供水路径", evidence_refs: [], action_refs: [] }, { id: "valve", title: "排查阀门操作", status: "pending", owner: "agent", detail: "等待水力路径结果", evidence_refs: [], action_refs: [] }, { id: "maintenance", title: "核对运维记录", status: "waiting", owner: "human", detail: "等待近期冲洗记录", evidence_refs: [], action_refs: [] }, { id: "stage-judgment", title: "阶段性判断", status: "completed", owner: "agent", detail: "疑似二次供水环节问题,待现场核验。", evidence_refs: ["e-1"], action_refs: [] }, { id: "secondary", title: "核查二次供水", status: "running", owner: "agent", detail: "核对二次供水设施", evidence_refs: [], action_refs: [] }, { id: "tank-record", title: "核对水箱记录", status: "waiting", owner: "human", detail: "等待水箱清洗记录", evidence_refs: [], action_refs: [] }, { id: "sampling", title: "现场采样", status: "waiting", owner: "human", detail: "等待现场采样结果", evidence_refs: [], action_refs: [] }, { id: "conclusion", title: "形成调查结论", status: "pending", owner: "agent", detail: "两路核验完成后汇总", evidence_refs: [], action_refs: [] } ], edges: [ { from: "accept", to: "upstream", condition: "上游监测" }, { from: "upstream", to: "upstream-review", condition: null }, { from: "upstream-review", to: "stage-judgment", condition: null }, { from: "accept", to: "nearby", condition: "邻近投诉" }, { from: "nearby", to: "stage-judgment", condition: null }, { from: "accept", to: "pressure", condition: "压力路径" }, { from: "pressure", to: "hydraulic", condition: null }, { from: "hydraulic", to: "valve", condition: null }, { from: "valve", to: "stage-judgment", condition: null }, { from: "accept", to: "maintenance", condition: "运维记录" }, { from: "maintenance", to: "stage-judgment", condition: null }, { from: "stage-judgment", to: "secondary", condition: "设施核验" }, { from: "secondary", to: "tank-record", condition: null }, { from: "tank-record", to: "conclusion", condition: null }, { from: "stage-judgment", to: "sampling", condition: "现场采样" }, { from: "sampling", to: "conclusion", condition: null } ] }; let operationalTask: Record | null = null; const summarizeTask = () => operationalTask ? Object.fromEntries(Object.entries(operationalTask).filter(([key]) => ![ "latest_plan", "latest_report", "evidence", "actions", "permissions", "work_order_snapshot" ].includes(key))) : null; page.on("request", (request) => { if ( request.method() === "POST" && request.url().includes("/api/v1/agent/chat/") && request.postData() ) { agentRequests.push(request.postDataJSON() as Record); } }); 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_AGENT_API_BASE_URL: "http://127.0.0.1:8787", TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL: "http://peripheral.test" })}` })); await page.route("http://peripheral.test/api/sample/v1/**", async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; if (path.endsWith("/context")) return route.fulfill({ json: { protocol_version: "tjwater-peripheral-sample@1", origin: "synthetic", project_id: projectId, user_id: "50000000-0000-4000-8000-000000000001", can_write: true, scenarios: ["normal"], managed_resources: { work_orders: "available", test_data: "available" }, state_transitions: { pending: [], processing: [], pending_review: [], completed: [] } } }); if (path.endsWith("/scenario")) return route.fulfill({ json: { scenario: "normal", revision: 1 } }); if (path.endsWith("/monitoring/points")) return route.fulfill({ json: { items: [], next_cursor: null, origin: "historical_replay", source_id: "test" } }); if (path.endsWith("/monitoring/alerts")) return route.fulfill({ json: { items: [], availability: "not_configured", reason: "未配置", origin: "historical_replay", source_id: "test" } }); if (path.endsWith("/work-orders") && request.method() === "GET") return route.fulfill({ json: { items: workOrders, next_cursor: null } }); if (path.endsWith("/work-orders") && request.method() === "POST") { const body = request.postDataJSON() as Record; const item = { ...body, work_order_id: workOrderId, project_id: projectId, revision: 1, origin: "synthetic", created_at: "2026-09-22T02:00:00Z", updated_at: "2026-09-22T02:00:00Z", created_by: "tester", updated_by: "tester" }; delete item.request_id; workOrders = [item]; return route.fulfill({ status: 201, json: item }); } if (path.endsWith("/test-data")) return route.fulfill({ json: { items: [], next_cursor: null } }); return route.fulfill({ status: 404, json: { code: "NOT_FOUND", message: "not found", effect: "none", request_id: null } }); }); 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/schedules", (route) => route.fulfill({ json: { items: [] } }) ); 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: "2026-09-23T08:30:00Z", conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: [] }, tasks: summarizeTask() ? [summarizeTask()] : [], notifications: [], generated_at: "2026-09-23T08:30:00Z" } }) ); await page.route("http://server.test/api/v1/workbench/events**", (route) => route.fulfill({ contentType: "text/event-stream", body: ": test keepalive\n\n" }) ); await page.route("http://server.test/api/v1/operational-tasks**", async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() === "GET" && url.pathname.endsWith("/operational-tasks/schedules")) { return route.fulfill({ json: { items: [] } }); } if (request.method() === "POST" && url.pathname.endsWith("/operational-tasks")) { const input = request.postDataJSON() as Record; operationalTask = { project_id: projectId, task_id: "70000000-0000-4000-8000-000000000001", task_type: "event_investigation", workflow: "water_quality_complaint", lifecycle_status: "waiting_human", active_run: null, status: "waiting_human", priority: "normal", revision: 2, execution_generation: 1, execution_request_id: "71000000-0000-4000-8000-000000000001", title: "水质投诉调查", summary: input.summary, source: input.source, trigger: input.trigger, scheduled_for: null, agent_session_id: "browser-session-1", agent_lease_expires_at: null, current_plan_revision: 1, current_report_revision: 1, created_at: "2026-09-23T08:20:00Z", updated_at: "2026-09-23T08:25:00Z", latest_plan: { revision: 1, execution_generation: 1, source_session_id: "browser-session-1", published_at: "2026-09-23T01:08:00Z", published_by: "agent", plan }, latest_report: { revision: 1, execution_generation: 1, source_session_id: "browser-session-1", published_at: "2026-09-23T01:09:00Z", published_by: "agent", report: { title: "水质投诉调查报告", current_conclusion: "疑似二次供水环节问题,待现场核验。", conclusion_boundary: "现场采样和水箱清洗记录尚未返回。", evidence_refs: ["e-1"], recommendations: ["安排现场采样"], pending_items: ["确认水箱清洗记录"] } }, evidence: [{ evidence_id: "e-1", kind: "synthetic", summary: "上游余氯处于正常范围", source_label: "外围合成场景", source_ref: "WQ-DEMO-01", observed_at: "2026-09-23T00:55:00Z", recorded_at: "2026-09-23T01:08:00Z", recorded_by: "agent", has_snapshot: false }], actions: [], 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 } }; return route.fulfill({ status: 201, json: operationalTask }); } if (request.method() === "GET" && url.pathname.endsWith("/operational-tasks")) { const summary = summarizeTask(); return route.fulfill({ json: { items: summary ? [summary] : [], limit: 50, offset: 0, next_offset: null } }); } if (request.method() === "GET" && operationalTask) return route.fulfill({ json: operationalTask }); return route.fulfill({ status: 404, json: {} }); }); await page.goto("/"); await page.getByRole("button", { name: "外围样板管理" }).click(); const panel = page.getByRole("complementary", { name: "外围工单测试" }); await panel.getByRole("button", { name: "投诉与爆管上报" }).click(); await panel.getByRole("textbox", { name: "事件位置" }).fill("演示小区"); await panel.getByRole("textbox", { name: "现场描述" }).fill("多户居民反映晨间用水有异味"); await panel.getByRole("button", { name: "创建事件工单" }).click(); await expect(panel).toBeHidden(); await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeHidden(); const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true }); await expect(conditionPanel).toBeVisible(); await conditionPanel.getByRole("button", { name: /调查任务/ }).click(); await expect(conditionPanel.getByRole("heading", { name: "水质投诉调查", exact: true })).toBeVisible(); await expect(conditionPanel.getByText("调查处理流程", { exact: true })).toBeVisible(); await expect(conditionPanel.getByText(/已完成 5 项 · 执行中 3 项 · 待人工或资料 3 项/)).toBeVisible(); await expect(conditionPanel.locator('[data-execution-node]')).toHaveCount(13); await expect(conditionPanel.getByLabel("并行分枝 4 项")).toBeVisible(); await expect(conditionPanel.getByLabel("并行分枝 2 项")).toBeVisible(); await expect(conditionPanel.getByRole("button", { name: "完整工程图" })).toHaveCount(0); await expect(conditionPanel.getByText("节点与执行记录", { exact: true })).toHaveCount(0); await expect(conditionPanel.getByLabel("计划版本", { exact: true })).toHaveCount(0); await expect(conditionPanel.locator('[data-execution-node]')).toHaveCount(13); await expect(conditionPanel.getByText("等待水箱清洗记录", { exact: true }).first()).toBeVisible(); const report = conditionPanel.getByRole("region", { name: "任务报告" }); await expect(report.getByText(/疑似二次供水环节问题,待现场核验/)).toBeVisible(); await expect(report.getByText("完整调查报告", { exact: true })).toHaveCount(0); await expect(report.getByText(/结论边界/)).toBeVisible(); await expect(conditionPanel.getByText("TJWater operational task", { exact: true })).toHaveCount(0); await expect(conditionPanel.getByText("browser-session-1", { exact: true })).toHaveCount(0); await conditionPanel.getByText("调查处理流程", { exact: true }).scrollIntoViewIfNeeded(); await expect(conditionPanel.getByText("前置工作与条件", { exact: true })).toHaveCount(0); await conditionPanel.getByText("调查处理流程", { exact: true }).scrollIntoViewIfNeeded(); await expect(page.getByText("已建立水质投诉调查", { exact: true })).toBeHidden({ timeout: 10_000 }); await page.addStyleTag({ content: "[data-sonner-toaster] { display: none !important; }" }); await page.evaluate(() => document.fonts.ready); await testInfo.attach("water-quality-condition-task-1280", { body: await conditionPanel.screenshot({ path: testInfo.outputPath("execution-plan-desktop.png") }), contentType: "image/png" }); await conditionPanel.getByLabel("并行分枝 2 项").scrollIntoViewIfNeeded(); await testInfo.attach("water-quality-condition-task-second-fork-1280", { body: await conditionPanel.screenshot({ path: testInfo.outputPath("execution-plan-second-fork-desktop.png") }), contentType: "image/png" }); for (const width of [375, 320]) { await page.setViewportSize({ width, height: 720 }); const responsiveConditionPanel = page.getByRole("region", { name: "工况任务", exact: true }); if (!await responsiveConditionPanel.isVisible()) { await page.getByRole("button", { name: "打开工况任务", exact: true }).click(); } await expect(responsiveConditionPanel).toBeVisible(); const expandResponsiveTask = responsiveConditionPanel.getByRole("button", { name: "展开工况任务" }); if (await expandResponsiveTask.isVisible()) { await expandResponsiveTask.click(); } await responsiveConditionPanel.getByRole("button", { name: /调查任务/ }).click(); await expect(responsiveConditionPanel.getByRole("heading", { name: "水质投诉调查" })).toBeVisible(); const responsiveWorkflowStep = responsiveConditionPanel.locator('[data-execution-node="upstream"]'); await responsiveWorkflowStep.scrollIntoViewIfNeeded(); await expect(responsiveWorkflowStep).toBeVisible(); await responsiveConditionPanel.getByText("调查处理流程", { exact: true }).scrollIntoViewIfNeeded(); await expect(responsiveConditionPanel.locator('[data-execution-node]')).toHaveCount(13); await expect(responsiveConditionPanel.getByLabel("并行分枝 4 项")).toBeVisible(); await expect(responsiveConditionPanel.getByLabel("并行分枝 2 项")).toBeVisible(); await expect(responsiveConditionPanel.getByRole("button", { name: "放大流程图" })).toHaveCount(0); await page.waitForTimeout(350); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await testInfo.attach(`water-quality-condition-task-${width}`, { body: await page.screenshot({ path: testInfo.outputPath(`execution-plan-${width}.png`) }), contentType: "image/png" }); await responsiveConditionPanel.getByLabel("并行分枝 2 项").scrollIntoViewIfNeeded(); await page.evaluate(() => document.fonts.ready); await testInfo.attach(`water-quality-condition-task-second-fork-${width}`, { body: await page.screenshot({ path: testInfo.outputPath(`execution-plan-second-fork-${width}.png`) }), contentType: "image/png" }); } await page.setViewportSize({ width: 1280, height: 720 }); const restoredConditionPanel = page.getByRole("region", { name: "工况任务", exact: true }); if (!await restoredConditionPanel.isVisible()) { await page.getByRole("button", { name: "工况任务", exact: true }).click(); } const agentCollaborationButton = restoredConditionPanel.getByRole("button", { name: "Agent 协作" }); if (!await agentCollaborationButton.isVisible()) { const expandRestoredTask = restoredConditionPanel.getByRole("button", { name: "展开工况任务" }); if (await expandRestoredTask.isVisible()) await expandRestoredTask.click(); await restoredConditionPanel.getByRole("button", { name: /调查任务/ }).click(); } await agentCollaborationButton.click(); await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeVisible(); expect(workOrders[0]).toMatchObject({ work_order_type: "water_quality_complaint", scenario_code: "WQ-SECONDARY-SUPPLY-001", status: "open" }); expect(agentRequests.filter((body) => "trigger_context" in body)).toHaveLength(0); await page.route("**/api/v1/agent/chat/sessions", (route) => route.fulfill({ json: { sessions: [{ id: "browser-session-1", title: "水质投诉调查", created_at: "2026-09-23T01:00:00Z", updated_at: "2026-09-23T01:00:00Z", trigger: { subject_id: workOrderId, workflow: "water_quality_complaint", occurred_at: "2026-09-23T01:00:00Z" } }] } })); await page.reload(); await page.getByRole("button", { name: "工况任务", exact: true }).click(); const recovered = page.getByRole("region", { name: "工况任务", exact: true }); await recovered.getByRole("button", { name: "展开工况任务" }).click(); await recovered.getByRole("button", { name: /调查任务/ }).click(); const taskRow = recovered.getByRole("button", { name: /水质投诉调查/ }).first(); await taskRow.click(); await expect(recovered.getByLabel("并行分枝 4 项")).toBeVisible(); await expect(recovered.getByLabel("并行分枝 2 项")).toBeVisible(); expect(agentRequests.filter((body) => "trigger_context" in body)).toHaveLength(0); }); test("在 375px 视口内完整呈现面板", async ({ page }) => { await page.setViewportSize({ width: 375, height: 720 }); await page.goto("/"); const launcher = page.getByRole("button", { name: "外围样板管理" }); await launcher.focus(); await page.keyboard.press("Enter"); const panel = page.getByRole("complementary", { name: "外围工单测试" }); await expect(panel).toBeVisible(); const box = await panel.boundingBox(); expect(box).not.toBeNull(); expect(box!.x).toBeGreaterThanOrEqual(0); expect(box!.x + box!.width).toBeLessThanOrEqual(375); }); }); function createDemoDataset(projectId: string) { const points = Array.from({ length: 5 }, (_, index) => ({ point_id: `DEMO-${index + 1}`, label: `虚拟压力测点 ${index + 1}`, unit: "m", status: "active", origin: "synthetic" })); return { generator_version: "water-demo@1", seed: "water-demo-seed-20260917", origin: "synthetic", window: { start: "2026-09-17T00:00:00Z", end: "2026-09-17T12:00:00Z" }, project_id: projectId, source_id: "peripheral-demo", source_revision: "demo-r1", points, observations: Array.from({ length: 240 }, (_, index) => ({ record_id: `demo-reading-${index}`, point_id: `DEMO-${(index % 5) + 1}`, observed_at: `2026-09-17T${String(Math.floor(index / 20)).padStart(2, "0")}:${String((index % 4) * 15).padStart(2, "0")}:00Z`, value: index === 0 ? 0 : index === 5 ? null : 31 + index / 100, quality: index === 5 ? "missing" : "valid", unit: "m" })), device_states: Array.from({ length: 9 }, (_, index) => ({ record_id: `60000000-0000-4000-8000-${String(index).padStart(12, "0")}`, asset_id: `VIRTUAL-${index + 1}`, asset_type: "pressure_sensor", observed_at: "2026-09-17T01:00:00Z", state: index === 1 ? "offline" : "online" })), alerts: [ { alert_id: "70000000-0000-4000-8000-000000000001", asset_id: "VIRTUAL-2", code: "communication_lost", message: "演示通信中断", occurred_at: "2026-09-17T02:00:00Z", cleared_at: "2026-09-17T02:15:00Z" }, { alert_id: "70000000-0000-4000-8000-000000000002", asset_id: "VIRTUAL-3", code: "low_level", message: "演示低水位", occurred_at: "2026-09-17T05:00:00Z", cleared_at: null } ], operations: Array.from({ length: 4 }, (_, index) => ({ record_id: `80000000-0000-4000-8000-${String(index).padStart(12, "0")}`, actor: "synthetic-generator", asset_id: `VIRTUAL-${index + 1}`, operation: ["pump_start", "pump_stop", "valve_open", "valve_close"][index], note: "演示事件,不是控制指令", occurred_at: `2026-09-17T0${index + 1}:30:00Z` })) }; } function createSourceWorkOrders(projectId: string) { return [ ["合成通信核查", "核查监测点通信和数据上送状态", "open", "high"], ["合成压力偏差复核", "复核压力偏差与历史回放证据", "in_progress", "urgent"], ["合成零流量样本确认", "确认零流量样本及质量标记", "completed", "normal"] ].map(([title, summary, status, priority], index) => ({ work_order_id: `50000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`, project_id: projectId, revision: 1, title, summary, status, priority, assignee: null, scheduled_at: "2026-09-17T08:00:00Z", work_order_type: "general", location: null, scenario_code: null, subtasks: null, origin: "synthetic", created_at: "2026-09-17T00:00:00Z", updated_at: "2026-09-17T00:00:00Z", created_by: "synthetic-generator", updated_by: "synthetic-generator" })); }