chore: disable external and frontend tools

This commit is contained in:
2026-07-20 17:27:46 +08:00
parent c6231f7b00
commit 72019ac1f2
31 changed files with 63 additions and 899 deletions
-42
View File
@@ -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);
});
});
+3 -38
View File
@@ -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");
});
});
+22
View File
@@ -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");
});
});
-63
View File
@@ -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",
},
});
});
});
-53
View File
@@ -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" },
});
});
});
-17
View File
@@ -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();
});
});
+1 -16
View File
@@ -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();
});