feat(workbench): integrate runtime data and map tooling
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user