91 lines
4.2 KiB
TypeScript
91 lines
4.2 KiB
TypeScript
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" });
|
|
});
|
|
});
|