182 lines
13 KiB
TypeScript
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"
|
|
});
|
|
}
|
|
});
|