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
@@ -153,7 +153,7 @@ describe("live investigation presentation following", () => {
test.channel.publish({ type: "events", events: [event(12, openChart), event(20, openChart), event(21, closeChart)] });
oldDisplay.resolve();
await vi.waitFor(() => expect(execute.mock.calls.map(([command]) => command.action)).toEqual(["highlight_features", "close_chart"]));
expect(test.clear.mock.calls.length).toBeGreaterThan(clearCount);
expect(test.clear.mock.calls.length).toBe(clearCount);
expect(execute.mock.calls[1]?.[1].aborted).toBe(false);
} finally { oldDisplay.resolve(); test.follower.stop(); }
});
@@ -199,3 +199,51 @@ it("reads saved history even before the live event stream opens", async () => {
expect(test.execute).not.toHaveBeenCalled();
} finally { test.follower.stop(); }
});
it("keeps successful results across disconnect and failed reconnect, clearing only for a new identity", async () => {
let visible = "saved chart";
const clear = vi.fn(() => { visible = ""; });
const fetchCursor = vi.fn(async () => cursor(10));
const test = setup({ clear, fetchCursor });
test.follower.start(); test.channel.publish({ type: "open" });
try {
await vi.waitFor(() => expect(test.onStatus).toHaveBeenCalledWith("live"));
visible = "saved chart";
clear.mockClear();
test.channel.publish({ type: "disconnect" });
expect(visible).toBe("saved chart");
expect(clear).not.toHaveBeenCalled();
fetchCursor.mockRejectedValueOnce(new Error("offline"));
test.channel.publish({ type: "open" });
await vi.waitFor(() => expect(test.onStatus).toHaveBeenCalledWith("error", "offline"));
expect(visible).toBe("saved chart");
fetchCursor.mockResolvedValueOnce(cursor(20, 2));
test.channel.publish({ type: "open" });
await vi.waitFor(() => expect(clear).toHaveBeenCalledTimes(1));
expect(visible).toBe("");
} finally { test.follower.stop(); }
});
it("clears a changed generation before hydrating its saved results and ignores a stale connection", async () => {
const stale = deferred<InvestigationPresentationCursor>();
const order: string[] = [];
const fetchCursor = vi.fn(async () => cursor(10))
.mockResolvedValueOnce(cursor(10))
.mockImplementationOnce(() => stale.promise)
.mockResolvedValueOnce(cursor(30, 2));
const test = setup({ fetchCursor, clear: () => order.push("clear"), loadHistory: async (next) => { order.push(`history:${next.execution_generation}`); } });
test.follower.start();
try {
await vi.waitFor(() => expect(order).toContain("history:1"));
order.length = 0;
test.channel.publish({ type: "open" });
test.channel.publish({ type: "disconnect" });
test.channel.publish({ type: "open" });
await vi.waitFor(() => expect(order).toContain("history:2"));
expect(order).toEqual(["clear", "history:2"]);
stale.resolve(cursor(20));
await Promise.resolve();
expect(order).toEqual(["clear", "history:2"]);
} finally { stale.resolve(cursor(20)); test.follower.stop(); }
});