Files
openclaw/src/agents/pi-tools.params.test.ts
2026-04-05 12:53:15 +01:00

144 lines
4.2 KiB
TypeScript

import { Type } from "@sinclair/typebox";
import { describe, expect, it, vi } from "vitest";
import {
assertRequiredParams,
CLAUDE_PARAM_GROUPS,
patchToolSchemaForClaudeCompatibility,
wrapToolParamNormalization,
} from "./pi-tools.params.js";
describe("assertRequiredParams", () => {
it("patches Claude-compatible file tool schemas to disallow unknown parameters", () => {
const patched = patchToolSchemaForClaudeCompatibility({
name: "read",
label: "read",
description: "read a file",
parameters: Type.Object({
path: Type.String(),
offset: Type.Optional(Type.Number()),
limit: Type.Optional(Type.Number()),
}),
execute: vi.fn(),
});
expect((patched.parameters as { additionalProperties?: unknown }).additionalProperties).toBe(
false,
);
});
it("includes received keys in error when some params are present but content is missing", () => {
expect(() =>
assertRequiredParams(
{ file_path: "test.txt" },
[
{ keys: ["path", "file_path"], label: "path alias" },
{ keys: ["content"], label: "content" },
],
"write",
),
).toThrow(/\(received: file_path\)/);
});
it("shows normalized key in hint when called through wrapToolParamNormalization (file_path alias -> path)", async () => {
const tool = wrapToolParamNormalization(
{
name: "write",
label: "write",
description: "write a file",
parameters: {},
execute: vi.fn(),
},
CLAUDE_PARAM_GROUPS.write,
);
await expect(
tool.execute("id", { file_path: "test.txt" }, new AbortController().signal, vi.fn()),
).rejects.toThrow(/\(received: path\)/);
});
it("excludes null and undefined values from received hint", () => {
expect(() =>
assertRequiredParams(
{ file_path: "test.txt", content: null },
[
{ keys: ["path", "file_path"], label: "path alias" },
{ keys: ["content"], label: "content" },
],
"write",
),
).toThrow(/\(received: file_path\)[^,]/);
});
it("shows empty-string values for present params that still fail validation", () => {
expect(() =>
assertRequiredParams(
{ path: "/tmp/a.txt", content: " " },
[
{ keys: ["path", "file_path"], label: "path alias" },
{ keys: ["content"], label: "content" },
],
"write",
),
).toThrow(/\(received: path, content=<empty-string>\)/);
});
it("shows wrong-type values for present params that still fail validation", async () => {
const tool = wrapToolParamNormalization(
{
name: "write",
label: "write",
description: "write a file",
parameters: {},
execute: vi.fn(),
},
CLAUDE_PARAM_GROUPS.write,
);
await expect(
tool.execute(
"id",
{ file_path: "test.txt", content: { unexpected: true } },
new AbortController().signal,
vi.fn(),
),
).rejects.toThrow(/\(received: (?:path, content=<object>|content=<object>, path)\)/);
});
it("includes multiple received keys when several params are present", () => {
expect(() =>
assertRequiredParams(
{ path: "/tmp/a.txt", extra: "yes" },
[
{ keys: ["path", "file_path"], label: "path alias" },
{ keys: ["content"], label: "content" },
],
"write",
),
).toThrow(/\(received: path, extra\)/);
});
it("omits received hint when the record is empty", () => {
const err = (() => {
try {
assertRequiredParams({}, [{ keys: ["content"], label: "content" }], "write");
} catch (e) {
return e instanceof Error ? e.message : "";
}
return "";
})();
expect(err).not.toMatch(/received:/);
expect(err).toMatch(/Missing required parameter: content/);
});
it("does not throw when all required params are present", () => {
expect(() =>
assertRequiredParams(
{ path: "a.txt", content: "hello" },
[
{ keys: ["path", "file_path"], label: "path alias" },
{ keys: ["content"], label: "content" },
],
"write",
),
).not.toThrow();
});
});