feat: add operational timeline workspace

This commit is contained in:
2026-08-19 16:42:41 +08:00
parent d2f2895c36
commit 45f1ebee7e
19 changed files with 1127 additions and 105 deletions
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import type { ScheduledConditionItem } from "../types";
import { clusterOperationalEvents, createOperationalEvents } from "./operational-timeline-model";
const workOrder: ScheduledConditionItem = {
id: "work-1", kind: "work_order", code: "WO-1", scheduledAt: "2026-08-19T10:00:00.000Z",
title: "阀门调整", summary: "调整阀门", status: "running", riskLevel: "attention", updatedAt: 1,
durationMinutes: 40, source: "调度方案", location: "边界阀门 BV-07", dispatcher: "调度",
assignee: "班组", priority: "urgent", replyWindowMinutes: 20, stages: [], replyRequirements: []
};
describe("operational timeline model", () => {
it("creates linked plan and actual events for active work orders", () => {
const events = createOperationalEvents([workOrder]);
expect(events.map((event) => event.lane)).toEqual(["plan", "actual"]);
expect(events[0].correlationId).toBe(events[1].correlationId);
});
it("only promotes abnormal condition runs into exception events", () => {
const normal = {
id: "condition-normal", kind: "condition", taskId: "scada-diagnosis", sessionId: "s-1",
scheduledAt: "2026-08-19T10:00:00.000Z", title: "诊断", summary: "正常", status: "completed",
riskLevel: "normal", updatedAt: 1
} satisfies ScheduledConditionItem;
const abnormal = { ...normal, id: "condition-error", status: "error", riskLevel: "critical" } satisfies ScheduledConditionItem;
expect(createOperationalEvents([normal])).toHaveLength(0);
expect(createOperationalEvents([abnormal])[0].lane).toBe("exception");
});
it("aggregates nearby events into ten minute lane buckets", () => {
const events = createOperationalEvents([workOrder]);
const nearbyPlan = {
...events[0],
id: "plan-nearby",
cursorTime: "2026-08-19T10:05:00.000Z"
};
const clusters = clusterOperationalEvents([...events, nearbyPlan], 10);
expect(clusters).toHaveLength(2);
expect(clusters.find((cluster) => cluster.lane === "plan")?.events).toHaveLength(2);
});
});