Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<unknown>;
};

function registerTools(apiUrl: string): Tool[] {
let toolFactory: ((ctx: Record<string, unknown>) => 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<void>((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<void>((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);
},
);
});
18 changes: 13 additions & 5 deletions plugins/openclaw/openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,17 @@ function collectRecentConversationMessages(
function readString(
params: Record<string, unknown>,
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) {
throw new Error(`${label} required`);
}
return undefined;
}
return raw.trim();
return preserveWhitespace ? raw : raw.trim();
}

function readNumber(params: Record<string, unknown>, key: string): number | undefined {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading