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,405 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
fetchWorkbenchAnalysisRun,
fetchWorkbenchRuntimeSnapshot
} from "./workbench-runtime-client";
const projectPage = {
items: [{ project_id: "project-1", gs_workspace: "tjwater_next", status: "active" }],
total: 1,
limit: 1000,
offset: 0
};
const runtimeSnapshot = {
availability: "ready",
source: "scheduled_tasks",
scenario_availability: "ready",
artifact_availability: "ready",
schedule_availability: "not_configured",
work_order_availability: "not_configured",
alert_availability: "ready",
generated_at: "2026-09-17T02:30:00Z",
conditions: [{
id: "schedule-run-1",
task_id: "backend-schedule",
session_id: "scheduled-condition-schedule-run-1",
title: "演示 合成低压调查",
summary: "来自后端持久化 Action Run 的 演示 沙箱测试状态。",
status: "error",
risk_level: "critical",
scheduled_at: "2026-09-17T02:20:00Z",
updated_at: "2026-09-17T02:25:00Z",
detail: "演示 测试运行状态:已失败。",
recommendation: "请检查失败步骤。",
evidence: ["运行状态:failed"],
model_name: "TJWater scheduler"
}],
alerts: [{
id: "scheduled-condition-alert-schedule-run-1",
condition_id: "schedule-run-1",
source: {
task_type: "analysis_task",
task_id: "backend-schedule",
run_id: "schedule-run-1"
},
title: "演示 测试待复核:演示 合成低压调查",
description: "运行失败。",
occurred_at: "2026-09-17T02:25:00Z",
severity: "critical"
}],
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: "2026-09-17T02:20:00Z"
}],
latest_analysis: {
run_id: "analysis-1",
name: "北区爆管定位",
run_type: "burst_location",
run_type_label: "爆管定位",
status: "completed",
status_label: "已完成",
created_by: "alice",
created_at: "2026-09-17T02:20:00Z",
started_at: "2026-09-17T02:10:00Z",
results: [{
result_type: "burst_location",
created_at: "2026-09-17T02:20:00Z",
facts: [{ label: "关联管段", value: "P-42" }]
}],
map_payload: {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: { label: "后端爆管定位" },
geometry: { type: "Point", coordinates: [121.5, 30.9] }
}]
}
},
work_orders: []
};
describe("workbench runtime API client", () => {
afterEach(() => vi.unstubAllGlobals());
it("resolves the project and maps a persisted scheduled condition", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(projectPage), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify(runtimeSnapshot), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const snapshot = await fetchWorkbenchRuntimeSnapshot(
vi.fn().mockResolvedValue("access-token")
);
expect(snapshot.source).toBe("scheduled_tasks");
expect(snapshot.conditions).toEqual([
expect.objectContaining({
id: "schedule-run-1",
kind: "condition",
taskId: "backend-schedule",
status: "error",
modelName: "TJWater scheduler"
})
]);
expect(snapshot.alerts[0]).toEqual(expect.objectContaining({
conditionId: "schedule-run-1",
id: "scheduled-condition-alert-schedule-run-1",
title: "演示 测试待复核:演示 合成低压调查",
source: {
taskType: "analysis_task",
taskId: "backend-schedule",
runId: "schedule-run-1"
}
}));
expect(snapshot.scenarios[0]).toEqual(expect.objectContaining({
id: "analysis-run-analysis-1",
runId: "analysis-1",
runType: "burst_location"
}));
expect(snapshot.latestAnalysisArtifact).toEqual(expect.objectContaining({
id: "persisted-analysis-analysis-1",
title: "北区爆管定位",
confidence: "后端未提供",
mapRelation: "required",
spatialOverlay: expect.objectContaining({ type: "FeatureCollection" }),
sources: ["项目 BizDB · analysis.runs", "项目 BizDB · analysis.results"]
}));
expect(snapshot.latestAnalysisArtifact?.blocks).toEqual(expect.arrayContaining([
expect.objectContaining({
kind: "data-table",
rows: [expect.objectContaining({ facts: "关联管段:P-42" })]
})
]));
expect(snapshot.simulationOverlay?.features).toHaveLength(1);
const runtimeHeaders = new Headers(fetchMock.mock.calls[1]?.[1]?.headers);
expect(runtimeHeaders.get("Authorization")).toBe("Bearer access-token");
expect(runtimeHeaders.get("X-Project-Id")).toBe("project-1");
expect(String(fetchMock.mock.calls[1]?.[0])).toContain("/api/v1/workbench/runtime");
});
it("keeps legacy analysis conditions out of the operational task feed", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(projectPage), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({
...runtimeSnapshot,
source: "analysis_runs",
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: "2026-09-17T02:10:00Z",
updated_at: "2026-09-17T02:20:00Z",
detail: "分析结果已完成。",
recommendation: null,
evidence: ["运行 ID:analysis-1"],
model_name: "TJWater analysis runtime"
}]
}), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const snapshot = await fetchWorkbenchRuntimeSnapshot();
expect(snapshot.source).toBe("none");
expect(snapshot.conditions).toEqual([]);
expect(snapshot.latestAnalysisArtifact).toEqual(expect.objectContaining({
id: "persisted-analysis-analysis-1",
title: "北区爆管定位"
}));
expect(snapshot.scenarios).toHaveLength(1);
});
it("rejects an untyped runtime payload instead of treating it as real data", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(projectPage), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ ...runtimeSnapshot, source: "production" }), {
status: 200
}));
vi.stubGlobal("fetch", fetchMock);
await expect(fetchWorkbenchRuntimeSnapshot()).rejects.toThrow();
});
it("loads the requested analysis run instead of reusing the latest result", async () => {
const runId = "86fd7c19-5b75-434f-999c-ec8fdf67d10b";
const selectedAnalysis = {
...runtimeSnapshot.latest_analysis,
run_id: runId,
name: "历史爆管定位",
results: [{
result_type: "burst_location",
created_at: "2026-09-17T01:20:00Z",
facts: [{ label: "关联管段", value: "P-older" }]
}],
map_payload: {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: { label: "历史运行几何" },
geometry: { type: "Point", coordinates: [121.4, 30.8] }
}]
}
};
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(projectPage), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify(selectedAnalysis), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const projection = await fetchWorkbenchAnalysisRun(
runId,
vi.fn().mockResolvedValue("access-token")
);
expect(projection.runId).toBe(runId);
expect(projection.artifact).toEqual(expect.objectContaining({
id: `persisted-analysis-${runId}`,
title: "历史爆管定位"
}));
expect(projection.artifact.blocks).toEqual(expect.arrayContaining([
expect.objectContaining({
kind: "data-table",
rows: [expect.objectContaining({ facts: "关联管段:P-older" })]
})
]));
expect(projection.simulationOverlay?.features[0]?.properties).toEqual({
label: "历史运行几何"
});
expect(String(fetchMock.mock.calls[1]?.[0])).toContain(
`/api/v1/workbench/analysis-runs/${runId}`
);
const headers = new Headers(fetchMock.mock.calls[1]?.[1]?.headers);
expect(headers.get("Authorization")).toBe("Bearer access-token");
expect(headers.get("X-Project-Id")).toBe("project-1");
});
it("drops invalid backend map geometry without discarding other real sections", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(projectPage), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({
...runtimeSnapshot,
latest_analysis: {
...runtimeSnapshot.latest_analysis,
map_payload: {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: {},
geometry: { type: "Point", coordinates: ["east", "north"] }
}]
}
}
}), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const snapshot = await fetchWorkbenchRuntimeSnapshot();
expect(snapshot.source).toBe("scheduled_tasks");
expect(snapshot.scenarios).toHaveLength(1);
expect(snapshot.simulationOverlay).toBeNull();
expect(snapshot.latestAnalysisArtifact?.mapRelation).toBe("none");
expect(snapshot.latestAnalysisArtifact).not.toHaveProperty("spatialOverlay");
});
it("maps provisioned backend work orders without using the demo shape", async () => {
const backendWorkOrder = {
id: "wo-1",
code: "WO-20260917-001",
scheduled_at: "2026-09-17T03:00:00Z",
title: "北区阀门现场复核",
summary: "复核阀门开度并回传。",
status: "completed",
risk_level: "attention",
updated_at: "2026-09-17T02:30:00Z",
detail: "来自正式工单同步适配器。",
recommendation: "完成后复核压力。",
duration_minutes: 45,
source: "生产工单系统",
dispatcher: "调度中心",
priority: "urgent",
subtasks: [{
id: "wo-1-subtask-1",
title: "阀门开度复核",
summary: "现场复核阀门开度。",
status: "completed",
scheduled_at: "2026-09-17T03:00:00Z",
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: "done",
owner: "北区班组",
time_label: "11:00—11:45",
description: "现场执行中。"
},
{
id: "reply",
sequence: 3,
title: "待复令",
status: "done",
owner: "北区班组",
time_label: "11:45—12:05",
description: "等待复令。"
}
],
reply_requirements: ["回传阀门开度"]
}]
};
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(projectPage), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({
...runtimeSnapshot,
work_order_availability: "ready",
work_orders: [backendWorkOrder]
}), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const snapshot = await fetchWorkbenchRuntimeSnapshot();
expect(snapshot.workOrderAvailability).toBe("ready");
expect(snapshot.workOrders).toEqual([expect.objectContaining({
id: "wo-1",
kind: "work_order",
code: "WO-20260917-001",
status: "completed",
source: "生产工单系统",
subtasks: expect.arrayContaining([
expect.objectContaining({
id: "wo-1-subtask-1",
stages: expect.arrayContaining([
expect.objectContaining({ id: "execution", sequence: 2, timeLabel: "11:00—11:45" })
])
})
])
})]);
});
it("maps a provisioned backend alert without inventing a condition link", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify(projectPage), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({
...runtimeSnapshot,
alert_availability: "ready",
alerts: [{
id: "backend-alert-alarm-1",
condition_id: null,
source: {
task_type: "scheduled_task",
task_id: "scada-diagnosis-5m",
run_id: "trigger-1"
},
title: "泵站通信告警",
description: "由 SCADA 数据诊断任务明确推送。",
occurred_at: "2026-09-17T02:29:00Z",
severity: "warning"
}]
}), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const snapshot = await fetchWorkbenchRuntimeSnapshot();
expect(snapshot.alertAvailability).toBe("ready");
expect(snapshot.alerts).toEqual([expect.objectContaining({
id: "backend-alert-alarm-1",
title: "泵站通信告警",
severity: "warning",
source: {
taskType: "scheduled_task",
taskId: "scada-diagnosis-5m",
runId: "trigger-1"
}
})]);
expect(snapshot.alerts[0]).not.toHaveProperty("conditionId");
});
});