chore: disable external and frontend tools
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { FRONTEND_ACTION_TOOLS, frontendActionCallIdPlugin, server } from "../../.opencode/plugins/frontend-action-call-id.js";
|
||||
import { frontendActionRegistry } from "../../src/frontendAction/registry.js";
|
||||
|
||||
const frontendActionToolNames = () => readdirSync(".opencode/tools")
|
||||
.filter((file) => file.endsWith(".ts"))
|
||||
.filter((file) => readFileSync(join(".opencode/tools", file), "utf8").includes("executeFrontendAction("))
|
||||
.map((file) => file.replace(/\.ts$/, ""))
|
||||
.sort();
|
||||
|
||||
describe("frontend action call ID plugin", () => {
|
||||
it("is registered in the OpenCode project config", () => {
|
||||
const config = JSON.parse(readFileSync("opencode.json", "utf8")) as { plugin?: unknown[] };
|
||||
expect(config.plugin).toContain("./.opencode/plugins/frontend-action-call-id.ts");
|
||||
});
|
||||
|
||||
it("exposes the server export required by OpenCode", () => {
|
||||
expect(server).toBe(frontendActionCallIdPlugin);
|
||||
});
|
||||
|
||||
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" });
|
||||
});
|
||||
|
||||
it("covers every tool that uses the frontend action bridge", () => {
|
||||
const bridgeTools = frontendActionToolNames();
|
||||
expect([...FRONTEND_ACTION_TOOLS].sort()).toEqual(bridgeTools);
|
||||
expect(frontendActionRegistry.map((action) => action.id).sort()).toEqual(bridgeTools);
|
||||
});
|
||||
});
|
||||
@@ -1,47 +1,12 @@
|
||||
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 () => {
|
||||
it("rejects frontend actions when the registry is empty", () => {
|
||||
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");
|
||||
expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "zoom_to_map", params: {} })).toThrow(FrontendActionError);
|
||||
expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "zoom_to_map", params: {} })).toThrow("unknown frontend action");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
const disabledServerTools = [
|
||||
"geocode",
|
||||
"get_project_context",
|
||||
"list_scada_assets",
|
||||
"query_scada_readings",
|
||||
"web_search",
|
||||
];
|
||||
|
||||
describe("disabled opencode tools", () => {
|
||||
it("does not expose Server-dependent or frontend-action tools", () => {
|
||||
for (const toolName of disabledServerTools) {
|
||||
expect(existsSync(`.opencode/tools/${toolName}.ts`)).toBeFalse();
|
||||
}
|
||||
expect(existsSync(".opencode/tools/server_api_shared.ts")).toBeFalse();
|
||||
expect(existsSync(".opencode/tools/zoom_to_map.ts")).toBeFalse();
|
||||
expect(existsSync(".opencode/tools/frontend_action.ts")).toBeFalse();
|
||||
expect(existsSync(".opencode/plugins/frontend-action-call-id.ts")).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
describe("SCADA opencode tools", () => {
|
||||
it("documents asset listing and reading query parameters", () => {
|
||||
const listTool = readFileSync(".opencode/tools/list_scada_assets.ts", "utf8");
|
||||
const queryTool = readFileSync(".opencode/tools/query_scada_readings.ts", "utf8");
|
||||
|
||||
expect(listTool).toContain("无请求参数");
|
||||
expect(listTool).toContain("assets[].scada_id");
|
||||
expect(queryTool).toContain("asset_ids");
|
||||
expect(queryTool).toContain("observed_from");
|
||||
expect(queryTool).toContain("observed_to");
|
||||
expect(queryTool).toContain("granularity");
|
||||
expect(queryTool).toContain("metrics");
|
||||
expect(queryTool).toContain("半开区间");
|
||||
expect(queryTool).toContain("不要使用 GeoServer feature id/fid、external_id 或 site_external_id");
|
||||
});
|
||||
});
|
||||
@@ -535,67 +535,4 @@ describe("streamPromptResponse", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps zoom_to_map tool calls to UIEnvelope SSE payloads", async () => {
|
||||
const runtime = {
|
||||
subscribeEvents: async () =>
|
||||
createEventStream([
|
||||
{
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID: "runtime-session-1",
|
||||
part: {
|
||||
id: "tool-part-map",
|
||||
sessionID: "runtime-session-1",
|
||||
messageID: "message-1",
|
||||
type: "tool",
|
||||
callID: "call-map",
|
||||
tool: "zoom_to_map",
|
||||
state: {
|
||||
status: "running",
|
||||
input: {
|
||||
reason: "定位 SCADA 资产附近位置",
|
||||
x: 121.47,
|
||||
y: 31.23,
|
||||
source_crs: "EPSG:4326",
|
||||
},
|
||||
time: { start: Date.now() },
|
||||
},
|
||||
},
|
||||
time: Date.now(),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "session.idle",
|
||||
properties: {
|
||||
sessionID: "runtime-session-1",
|
||||
},
|
||||
},
|
||||
]),
|
||||
prompt: async () => undefined,
|
||||
messages: async () => [],
|
||||
} as unknown as OpencodeRuntimeAdapter;
|
||||
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||
|
||||
await streamPromptResponse({
|
||||
runtime,
|
||||
sessionId: "runtime-session-1",
|
||||
clientSessionId: "client-session-1",
|
||||
message: "chart",
|
||||
write: (event, data) => events.push({ event, data }),
|
||||
});
|
||||
|
||||
expect(events.find((item) => item.event === "tool_call")?.data).toMatchObject({
|
||||
session_id: "client-session-1",
|
||||
tool: "zoom_to_map",
|
||||
});
|
||||
expect(events.find((item) => item.event === "ui_envelope")?.data).toMatchObject({
|
||||
session_id: "client-session-1",
|
||||
envelope: {
|
||||
kind: "map_action",
|
||||
schemaVersion: "agent-ui@1",
|
||||
action: "zoom_to_map",
|
||||
surface: "map_overlay",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { ServerApiGateway } from "../../src/serverApi/gateway.js";
|
||||
|
||||
const context = {
|
||||
actorKey: "user",
|
||||
clientSessionId: "session",
|
||||
projectId: "1c60fd8c-89fb-47ca-84f1-66ec10f5bf73",
|
||||
projectKey: "project",
|
||||
sessionId: "runtime",
|
||||
traceId: "trace",
|
||||
};
|
||||
|
||||
describe("ServerApiGateway project context", () => {
|
||||
test("uses the metadata path and accepts project_id responses", async () => {
|
||||
let requestedPath = "";
|
||||
const fetchImpl = (async (input: URL | RequestInfo) => {
|
||||
requestedPath = new URL(input.toString()).pathname;
|
||||
return Response.json([{
|
||||
project_id: context.projectId,
|
||||
code: "lingang",
|
||||
name: "SCADA 物联网监测",
|
||||
description: null,
|
||||
status: "active",
|
||||
project_role: "owner",
|
||||
gs_workspace: "lingang",
|
||||
map_extent: { xmin: 120.0, ymin: 30.0, xmax: 122.0, ymax: 32.0 },
|
||||
}]);
|
||||
}) as typeof fetch;
|
||||
const gateway = new ServerApiGateway({} as never, fetchImpl);
|
||||
|
||||
const result = await gateway.execute("get_project_context", {}, context);
|
||||
|
||||
expect(requestedPath).toBe("/api/v1/projects");
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
data: { project_id: context.projectId, status: "active", project_role: "owner" },
|
||||
});
|
||||
});
|
||||
|
||||
test("does not select the removed id response shape", async () => {
|
||||
const fetchImpl = (async () =>
|
||||
Response.json([{ id: context.projectId, code: "lingang", name: "SCADA 物联网监测" }])) as unknown as typeof fetch;
|
||||
const gateway = new ServerApiGateway({} as never, fetchImpl);
|
||||
|
||||
const result = await gateway.execute("get_project_context", {}, context);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "NOT_FOUND" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { domainToolSchemas } from "../../src/serverApi/schemas.js";
|
||||
|
||||
describe("domain tool validation", () => {
|
||||
test("rejects unknown metrics and reversed time", () => {
|
||||
const base = { asset_ids: ["940468c1-87cc-478f-b9f5-537ffbdb9025"], observed_from: "2026-07-14T02:00:00Z", observed_to: "2026-07-14T01:00:00Z", granularity: "5m" };
|
||||
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["ph_mean"] }).success).toBeFalse();
|
||||
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["flow_mean"] }).success).toBeFalse();
|
||||
});
|
||||
|
||||
test("accepts four current metrics and rejects removed metrics", () => {
|
||||
const base = { asset_ids: ["940468c1-87cc-478f-b9f5-537ffbdb9025"], observed_from: "2026-07-14T01:00:00Z", observed_to: "2026-07-14T02:00:00Z", granularity: "5m" };
|
||||
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["conductivity_mean", "level_mean", "flow_mean", "temperature_mean"] }).success).toBeTrue();
|
||||
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["flow_total"] }).success).toBeFalse();
|
||||
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["velocity_mean"] }).success).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -3,23 +3,8 @@ import { describe, expect, it } from "bun:test";
|
||||
import { toUiEnvelopeFromToolCall } from "../../src/uiEnvelope/fromToolCall.js";
|
||||
|
||||
describe("toUiEnvelopeFromToolCall", () => {
|
||||
it("maps zoom_to_map calls to map action envelopes", () => {
|
||||
const envelope = toUiEnvelopeFromToolCall({
|
||||
tool: "zoom_to_map",
|
||||
reason: "定位 SCADA 资产附近位置",
|
||||
params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" },
|
||||
});
|
||||
|
||||
expect(envelope).toMatchObject({
|
||||
kind: "map_action",
|
||||
action: "zoom_to_map",
|
||||
surface: "map_overlay",
|
||||
params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" },
|
||||
fallbackText: "定位 SCADA 资产附近位置",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not map removed visual tools", () => {
|
||||
expect(toUiEnvelopeFromToolCall({ tool: "zoom_to_map", params: {} })).toBeNull();
|
||||
expect(toUiEnvelopeFromToolCall({ tool: "show_chart", params: {} })).toBeNull();
|
||||
expect(toUiEnvelopeFromToolCall({ tool: "view_scada", params: { asset_ids: ["A-1"] } })).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user