From aed8dc1adee4f028a92d2a770673c99ecbfec9a8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 23 Jan 2026 07:34:46 +0000 Subject: [PATCH] test: consolidate pi-tools shards --- ...aliases-schemas-without-dropping-a.test.ts | 147 ---------- ...aliases-schemas-without-dropping-b.test.ts | 12 +- ...aliases-schemas-without-dropping-d.test.ts | 5 +- ...aliases-schemas-without-dropping-e.test.ts | 119 -------- ...aliases-schemas-without-dropping-g.test.ts | 31 -- ...e-aliases-schemas-without-dropping.test.ts | 274 +++++++++++++++++- 6 files changed, 281 insertions(+), 307 deletions(-) delete mode 100644 src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts delete mode 100644 src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts delete mode 100644 src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts deleted file mode 100644 index d66fb555f..000000000 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { __testing, createClawdbotCodingTools } from "./pi-tools.js"; -import { createBrowserTool } from "./tools/browser-tool.js"; - -const defaultTools = createClawdbotCodingTools(); - -describe("createClawdbotCodingTools", () => { - it("keeps browser tool schema OpenAI-compatible without normalization", () => { - const browser = createBrowserTool(); - const schema = browser.parameters as { type?: unknown; anyOf?: unknown }; - expect(schema.type).toBe("object"); - expect(schema.anyOf).toBeUndefined(); - }); - it("mentions Chrome extension relay in browser tool description", () => { - const browser = createBrowserTool(); - expect(browser.description).toMatch(/Chrome extension/i); - expect(browser.description).toMatch(/profile="chrome"/i); - }); - it("keeps browser tool schema properties after normalization", () => { - const browser = defaultTools.find((tool) => tool.name === "browser"); - expect(browser).toBeDefined(); - const parameters = browser?.parameters as { - anyOf?: unknown[]; - properties?: Record; - required?: string[]; - }; - expect(parameters.properties?.action).toBeDefined(); - expect(parameters.properties?.target).toBeDefined(); - expect(parameters.properties?.controlUrl).toBeDefined(); - expect(parameters.properties?.targetUrl).toBeDefined(); - expect(parameters.properties?.request).toBeDefined(); - expect(parameters.required ?? []).toContain("action"); - }); - it("exposes raw for gateway config.apply tool calls", () => { - const gateway = defaultTools.find((tool) => tool.name === "gateway"); - expect(gateway).toBeDefined(); - - const parameters = gateway?.parameters as { - type?: unknown; - required?: string[]; - properties?: Record; - }; - expect(parameters.type).toBe("object"); - expect(parameters.properties?.raw).toBeDefined(); - expect(parameters.required ?? []).not.toContain("raw"); - }); - it("flattens anyOf-of-literals to enum for provider compatibility", () => { - const browser = defaultTools.find((tool) => tool.name === "browser"); - expect(browser).toBeDefined(); - - const parameters = browser?.parameters as { - properties?: Record; - }; - const action = parameters.properties?.action as - | { - type?: unknown; - enum?: unknown[]; - anyOf?: unknown[]; - } - | undefined; - - expect(action?.type).toBe("string"); - expect(action?.anyOf).toBeUndefined(); - expect(Array.isArray(action?.enum)).toBe(true); - expect(action?.enum).toContain("act"); - - const snapshotFormat = parameters.properties?.snapshotFormat as - | { - type?: unknown; - enum?: unknown[]; - anyOf?: unknown[]; - } - | undefined; - expect(snapshotFormat?.type).toBe("string"); - expect(snapshotFormat?.anyOf).toBeUndefined(); - expect(snapshotFormat?.enum).toEqual(["aria", "ai"]); - }); - it("inlines local $ref before removing unsupported keywords", () => { - const cleaned = __testing.cleanToolSchemaForGemini({ - type: "object", - properties: { - foo: { $ref: "#/$defs/Foo" }, - }, - $defs: { - Foo: { type: "string", enum: ["a", "b"] }, - }, - }) as { - $defs?: unknown; - properties?: Record; - }; - - expect(cleaned.$defs).toBeUndefined(); - expect(cleaned.properties).toBeDefined(); - expect(cleaned.properties?.foo).toMatchObject({ - type: "string", - enum: ["a", "b"], - }); - }); - it("cleans tuple items schemas", () => { - const cleaned = __testing.cleanToolSchemaForGemini({ - type: "object", - properties: { - tuples: { - type: "array", - items: [ - { type: "string", format: "uuid" }, - { type: "number", minimum: 1 }, - ], - }, - }, - }) as { - properties?: Record; - }; - - const tuples = cleaned.properties?.tuples as { items?: unknown } | undefined; - const items = Array.isArray(tuples?.items) ? tuples?.items : []; - const first = items[0] as { format?: unknown } | undefined; - const second = items[1] as { minimum?: unknown } | undefined; - - expect(first?.format).toBeUndefined(); - expect(second?.minimum).toBeUndefined(); - }); - it("drops null-only union variants without flattening other unions", () => { - const cleaned = __testing.cleanToolSchemaForGemini({ - type: "object", - properties: { - parentId: { anyOf: [{ type: "string" }, { type: "null" }] }, - count: { oneOf: [{ type: "string" }, { type: "number" }] }, - }, - }) as { - properties?: Record; - }; - - const parentId = cleaned.properties?.parentId as - | { type?: unknown; anyOf?: unknown; oneOf?: unknown } - | undefined; - expect(parentId?.anyOf).toBeUndefined(); - expect(parentId?.oneOf).toBeUndefined(); - expect(parentId?.type).toBe("string"); - - const count = cleaned.properties?.count as - | { type?: unknown; anyOf?: unknown; oneOf?: unknown } - | undefined; - expect(count?.anyOf).toBeUndefined(); - expect(Array.isArray(count?.oneOf)).toBe(true); - }); -}); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts index de6bc0a19..e440ecaeb 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts @@ -2,9 +2,10 @@ import { describe, expect, it } from "vitest"; import type { ClawdbotConfig } from "../config/config.js"; import { createClawdbotCodingTools } from "./pi-tools.js"; +const defaultTools = createClawdbotCodingTools(); + describe("createClawdbotCodingTools", () => { it("preserves action enums in normalized schemas", () => { - const tools = createClawdbotCodingTools(); const toolNames = ["browser", "canvas", "nodes", "cron", "gateway", "message"]; const collectActionValues = (schema: unknown, values: Set): void => { @@ -24,7 +25,7 @@ describe("createClawdbotCodingTools", () => { }; for (const name of toolNames) { - const tool = tools.find((candidate) => candidate.name === name); + const tool = defaultTools.find((candidate) => candidate.name === name); expect(tool).toBeDefined(); const parameters = tool?.parameters as { properties?: Record; @@ -44,10 +45,9 @@ describe("createClawdbotCodingTools", () => { } }); it("includes exec and process tools by default", () => { - const tools = createClawdbotCodingTools(); - expect(tools.some((tool) => tool.name === "exec")).toBe(true); - expect(tools.some((tool) => tool.name === "process")).toBe(true); - expect(tools.some((tool) => tool.name === "apply_patch")).toBe(false); + expect(defaultTools.some((tool) => tool.name === "exec")).toBe(true); + expect(defaultTools.some((tool) => tool.name === "process")).toBe(true); + expect(defaultTools.some((tool) => tool.name === "apply_patch")).toBe(false); }); it("gates apply_patch behind tools.exec.applyPatch for OpenAI models", () => { const config: ClawdbotConfig = { diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts index 070452ef8..f493164cd 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts @@ -5,10 +5,11 @@ import sharp from "sharp"; import { describe, expect, it } from "vitest"; import { createClawdbotCodingTools } from "./pi-tools.js"; +const defaultTools = createClawdbotCodingTools(); + describe("createClawdbotCodingTools", () => { it("keeps read tool image metadata intact", async () => { - const tools = createClawdbotCodingTools(); - const readTool = tools.find((tool) => tool.name === "read"); + const readTool = defaultTools.find((tool) => tool.name === "read"); expect(readTool).toBeDefined(); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-")); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts deleted file mode 100644 index 4bafc4118..000000000 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createClawdbotCodingTools } from "./pi-tools.js"; - -describe("createClawdbotCodingTools", () => { - it("applies tool profiles before allow/deny policies", () => { - const tools = createClawdbotCodingTools({ - config: { tools: { profile: "messaging" } }, - }); - const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("message")).toBe(true); - expect(names.has("sessions_send")).toBe(true); - expect(names.has("sessions_spawn")).toBe(false); - expect(names.has("exec")).toBe(false); - expect(names.has("browser")).toBe(false); - }); - it("expands group shorthands in global tool policy", () => { - const tools = createClawdbotCodingTools({ - config: { tools: { allow: ["group:fs"] } }, - }); - const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("read")).toBe(true); - expect(names.has("write")).toBe(true); - expect(names.has("edit")).toBe(true); - expect(names.has("exec")).toBe(false); - expect(names.has("browser")).toBe(false); - }); - it("expands group shorthands in global tool deny policy", () => { - const tools = createClawdbotCodingTools({ - config: { tools: { deny: ["group:fs"] } }, - }); - const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("read")).toBe(false); - expect(names.has("write")).toBe(false); - expect(names.has("edit")).toBe(false); - expect(names.has("exec")).toBe(true); - }); - it("lets agent profiles override global profiles", () => { - const tools = createClawdbotCodingTools({ - sessionKey: "agent:work:main", - config: { - tools: { profile: "coding" }, - agents: { - list: [{ id: "work", tools: { profile: "messaging" } }], - }, - }, - }); - const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("message")).toBe(true); - expect(names.has("exec")).toBe(false); - expect(names.has("read")).toBe(false); - }); - it("removes unsupported JSON Schema keywords for Cloud Code Assist API compatibility", () => { - const tools = createClawdbotCodingTools(); - - // Helper to recursively check schema for unsupported keywords - const unsupportedKeywords = new Set([ - "patternProperties", - "additionalProperties", - "$schema", - "$id", - "$ref", - "$defs", - "definitions", - "examples", - "minLength", - "maxLength", - "minimum", - "maximum", - "multipleOf", - "pattern", - "format", - "minItems", - "maxItems", - "uniqueItems", - "minProperties", - "maxProperties", - ]); - - const findUnsupportedKeywords = (schema: unknown, path: string): string[] => { - const found: string[] = []; - if (!schema || typeof schema !== "object") return found; - if (Array.isArray(schema)) { - schema.forEach((item, i) => { - found.push(...findUnsupportedKeywords(item, `${path}[${i}]`)); - }); - return found; - } - - const record = schema as Record; - const properties = - record.properties && - typeof record.properties === "object" && - !Array.isArray(record.properties) - ? (record.properties as Record) - : undefined; - if (properties) { - for (const [key, value] of Object.entries(properties)) { - found.push(...findUnsupportedKeywords(value, `${path}.properties.${key}`)); - } - } - - for (const [key, value] of Object.entries(record)) { - if (key === "properties") continue; - if (unsupportedKeywords.has(key)) { - found.push(`${path}.${key}`); - } - if (value && typeof value === "object") { - found.push(...findUnsupportedKeywords(value, `${path}.${key}`)); - } - } - return found; - }; - - for (const tool of tools) { - const violations = findUnsupportedKeywords(tool.parameters, `${tool.name}.parameters`); - expect(violations).toEqual([]); - } - }); -}); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts deleted file mode 100644 index a5cbf2320..000000000 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { createClawdbotCodingTools } from "./pi-tools.js"; -import { createSandboxedReadTool } from "./pi-tools.read.js"; - -describe("createClawdbotCodingTools", () => { - it("applies sandbox path guards to file_path alias", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sbx-")); - const outsidePath = path.join(os.tmpdir(), "clawdbot-outside.txt"); - await fs.writeFile(outsidePath, "outside", "utf8"); - try { - const readTool = createSandboxedReadTool(tmpDir); - await expect(readTool.execute("tool-sbx-1", { file_path: outsidePath })).rejects.toThrow(); - } finally { - await fs.rm(tmpDir, { recursive: true, force: true }); - await fs.rm(outsidePath, { force: true }); - } - }); - it("falls back to process.cwd() when workspaceDir not provided", () => { - const prevCwd = process.cwd(); - const tools = createClawdbotCodingTools(); - // Tools should be created without error - expect(tools.some((tool) => tool.name === "read")).toBe(true); - expect(tools.some((tool) => tool.name === "write")).toBe(true); - expect(tools.some((tool) => tool.name === "edit")).toBe(true); - // cwd should be unchanged - expect(process.cwd()).toBe(prevCwd); - }); -}); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts index 497eb41a9..8cb3a3522 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts @@ -1,7 +1,14 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { describe, expect, it, vi } from "vitest"; import { createClawdbotTools } from "./clawdbot-tools.js"; import { __testing, createClawdbotCodingTools } from "./pi-tools.js"; +import { createSandboxedReadTool } from "./pi-tools.read.js"; +import { createBrowserTool } from "./tools/browser-tool.js"; + +const defaultTools = createClawdbotCodingTools(); describe("createClawdbotCodingTools", () => { describe("Claude/Gemini alias support", () => { @@ -67,8 +74,144 @@ describe("createClawdbotCodingTools", () => { }); }); + it("keeps browser tool schema OpenAI-compatible without normalization", () => { + const browser = createBrowserTool(); + const schema = browser.parameters as { type?: unknown; anyOf?: unknown }; + expect(schema.type).toBe("object"); + expect(schema.anyOf).toBeUndefined(); + }); + it("mentions Chrome extension relay in browser tool description", () => { + const browser = createBrowserTool(); + expect(browser.description).toMatch(/Chrome extension/i); + expect(browser.description).toMatch(/profile="chrome"/i); + }); + it("keeps browser tool schema properties after normalization", () => { + const browser = defaultTools.find((tool) => tool.name === "browser"); + expect(browser).toBeDefined(); + const parameters = browser?.parameters as { + anyOf?: unknown[]; + properties?: Record; + required?: string[]; + }; + expect(parameters.properties?.action).toBeDefined(); + expect(parameters.properties?.target).toBeDefined(); + expect(parameters.properties?.controlUrl).toBeDefined(); + expect(parameters.properties?.targetUrl).toBeDefined(); + expect(parameters.properties?.request).toBeDefined(); + expect(parameters.required ?? []).toContain("action"); + }); + it("exposes raw for gateway config.apply tool calls", () => { + const gateway = defaultTools.find((tool) => tool.name === "gateway"); + expect(gateway).toBeDefined(); + + const parameters = gateway?.parameters as { + type?: unknown; + required?: string[]; + properties?: Record; + }; + expect(parameters.type).toBe("object"); + expect(parameters.properties?.raw).toBeDefined(); + expect(parameters.required ?? []).not.toContain("raw"); + }); + it("flattens anyOf-of-literals to enum for provider compatibility", () => { + const browser = defaultTools.find((tool) => tool.name === "browser"); + expect(browser).toBeDefined(); + + const parameters = browser?.parameters as { + properties?: Record; + }; + const action = parameters.properties?.action as + | { + type?: unknown; + enum?: unknown[]; + anyOf?: unknown[]; + } + | undefined; + + expect(action?.type).toBe("string"); + expect(action?.anyOf).toBeUndefined(); + expect(Array.isArray(action?.enum)).toBe(true); + expect(action?.enum).toContain("act"); + + const snapshotFormat = parameters.properties?.snapshotFormat as + | { + type?: unknown; + enum?: unknown[]; + anyOf?: unknown[]; + } + | undefined; + expect(snapshotFormat?.type).toBe("string"); + expect(snapshotFormat?.anyOf).toBeUndefined(); + expect(snapshotFormat?.enum).toEqual(["aria", "ai"]); + }); + it("inlines local $ref before removing unsupported keywords", () => { + const cleaned = __testing.cleanToolSchemaForGemini({ + type: "object", + properties: { + foo: { $ref: "#/$defs/Foo" }, + }, + $defs: { + Foo: { type: "string", enum: ["a", "b"] }, + }, + }) as { + $defs?: unknown; + properties?: Record; + }; + + expect(cleaned.$defs).toBeUndefined(); + expect(cleaned.properties).toBeDefined(); + expect(cleaned.properties?.foo).toMatchObject({ + type: "string", + enum: ["a", "b"], + }); + }); + it("cleans tuple items schemas", () => { + const cleaned = __testing.cleanToolSchemaForGemini({ + type: "object", + properties: { + tuples: { + type: "array", + items: [ + { type: "string", format: "uuid" }, + { type: "number", minimum: 1 }, + ], + }, + }, + }) as { + properties?: Record; + }; + + const tuples = cleaned.properties?.tuples as { items?: unknown } | undefined; + const items = Array.isArray(tuples?.items) ? tuples?.items : []; + const first = items[0] as { format?: unknown } | undefined; + const second = items[1] as { minimum?: unknown } | undefined; + + expect(first?.format).toBeUndefined(); + expect(second?.minimum).toBeUndefined(); + }); + it("drops null-only union variants without flattening other unions", () => { + const cleaned = __testing.cleanToolSchemaForGemini({ + type: "object", + properties: { + parentId: { anyOf: [{ type: "string" }, { type: "null" }] }, + count: { oneOf: [{ type: "string" }, { type: "number" }] }, + }, + }) as { + properties?: Record; + }; + + const parentId = cleaned.properties?.parentId as + | { type?: unknown; anyOf?: unknown; oneOf?: unknown } + | undefined; + const count = cleaned.properties?.count as + | { type?: unknown; anyOf?: unknown; oneOf?: unknown } + | undefined; + + expect(parentId?.type).toBe("string"); + expect(parentId?.anyOf).toBeUndefined(); + expect(count?.oneOf).toBeDefined(); + }); it("avoids anyOf/oneOf/allOf in tool schemas", () => { - const tools = createClawdbotCodingTools(); const offenders: Array<{ name: string; keyword: string; @@ -96,7 +239,7 @@ describe("createClawdbotCodingTools", () => { } }; - for (const tool of tools) { + for (const tool of defaultTools) { walk(tool.parameters, "", tool.name); } @@ -192,4 +335,131 @@ describe("createClawdbotCodingTools", () => { }); expect(tools.map((tool) => tool.name)).toEqual(["read"]); }); + + it("applies tool profiles before allow/deny policies", () => { + const tools = createClawdbotCodingTools({ + config: { tools: { profile: "messaging" } }, + }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("message")).toBe(true); + expect(names.has("sessions_send")).toBe(true); + expect(names.has("sessions_spawn")).toBe(false); + expect(names.has("exec")).toBe(false); + expect(names.has("browser")).toBe(false); + }); + it("expands group shorthands in global tool policy", () => { + const tools = createClawdbotCodingTools({ + config: { tools: { allow: ["group:fs"] } }, + }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("read")).toBe(true); + expect(names.has("write")).toBe(true); + expect(names.has("edit")).toBe(true); + expect(names.has("exec")).toBe(false); + expect(names.has("browser")).toBe(false); + }); + it("expands group shorthands in global tool deny policy", () => { + const tools = createClawdbotCodingTools({ + config: { tools: { deny: ["group:fs"] } }, + }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("read")).toBe(false); + expect(names.has("write")).toBe(false); + expect(names.has("edit")).toBe(false); + expect(names.has("exec")).toBe(true); + }); + it("lets agent profiles override global profiles", () => { + const tools = createClawdbotCodingTools({ + sessionKey: "agent:work:main", + config: { + tools: { profile: "coding" }, + agents: { + list: [{ id: "work", tools: { profile: "messaging" } }], + }, + }, + }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("message")).toBe(true); + expect(names.has("exec")).toBe(false); + expect(names.has("read")).toBe(false); + }); + it("removes unsupported JSON Schema keywords for Cloud Code Assist API compatibility", () => { + // Helper to recursively check schema for unsupported keywords + const unsupportedKeywords = new Set([ + "patternProperties", + "additionalProperties", + "$schema", + "$id", + "$ref", + "$defs", + "definitions", + "examples", + "minLength", + "maxLength", + "minimum", + "maximum", + "multipleOf", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems", + "minProperties", + "maxProperties", + ]); + + const findUnsupportedKeywords = (schema: unknown, path: string): string[] => { + const found: string[] = []; + if (!schema || typeof schema !== "object") return found; + if (Array.isArray(schema)) { + schema.forEach((item, i) => { + found.push(...findUnsupportedKeywords(item, `${path}[${i}]`)); + }); + return found; + } + + const record = schema as Record; + const properties = + record.properties && + typeof record.properties === "object" && + !Array.isArray(record.properties) + ? (record.properties as Record) + : undefined; + if (properties) { + for (const [key, value] of Object.entries(properties)) { + found.push(...findUnsupportedKeywords(value, `${path}.properties.${key}`)); + } + } + + for (const [key, value] of Object.entries(record)) { + if (key === "properties") continue; + if (unsupportedKeywords.has(key)) { + found.push(`${path}.${key}`); + } + if (value && typeof value === "object") { + found.push(...findUnsupportedKeywords(value, `${path}.${key}`)); + } + } + return found; + }; + + for (const tool of defaultTools) { + const violations = findUnsupportedKeywords(tool.parameters, `${tool.name}.parameters`); + expect(violations).toEqual([]); + } + }); + it("applies sandbox path guards to file_path alias", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sbx-")); + const outsidePath = path.join(os.tmpdir(), "clawdbot-outside.txt"); + await fs.writeFile(outsidePath, "outside", "utf8"); + try { + const readTool = createSandboxedReadTool(tmpDir); + await expect(readTool.execute("sandbox-1", { file_path: outsidePath })).rejects.toThrow( + /sandbox root/i, + ); + } finally { + await fs.rm(outsidePath, { force: true }); + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); });