feat(agent): add frontend action bridge
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { frontendActionCallIdPlugin } from "../../.opencode/plugins/frontend-action-call-id.js";
|
||||
|
||||
describe("frontend action call ID plugin", () => {
|
||||
it("overwrites forged IDs only for allowlisted tools", async () => {
|
||||
const hooks = await frontendActionCallIdPlugin({} as never);
|
||||
const before = hooks["tool.execute.before"];
|
||||
if (!before) throw new Error("hook missing");
|
||||
const actionOutput = { args: { x: 1, __frontend_action_call_id: "forged" } };
|
||||
await before({ tool: "zoom_to_map", sessionID: "session", callID: "real" }, actionOutput);
|
||||
expect(actionOutput.args.__frontend_action_call_id).toBe("real");
|
||||
|
||||
const ordinaryOutput = { args: { query: "water" } };
|
||||
await before({ tool: "web_search", sessionID: "session", callID: "ignored" }, ordinaryOutput);
|
||||
expect(ordinaryOutput.args).toEqual({ query: "water" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user