import { describe, expect, it } from "bun:test"; import { FrontendActionCoordinator, FrontendActionError, } from "../../src/frontendAction/coordinator.js"; import { frontendActionRegistry } from "../../src/frontendAction/registry.js"; import type { FrontendActionRequest } from "../../src/frontendAction/types.js"; const items = [ { sensor_id: "MP01", level: "high" }, { sensor_id: "MP02", level: "medium" }, ] as const; const start = ( coordinator: FrontendActionCoordinator, name = "render_scada_analysis", params: unknown = { items }, ) => { let emitted: FrontendActionRequest | undefined; coordinator.registerSink("session-1", (request) => { emitted = request; }); const pending = coordinator.request({ sessionId: "session-1", toolCallId: "call-1", name, params, }); if (!emitted) throw new Error("request was not emitted"); return { emitted, pending }; }; describe("FrontendActionCoordinator", () => { it("registers only the two view-risk SCADA actions with 15 second expiry", () => { expect(frontendActionRegistry).toEqual([ expect.objectContaining({ id: "render_scada_analysis", risk: "view", timeoutMs: 15_000 }), expect.objectContaining({ id: "clear_scada_analysis", risk: "view", timeoutMs: 15_000 }), ]); }); it("emits once and resolves with validated browser output", async () => { const coordinator = new FrontendActionCoordinator(); const { emitted, pending } = start(coordinator); expect(emitted).toMatchObject({ name: "render_scada_analysis", risk: "view", replayPolicy: "never", params: { items }, }); expect(emitted.expiresAt - emitted.issuedAt).toBe(15_000); const result = { version: "frontend-action-result@1" as const, actionId: emitted.actionId, status: "succeeded" as const, output: { rendered_ids: ["MP01", "MP02"], missing_ids: [], level_counts: { high: 1, medium: 1, low: 0, unrated: 0 }, fitted: true as const, }, completedAt: Date.now(), }; expect(coordinator.submitResult("session-1", result).duplicate).toBe(false); expect((await pending).output).toEqual(result.output); expect(coordinator.submitResult("session-1", result).duplicate).toBe(true); }); it("does not replay never-policy actions when the browser sink reconnects", async () => { const coordinator = new FrontendActionCoordinator(); const emitted: FrontendActionRequest[] = []; const unregister = coordinator.registerSink("session-1", (request) => emitted.push(request)); const pending = coordinator.request({ sessionId: "session-1", toolCallId: "call-no-replay", name: "clear_scada_analysis", params: {}, }); unregister(); coordinator.registerSink("session-1", (request) => emitted.push(request)); expect(emitted).toHaveLength(1); coordinator.submitResult("session-1", { version: "frontend-action-result@1", actionId: emitted[0].actionId, status: "succeeded", output: { cleared: true }, completedAt: Date.now(), }); await pending; }); it("accepts clear output and rejects invalid successful output", async () => { const coordinator = new FrontendActionCoordinator(); const { emitted, pending } = start(coordinator, "clear_scada_analysis", {}); expect(() => coordinator.submitResult("session-1", { version: "frontend-action-result@1", actionId: emitted.actionId, status: "succeeded", output: { cleared: false }, completedAt: Date.now(), })).toThrow("invalid frontend action output"); coordinator.submitResult("session-1", { version: "frontend-action-result@1", actionId: emitted.actionId, status: "succeeded", output: { cleared: true }, completedAt: Date.now(), }); expect((await pending).output).toEqual({ cleared: true }); }); it("validates item count, unique IDs, ID length, and levels", () => { const coordinator = new FrontendActionCoordinator(); coordinator.registerSink("s", () => undefined); const request = (value: unknown) => coordinator.request({ sessionId: "s", toolCallId: "call-validation", name: "render_scada_analysis", params: value, }); expect(() => request({ items: [] })).toThrow(FrontendActionError); expect(() => request({ items: Array.from({ length: 101 }, (_, index) => ({ sensor_id: `MP${index}`, level: "low" })) })).toThrow("invalid frontend action params"); expect(() => request({ items: [{ sensor_id: "MP01", level: "high" }, { sensor_id: "MP01", level: "low" }] })).toThrow("invalid frontend action params"); expect(() => request({ items: [{ sensor_id: "x".repeat(129), level: "low" }] })).toThrow("invalid frontend action params"); expect(() => request({ items: [{ sensor_id: "MP01", level: "critical" }] })).toThrow("invalid frontend action params"); }); it("keeps terminal results idempotent and rejects conflicts", async () => { const coordinator = new FrontendActionCoordinator(); const { emitted, pending } = start(coordinator, "clear_scada_analysis", {}); const result = { version: "frontend-action-result@1" as const, actionId: emitted.actionId, status: "succeeded" as const, output: { cleared: true as const }, completedAt: Date.now(), }; expect(() => coordinator.submitResult("other", result)).toThrow("session mismatch"); coordinator.submitResult("session-1", result); await pending; expect(() => coordinator.submitResult("session-1", { ...result, completedAt: result.completedAt + 1 })).toThrow("conflicts"); }); });