import { beforeEach, describe, expect, it, vi } from "vitest"; import { FrontendActionExecutor } from "./executor"; import { parseFrontendActionRegistry, parseFrontendActionRequest, type FrontendActionResult } from "."; const storage = new Map(); beforeEach(() => { storage.clear(); vi.stubGlobal("sessionStorage", { getItem: (key: string) => storage.get(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value) }); }); describe("SCADA frontend actions", () => { it("accepts registered view actions and rejects unknown action names", () => { expect(parseFrontendActionRegistry({ schema_version: "frontend-action-registry@2", actions: [ { id: "render_scada_analysis", version: "frontend-action@2" }, { id: "clear_scada_analysis", version: "frontend-action@2" }, { id: "zoom_to_map", version: "frontend-action@2" } ] })?.actions.map((action) => action.id)).toEqual([ "render_scada_analysis", "clear_scada_analysis", "zoom_to_map" ]); expect(parseFrontendActionRequest(createRequest("execute_script"))).toBeNull(); expect(parseFrontendActionRequest(createRequest("render_scada_analysis"))).not.toBeNull(); }); it("submits the real browser output and replays the terminal receipt idempotently", async () => { const execute = vi.fn(async () => ({ rendered_ids: ["MP01"], missing_ids: [], level_counts: { high: 1, medium: 0, low: 0, unrated: 0 }, fitted: true })); const submit = vi.fn(async ( _sessionId: string, _actionId: string, _result: FrontendActionResult ) => undefined); const executor = new FrontendActionExecutor(execute, submit); const request = createRequest("render_scada_analysis"); await executor.handle(request, "session-1"); await executor.handle(request, "session-1"); expect(execute).toHaveBeenCalledTimes(1); expect(submit).toHaveBeenCalledTimes(2); expect(submit.mock.calls[0][2]).toMatchObject({ version: "frontend-action-result@2", status: "succeeded", output: { rendered_ids: ["MP01"], fitted: true } }); expect(submit.mock.calls[1][2]).toEqual(submit.mock.calls[0][2]); }); }); function createRequest(name: string) { return { version: "frontend-action@2", actionId: "action-1", toolCallId: "call-1", sessionId: "session-1", command: { action: name, items: [{ device_id: "MP01", level: "high" }] }, issuedAt: Date.now(), expiresAt: Date.now() + 15_000 }; } describe("ordered v2 actions", () => { it("rejects v1 envelopes, legacy name/params and malformed canonical commands", () => { const request = createRequest("render_scada_analysis"); expect(parseFrontendActionRequest({ ...request, version: "frontend-action@1" })).toBeNull(); expect(parseFrontendActionRequest({ ...request, name: "render_scada_analysis", params: {} })).toBeNull(); expect(parseFrontendActionRequest({ ...request, command: { action: "zoom_to_map", center: [181, 31], zoom: 14 } })).toBeNull(); }); it("keeps commands in stream order and cancels queued work when a session changes", async () => { let release!: () => void; const first = new Promise((resolve) => { release = resolve; }); const execute = vi.fn(async (request, signal) => { await first; signal.throwIfAborted(); return { action: request.command.action }; }); const submit = vi.fn(async () => {}); const executor = new FrontendActionExecutor(execute, submit); const one = executor.handle(createRequest("render_scada_analysis"), "session-1"); const two = executor.handle({ ...createRequest("render_scada_analysis"), actionId: "action-2" }, "session-1"); await Promise.resolve(); expect(execute).toHaveBeenCalledTimes(1); executor.cancelAll(); release(); await Promise.all([one, two]); expect(execute).toHaveBeenCalledTimes(1); expect(submit.mock.calls.map((call) => (call as unknown[])[2])).toEqual([ expect.objectContaining({ status: "cancelled" }), expect.objectContaining({ status: "cancelled" }) ]); }); });