Files
next-tjwater-frontend/tests/browser/dynamic-investigation-flow.e2e.ts
T

182 lines
13 KiB
TypeScript

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"
});
}
});