feat(workbench): expand SCADA monitoring and investigation presentation

This commit is contained in:
2026-11-07 19:50:14 +08:00
parent 0801e161ec
commit ba71e2d529
133 changed files with 6754 additions and 1438 deletions
@@ -0,0 +1,63 @@
import { act, cleanup, renderHook } from "@testing-library/react";
import { afterEach, expect, it } from "vitest";
import { useScadaEventAtmosphere, type ScadaEventTask } from "./use-scada-event-atmosphere";
import type { WorkbenchAlert } from "../types";
const task: ScadaEventTask = {
task_id: "task-1", project_id: "project-1", task_type: "event_investigation", workflow: "scada_anomaly",
lifecycle_status: "open", title: "监测事件", created_at: "2026-07-16T00:00:00Z",
source: { kind: "external_system", external_id: "scada:1", external_revision: 1, event_kind: "scada_anomaly",
replay_context: { replay_id: "replay-1", knowledge_cutoff: "2026-07-16T00:00:00Z", batch_sequence: 1 } }
};
const alert: WorkbenchAlert = { id: "incident-1", conditionId: "operational-task:task-1", title: "SCADA 指标异常",
time: "08:00", description: "调查已启动", source: { kind: "scada_replay", replayId: "replay-1", ruleId: "scada_diagnosis", batchSequence: 1 } };
afterEach(() => { cleanup(); sessionStorage.clear(); });
it("retains an event after sensor-alert recovery, human waiting and execution failure, until event completion", () => {
const { result, rerender } = renderHook(({ alerts, tasks }) => useScadaEventAtmosphere("project-1", alerts, tasks),
{ initialProps: { alerts: [alert], tasks: [task] } });
expect(result.current.atmosphere?.count).toBe(1);
rerender({ alerts: [], tasks: [{ ...task, lifecycle_status: "waiting_human" }] });
expect(result.current.atmosphere?.alert?.conditionId).toBe(alert.conditionId);
rerender({ alerts: [], tasks: [{ ...task, lifecycle_status: "failed" }] });
expect(result.current.atmosphere).not.toBeNull();
rerender({ alerts: [alert], tasks: [{ ...task, lifecycle_status: "completed" }] });
expect(result.current.atmosphere).toBeNull();
});
it("restores unfinished events on reload and remembers a manual dismissal for that event", () => {
const first = renderHook(() => useScadaEventAtmosphere("project-1", null, [task]));
expect(first.result.current.atmosphere).not.toBeNull();
act(() => first.result.current.dismissAtmosphere(first.result.current.atmosphere!.id));
expect(first.result.current.atmosphere).toBeNull();
first.unmount();
const next = renderHook(() => useScadaEventAtmosphere("project-1", [alert], [task]));
expect(next.result.current.atmosphere).toBeNull();
});
it("keeps other events visible when one ends and shows a new event after the earlier group was dismissed", () => {
const second = { ...task, task_id: "task-2" };
const { result, rerender } = renderHook(({ tasks }) => useScadaEventAtmosphere("project-1", [], tasks),
{ initialProps: { tasks: [task, second] } });
expect(result.current.atmosphere?.count).toBe(2);
rerender({ tasks: [{ ...task, lifecycle_status: "cancelled" }, second] });
expect(result.current.atmosphere?.count).toBe(1);
act(() => result.current.dismissAtmosphere(result.current.atmosphere!.id));
rerender({ tasks: [second, { ...task, task_id: "task-3" }] });
expect(result.current.atmosphere?.count).toBe(1);
expect(result.current.atmosphere?.alert?.conditionId).toBe("operational-task:task-3");
});
it("isolates project state and lets Dev end only the local preview", () => {
const { result, rerender } = renderHook(({ project, tasks }) => useScadaEventAtmosphere(project, [], tasks),
{ initialProps: { project: "project-1", tasks: [] as ScadaEventTask[] } });
act(() => result.current.previewAtmosphere());
expect(result.current.atmosphere?.preview).toBe(true);
act(() => result.current.endAtmospherePreview());
expect(result.current.atmosphere).toBeNull();
rerender({ project: "project-1", tasks: [task] });
act(() => result.current.endAtmospherePreview());
expect(result.current.atmosphere?.preview).toBe(false);
rerender({ project: "project-2", tasks: [task] });
expect(result.current.atmosphere).toBeNull();
});