From 41046902369709c0999c686fd7cdd73f3136e65e Mon Sep 17 00:00:00 2001 From: MOLW <85048713+MatrixAdventurer@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:24:25 +0800 Subject: [PATCH] fix(openclaw): preserve exact branch selectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 由 LW 授权的 ChatGPT Business 智能体提交。 --- .../__tests__/branch-tool-selector.test.ts | 65 +++++++++++++++++++ plugins/openclaw/openclaw/index.ts | 18 +++-- 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 plugins/openclaw/openclaw/__tests__/branch-tool-selector.test.ts diff --git a/plugins/openclaw/openclaw/__tests__/branch-tool-selector.test.ts b/plugins/openclaw/openclaw/__tests__/branch-tool-selector.test.ts new file mode 100644 index 00000000..99371899 --- /dev/null +++ b/plugins/openclaw/openclaw/__tests__/branch-tool-selector.test.ts @@ -0,0 +1,65 @@ +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import { describe, expect, it } from "vitest"; +import plugin from "../index.js"; + +type Tool = { + name: string; + execute: (toolCallId: string, params: unknown) => Promise; +}; + +function registerTools(apiUrl: string): Tool[] { + let toolFactory: ((ctx: Record) => Tool[]) | undefined; + plugin.register({ + pluginConfig: { + backend: "api", + apiUrl, + apiKey: "test-key", + }, + logger: { info() {}, warn() {} }, + on() {}, + registerTool(factory: typeof toolFactory) { toolFactory = factory; }, + registerCli() {}, + registerService() {}, + } as any); + return toolFactory!({}); +} + +describe("public branch selector tools", () => { + it.each([ + ["memory_checkout", "POST", " release ", "/v1/branches/%20release%20/checkout"], + ["memory_branch_delete", "DELETE", " release ", "/v1/branches/%20release%20"], + ["memory_checkout", "POST", "\trelease\n", "/v1/branches/%09release%0A/checkout"], + ["memory_branch_delete", "DELETE", "\trelease\n", "/v1/branches/%09release%0A"], + ["memory_checkout", "POST", "ordinary", "/v1/branches/ordinary/checkout"], + ["memory_branch_delete", "DELETE", "ordinary", "/v1/branches/ordinary"], + ])("preserves the exact %s %s selector %j as %s", async (toolName, method, name, expectedPath) => { + const requests: Array<{ method: string; url: string }> = []; + const server = createServer((req, res) => { + requests.push({ method: req.method!, url: req.url! }); + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ result: "ok" })); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const apiUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + + try { + const tool = registerTools(apiUrl).find((candidate) => candidate.name === toolName)!; + await tool.execute("call-1", { name }); + expect(requests).toEqual([{ method, url: expectedPath }]); + } finally { + await new Promise((resolve, reject) => + server.close((error) => error ? reject(error) : resolve()), + ); + } + }); + + it.each(["memory_checkout", "memory_branch_delete"])( + "rejects an empty %s selector before making a request", + async (toolName) => { + const tool = registerTools("http://127.0.0.1:1") + .find((candidate) => candidate.name === toolName)!; + await expect(tool.execute("call-1", { name: " \t\n " })).rejects.toThrow(/name required/i); + }, + ); +}); diff --git a/plugins/openclaw/openclaw/index.ts b/plugins/openclaw/openclaw/index.ts index dd2c6590..c1cb352e 100644 --- a/plugins/openclaw/openclaw/index.ts +++ b/plugins/openclaw/openclaw/index.ts @@ -160,9 +160,9 @@ function collectRecentConversationMessages( function readString( params: Record, key: string, - options: { required?: boolean; label?: string } = {}, + options: { required?: boolean; label?: string; preserveWhitespace?: boolean } = {}, ): string | undefined { - const { required = false, label = key } = options; + const { required = false, label = key, preserveWhitespace = false } = options; const raw = params[key]; if (typeof raw !== "string" || !raw.trim()) { if (required) { @@ -170,7 +170,7 @@ function readString( } return undefined; } - return raw.trim(); + return preserveWhitespace ? raw : raw.trim(); } function readNumber(params: Record, key: string): number | undefined { @@ -1368,7 +1368,11 @@ const plugin = { execute: async (_toolCallId: string, rawParams: unknown) => { const params = asRecord(rawParams) ?? {}; const userId = resolveUserId(config, ctx, readString(params, "userId")); - const name = readString(params, "name", { required: true, label: "name" })!; + const name = readString(params, "name", { + required: true, + label: "name", + preserveWhitespace: true, + })!; const result = await client.branchCheckout({ userId, name }); return jsonResult({ userId, @@ -1391,7 +1395,11 @@ const plugin = { execute: async (_toolCallId: string, rawParams: unknown) => { const params = asRecord(rawParams) ?? {}; const userId = resolveUserId(config, ctx, readString(params, "userId")); - const name = readString(params, "name", { required: true, label: "name" })!; + const name = readString(params, "name", { + required: true, + label: "name", + preserveWhitespace: true, + })!; const result = await client.branchDelete({ userId, name }); return jsonResult({ userId,