feat(workbench): integrate runtime data and map tooling

This commit is contained in:
2026-09-22 10:45:46 +08:00
parent bf44cc0f25
commit bebcf2a06c
163 changed files with 15642 additions and 1312 deletions
@@ -0,0 +1,513 @@
import { expect, test } from "@playwright/test";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
const TODAY = shanghaiTimestamp(10, 30);
const TODAY_10_10 = shanghaiTimestamp(10, 10);
const TODAY_10_20 = shanghaiTimestamp(10, 20);
const TODAY_10_25 = shanghaiTimestamp(10, 25);
const TODAY_10_35 = shanghaiTimestamp(10, 35);
const TODAY_11_00 = shanghaiTimestamp(11, 0);
const YESTERDAY_10_10 = shanghaiTimestamp(10, 10, -1);
const YESTERDAY_10_20 = shanghaiTimestamp(10, 20, -1);
test("history mode opens on the latest day with backend work orders", async ({ page }) => {
await mockWorkbenchContext(page, {
availability: "ready",
source: "scheduled_tasks",
scenario_availability: "not_configured",
artifact_availability: "not_configured",
schedule_availability: "ready",
work_order_availability: "ready",
alert_availability: "ready",
generated_at: "2026-09-18T02:30:00Z",
conditions: [{
id: "backend-schedule-demo-day-next",
task_id: "backend-schedule",
session_id: "scheduled-condition-backend-schedule-demo-day",
title: "历史 SCADA 诊断计划",
summary: "来自后端持久化的目标明确的诊断计划。",
status: "running",
risk_level: "normal",
scheduled_at: "2026-08-17T09:40:00Z",
updated_at: "2026-08-17T09:40:00Z",
detail: "按计划复核 SCADA 缺测与越界记录。",
recommendation: null,
evidence: ["调度 ID:demo-day"],
model_name: "TJWater scheduler"
}],
alerts: [],
scenarios: [],
latest_analysis: null,
work_orders: [{
id: "wo-history-1",
code: "WO-20260817-001",
scheduled_at: "2026-08-17T09:40:00Z",
title: "历史工单:阀门复核",
summary: "来自后端工单投影的历史预约任务。",
status: "pending",
risk_level: "normal",
updated_at: "2026-08-17T09:35:00Z",
detail: "复核阀门状态并回传结果。",
recommendation: null,
duration_minutes: 30,
source: "工单系统",
dispatcher: "调度中心",
priority: "normal",
subtasks: [{
id: "wo-history-1-subtask-1",
title: "阀门状态复核",
summary: "现场复核阀门状态并回传。",
status: "running",
scheduled_at: "2026-08-17T09:40:00Z",
location: "北区阀门 V-17",
assignee: "北区班组",
duration_minutes: 30,
reply_window_minutes: 15,
stages: [
{ id: "dispatch", sequence: 1, title: "派发", status: "done", owner: "调度中心", time_label: "17:20", description: "已派发。" },
{ id: "execution", sequence: 2, title: "执行", status: "current", owner: "北区班组", time_label: "17:40-18:10", description: "现场复核中。" },
{ id: "reply", sequence: 3, title: "复令", status: "pending", owner: "北区班组", time_label: "18:10-18:25", description: "等待结果回传。" }
],
reply_requirements: ["回传阀门状态"]
}]
}]
}, undefined, {}, [], { runtimeMode: "history" });
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByText("历史 2026-08-17 17:40", { exact: true })).toBeVisible();
await expect(page.getByRole("region", { name: "运行时间轴" })).toContainText("2026.08.17");
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const panel = page.getByRole("region", { name: "工况任务", exact: true });
await expect(panel).toContainText("工况演示数据 · 后端工单");
await expect(panel).toContainText("SCADA 数据诊断");
await panel.getByRole("button", { name: "展开工况任务" }).click();
await expect(panel).toContainText("历史工单:阀门复核");
await expect(panel).not.toContainText("历史 SCADA 诊断计划");
});
test("keeps mock conditions while retaining explicit backend alerts", async ({ page }) => {
await mockWorkbenchContext(page, {
availability: "ready",
source: "scheduled_tasks",
scenario_availability: "not_configured",
artifact_availability: "not_configured",
schedule_availability: "not_configured",
work_order_availability: "not_configured",
alert_availability: "ready",
generated_at: TODAY,
conditions: [{
id: "schedule-run-test-1",
task_id: "backend-schedule",
session_id: "scheduled-condition-schedule-run-test-1",
title: "演示 合成低压调查",
summary: "来自后端持久化 Action Run 的 演示 沙箱测试状态。",
status: "error",
risk_level: "critical",
scheduled_at: TODAY_10_20,
updated_at: TODAY_10_25,
detail: "演示 测试运行状态:已失败。",
recommendation: "请在 演示 调查任务中检查失败步骤与运行日志。",
evidence: ["运行状态:failed"],
model_name: "TJWater scheduler"
}],
alerts: [{
id: "scheduled-condition-alert-schedule-run-test-1",
condition_id: "schedule-run-test-1",
source: {
task_type: "analysis_task",
task_id: "backend-schedule",
run_id: "schedule-run-test-1"
},
title: "演示 测试待复核:演示 合成低压调查",
description: "运行失败。演示 测试运行状态:已失败。",
occurred_at: TODAY_10_25,
severity: "critical"
}],
scenarios: [],
latest_analysis: null,
work_orders: []
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const panel = page.getByRole("region", { name: "工况任务", exact: true });
await expect(panel).toContainText("工况演示数据 · 未接入工单");
await expect(panel).toContainText("SCADA 数据诊断");
await expect(panel).not.toContainText("演示 合成低压调查");
const alertButton = page.getByRole("button", {
name: "查看异常处置面板,当前 1 条待复核工况"
});
await alertButton.focus();
await page.keyboard.press("Enter");
await expect(page.getByText("演示 测试待复核:演示 合成低压调查")).toBeVisible();
});
test("opens the latest persisted analysis as the active visualization", async ({ page }) => {
const latestAnalysis = {
run_id: "analysis-1",
name: "北区爆管定位",
run_type: "burst_location",
run_type_label: "爆管定位",
status: "completed",
status_label: "已完成",
created_by: "alice",
created_at: TODAY_10_20,
started_at: TODAY_10_10,
results: [{
result_type: "burst_location",
created_at: TODAY_10_20,
facts: [{ label: "关联管段", value: "P-42" }]
}],
map_payload: {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: { label: "后端爆管定位" },
geometry: { type: "Point", coordinates: [121.5, 30.9] }
}]
}
};
const historicalAnalysis = {
...latestAnalysis,
run_id: "analysis-2",
name: "南区历史定位",
created_at: YESTERDAY_10_20,
started_at: YESTERDAY_10_10,
results: [{
result_type: "burst_location",
created_at: YESTERDAY_10_20,
facts: [{ label: "关联管段", value: "P-older" }]
}],
map_payload: {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: { label: "历史运行几何" },
geometry: { type: "Point", coordinates: [121.4, 30.8] }
}]
}
};
await mockWorkbenchContext(page, {
availability: "ready",
source: "analysis_runs",
scenario_availability: "ready",
artifact_availability: "ready",
schedule_availability: "ready",
work_order_availability: "not_configured",
alert_availability: "not_configured",
generated_at: TODAY,
conditions: [{
id: "analysis-run-analysis-1",
task_id: "backend-analysis",
session_id: "scheduled-condition-analysis-run-analysis-1",
title: "北区爆管定位",
summary: "来自项目后端持久化分析运行:爆管定位。",
status: "completed",
risk_level: "normal",
scheduled_at: TODAY_10_10,
updated_at: TODAY_10_20,
detail: "分析类型:爆管定位;后端状态:已完成。",
recommendation: null,
evidence: ["运行 ID:analysis-1"],
model_name: "TJWater analysis runtime"
}, {
id: "analysis-run-analysis-2",
task_id: "backend-analysis",
session_id: "scheduled-condition-analysis-run-analysis-2",
title: "南区历史定位",
summary: "来自项目后端持久化分析运行:爆管定位。",
status: "completed",
risk_level: "normal",
scheduled_at: YESTERDAY_10_10,
updated_at: YESTERDAY_10_20,
detail: "分析类型:爆管定位;后端状态:已完成。",
recommendation: null,
evidence: ["运行 ID:analysis-2"],
model_name: "TJWater analysis runtime"
}],
alerts: [],
scenarios: [
{
id: "analysis-run-analysis-1",
run_id: "analysis-1",
name: "北区爆管定位",
description: "爆管定位 · 已完成 · 创建人 alice",
status: "review",
run_type: "burst_location",
run_type_label: "爆管定位",
run_status: "completed",
created_at: TODAY_10_20
},
{
id: "analysis-run-analysis-2",
run_id: "analysis-2",
name: "南区历史定位",
description: "爆管定位 · 已完成 · 创建人 alice",
status: "review",
run_type: "burst_location",
run_type_label: "爆管定位",
run_status: "completed",
created_at: YESTERDAY_10_20
}
],
latest_analysis: latestAnalysis,
work_orders: []
}, undefined, {
"analysis-1": latestAnalysis,
"analysis-2": historicalAnalysis
}, [{
run_id: "analysis-1",
name: "北区爆管定位",
run_type: "burst_location",
run_type_label: "爆管定位",
status: "completed",
status_label: "已完成",
created_by: "alice",
created_at: TODAY_10_20,
started_at: TODAY_10_10,
has_results: true
}, {
run_id: "analysis-2",
name: "南区历史定位",
run_type: "burst_location",
run_type_label: "爆管定位",
status: "completed",
status_label: "已完成",
created_by: "alice",
created_at: YESTERDAY_10_20,
started_at: YESTERDAY_10_10,
has_results: true
}, ...Array.from({ length: 9 }, (_, index) => ({
run_id: `analysis-${index + 3}`,
name: `批次分析 ${index + 3}`,
run_type: "pressure_analysis",
run_type_label: "压力分析",
status: "completed",
status_label: "已完成",
created_by: "scheduler",
created_at: YESTERDAY_10_20,
started_at: YESTERDAY_10_10,
has_results: index % 2 === 0
}))]);
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("button", { name: "切换模拟方案" })).toHaveCount(0);
await expect(page.getByRole("button", { name: /当前分析视图/ })).toHaveCount(0);
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
await expect(conditionPanel).toContainText("工况演示数据 · 未接入工单");
await expect(conditionPanel).not.toContainText("北区爆管定位");
await expect(conditionPanel).not.toContainText("南区历史定位");
await expect(conditionPanel).toContainText("SCADA 数据诊断");
await page.getByRole("button", { name: "工况任务", exact: true }).click();
await expect(page.getByRole("region", { name: "工况任务", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "打开最新后端分析成果" }).click();
await expect(page.getByRole("complementary", { name: "后端分析成果预览" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "北区爆管定位" })).toBeVisible();
await expect(page.getByText("Backend Analysis")).toBeVisible();
await expect(page.getByText("关联管段:P-42")).toBeVisible();
const analysisViewButton = page.getByRole("button", { name: "当前分析视图:北区爆管定位" });
await expect(analysisViewButton).toContainText("分析视图 · 北区爆管定位");
await expect.poll(() => page.evaluate(() =>
window.__waterNetworkMap
?.querySourceFeatures("simulation-backend-analysis")
.map((feature) => feature.properties?.label) ?? []
)).toContain("后端爆管定位");
await analysisViewButton.click();
await expect(page.getByText(/爆管定位 · 项目后端分析运行 · 后端分析运行/)).toBeVisible();
await page.getByText("关闭分析视图", { exact: true }).click();
await expect(page.getByRole("button", { name: /当前分析视图/ })).toHaveCount(0);
await expect.poll(() => page.evaluate(() =>
window.__waterNetworkMap
?.querySourceFeatures("simulation-backend-analysis").length ?? 0
)).toBe(0);
await page.getByRole("button", { name: "项目分析成果" }).click();
const resultsPanel = page.getByRole("complementary", { name: "项目分析成果" });
await resultsPanel.getByText("浏览项目分析成果", { exact: true }).click();
await expect(resultsPanel.getByRole("button", { name: "打开分析成果:北区爆管定位" })).toBeVisible();
await expect(resultsPanel.getByRole("button", { name: "打开分析成果:批次分析 11" })).toHaveCount(0);
await resultsPanel.getByRole("button", { name: "加载更多成果" }).click();
await expect(resultsPanel.getByRole("button", { name: "打开分析成果:批次分析 11" })).toBeVisible();
await resultsPanel.getByRole("button", { name: "打开分析成果:南区历史定位" }).click();
await expect(resultsPanel).toHaveCount(0);
await expect(page.getByRole("heading", { name: "南区历史定位" })).toBeVisible();
await expect(page.getByRole("button", { name: "当前分析视图:南区历史定位" })).toBeVisible();
await expect.poll(() => page.evaluate(() =>
window.__waterNetworkMap
?.querySourceFeatures("simulation-backend-analysis")
.map((feature) => feature.properties?.label) ?? []
)).toContain("历史运行几何");
});
test("keeps a non-spatial backend result in the analysis workspace without demo geometry", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await mockWorkbenchContext(page, {
availability: "ready",
source: "analysis_runs",
scenario_availability: "ready",
artifact_availability: "ready",
schedule_availability: "not_configured",
work_order_availability: "not_configured",
alert_availability: "not_configured",
generated_at: TODAY,
conditions: [],
alerts: [],
scenarios: [],
latest_analysis: {
run_id: "analysis-table-only",
name: "压力统计结果",
run_type: "pressure_analysis",
run_type_label: "压力分析",
status: "completed",
status_label: "已完成",
created_by: "alice",
created_at: TODAY_10_20,
started_at: TODAY_10_10,
results: [{
result_type: "pressure_analysis",
created_at: TODAY_10_20,
facts: [{ label: "结果状态", value: "已完成" }]
}],
map_payload: null
},
work_orders: []
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开最新后端分析成果" }).click();
await expect(page.getByRole("heading", { name: "压力统计结果" })).toBeVisible();
await page.getByRole("button", { name: "当前分析视图:压力统计结果" }).click();
await expect(page.getByText("仅看地图", { exact: true })).toHaveCount(0);
await page.getByRole("menuitem", { name: /半屏显示/ }).click();
await expect(page.getByRole("menu")).toHaveCount(0);
await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_split");
await expect(page.locator("#workspace-map")).toBeVisible();
await expect(page.locator("#artifact-workspace")).toBeVisible();
await expect.poll(() => page.evaluate(() => ({
backend: window.__waterNetworkMap
?.querySourceFeatures("simulation-backend-analysis").length ?? 0,
demo: window.__waterNetworkMap
?.querySourceFeatures("simulation-impact-area").length ?? 0
}))).toEqual({ backend: 0, demo: 0 });
});
test("keeps mock conditions while using backend work orders and alerts", async ({ page }) => {
await mockWorkbenchContext(page, {
availability: "ready",
source: "scheduled_tasks",
scenario_availability: "not_configured",
artifact_availability: "not_configured",
schedule_availability: "ready",
work_order_availability: "ready",
alert_availability: "ready",
generated_at: TODAY,
conditions: [{
id: "backend-schedule-diagnosis-next",
task_id: "backend-schedule",
session_id: "scheduled-condition-backend-schedule-diagnosis",
title: "SCADA 后端定时诊断",
summary: "来自项目数据库的持久化调度定义。",
status: "pending",
risk_level: "normal",
scheduled_at: TODAY_10_35,
updated_at: TODAY,
detail: "每 5 分钟触发一次。",
recommendation: null,
evidence: ["调度 ID:diagnosis-5m"],
model_name: "TJWater scheduler"
}],
alerts: [{
id: "backend-alert-alarm-1",
condition_id: null,
source: {
task_type: "scheduled_task",
task_id: "diagnosis-5m",
run_id: "trigger-1"
},
title: "后端告警:泵站通信中断",
description: "由诊断任务明确推送到项目数据库。",
occurred_at: TODAY,
severity: "warning"
}],
scenarios: [],
latest_analysis: null,
work_orders: [{
id: "wo-backend-1",
code: "WO-20260917-001",
scheduled_at: TODAY_11_00,
title: "后端工单:北区阀门复核",
summary: "现场复核阀门开度并回传。",
status: "running",
risk_level: "attention",
updated_at: TODAY,
detail: "来自正式工单同步适配器。",
recommendation: "完成后复核压力。",
duration_minutes: 45,
source: "生产工单系统",
dispatcher: "调度中心",
priority: "urgent",
subtasks: [{
id: "wo-backend-1-subtask-1",
title: "阀门开度复核",
summary: "现场复核阀门开度。",
status: "running",
scheduled_at: TODAY_11_00,
location: "北区阀门 V-1",
assignee: "北区班组",
duration_minutes: 45,
reply_window_minutes: 20,
stages: [
{ id: "dispatch", sequence: 1, title: "派发", status: "done", owner: "调度中心", time_label: "10:50", description: "已派发。" },
{ id: "execution", sequence: 2, title: "执行中", status: "current", owner: "北区班组", time_label: "11:00—11:45", description: "现场执行中。" },
{ id: "reply", sequence: 3, title: "待复令", status: "pending", owner: "北区班组", time_label: "11:45—12:05", description: "等待复令。" }
],
reply_requirements: ["回传阀门开度"]
}]
}]
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const panel = page.getByRole("region", { name: "工况任务", exact: true });
await expect(panel).toContainText("工况演示数据 · 后端工单");
await expect(panel).toContainText("SCADA 数据诊断");
await expect(panel).not.toContainText("SCADA 后端定时诊断");
await panel.getByRole("button", { name: "展开工况任务" }).click();
await expect(panel).toContainText("后端工单:北区阀门复核");
await expect(panel).not.toContainText("工单演示回退");
await panel.getByRole("button", { name: /后端工单:北区阀门复核/ }).click();
const detail = panel.getByTestId("scheduled-condition-detail-scroll");
await expect(detail.getByText("子任务", { exact: true })).toBeVisible();
await expect(detail.getByText(/阀门开度复核/).first()).toBeVisible();
await expect(detail.getByText("0/1 完成", { exact: true })).toBeVisible();
await expect(
detail.locator("details").filter({ hasText: "阀门开度复核" }).getByText("执行中", { exact: true })
).toBeVisible();
const alertButton = page.getByRole("button", {
name: "查看异常处置面板,当前 1 条待复核工况"
});
await alertButton.focus();
await page.keyboard.press("Enter");
await expect(page.getByText("来源:后端运行态", { exact: true })).toBeVisible();
await expect(page.getByText("后端告警:泵站通信中断")).toBeVisible();
});
function shanghaiTimestamp(hour: number, minute: number, dayOffset = 0) {
const target = new Date(Date.now() + dayOffset * 24 * 60 * 60 * 1000);
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Shanghai",
year: "numeric",
month: "2-digit",
day: "2-digit"
}).formatToParts(target);
const get = (type: Intl.DateTimeFormatPartTypes) =>
parts.find((part) => part.type === type)?.value ?? "";
return `${get("year")}-${get("month")}-${get("day")}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00+08:00`;
}