import { describe, expect, it } from "bun:test"; import { FrontendActionCoordinator, FrontendActionError } from "../../src/frontendAction/coordinator.js"; import type { FrontendActionRequest } from "../../src/frontendAction/types.js"; const start = (coordinator: FrontendActionCoordinator, sessionId = "session-1") => { let emitted: FrontendActionRequest | undefined; coordinator.registerSink(sessionId, (request) => { emitted = request; }); const pending = coordinator.request({ sessionId, toolCallId: "call-1", name: "zoom_to_map", params: { x: 117.2, y: 39.1 } }); if (!emitted) throw new Error("request was not emitted"); return { emitted, pending }; }; describe("FrontendActionCoordinator", () => { it("emits once and resolves with a validated browser result", async () => { const coordinator = new FrontendActionCoordinator(); const { emitted, pending } = start(coordinator); const result = { version: "frontend-action-result@1" as const, actionId: emitted.actionId, status: "succeeded" as const, output: { zoom: 14 }, completedAt: Date.now() }; expect(coordinator.submitResult("session-1", result).duplicate).toBe(false); expect((await pending).output).toEqual({ zoom: 14 }); expect(coordinator.submitResult("session-1", result).duplicate).toBe(true); }); it("rejects conflicting terminal results and cross-session submissions", async () => { const coordinator = new FrontendActionCoordinator(); const { emitted, pending } = start(coordinator); const result = { version: "frontend-action-result@1" as const, actionId: emitted.actionId, status: "succeeded" as const, output: {}, 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"); }); it("cancels all pending actions for a session", async () => { const coordinator = new FrontendActionCoordinator(); const { pending } = start(coordinator); coordinator.cancelSession("session-1"); expect((await pending).status).toBe("cancelled"); }); it("validates params and requires an active browser sink", () => { const coordinator = new FrontendActionCoordinator(); expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "zoom_to_map", params: { x: "bad", y: 1 } })).toThrow(FrontendActionError); coordinator.registerSink("s", () => undefined); expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "unknown", params: {} })).toThrow("unknown frontend action"); }); it("requires a valid time range when opening history data", async () => { const coordinator = new FrontendActionCoordinator(); let emitted: FrontendActionRequest | undefined; coordinator.registerSink("s", (request) => { emitted = request; }); expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "view_history", params: { feature_infos: [["node-1", "junction"]], data_type: "realtime" }, })).toThrow("invalid frontend action params"); expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "view_history", params: { feature_infos: [["node-1", "junction"]], data_type: "realtime", start_time: "2026-07-14T00:00:00Z", end_time: "2026-07-14T00:00:00Z", }, })).toThrow("invalid frontend action params"); const pending = coordinator.request({ sessionId: "s", toolCallId: "c", name: "view_history", params: { feature_infos: [["node-1", "junction"]], data_type: "realtime", start_time: "2026-07-14T00:00:00Z", end_time: "2026-07-14T01:00:00Z", }, }); if (!emitted) throw new Error("request was not emitted"); coordinator.submitResult("s", { version: "frontend-action-result@1", actionId: emitted.actionId, status: "succeeded", output: { component: "HistoryPanel", rendered: true, itemCount: 1 }, completedAt: Date.now(), }); await expect(pending).resolves.toMatchObject({ status: "succeeded" }); }); });