import { describe, expect, test } from "bun:test" import { Result, Schema } from "effect" import { toJsonSchema } from "../../src/util/effect-zod" // Each tool exports its parameters schema at module scope so this test can // import them without running the tool's Effect-based init. The JSON Schema // snapshot captures what the LLM sees; the parse assertions pin down the // accepts/rejects contract. `toJsonSchema` is the same helper `session/ // prompt.ts` uses to emit tool schemas to the LLM, so the snapshots stay // byte-identical regardless of whether a tool has migrated from zod to Schema. import { Parameters as ApplyPatch } from "../../src/tool/apply_patch" import { Parameters as Bash } from "../../src/tool/bash" import { Parameters as CodeSearch } from "../../src/tool/codesearch" import { Parameters as Edit } from "../../src/tool/edit" import { Parameters as Glob } from "../../src/tool/glob" import { Parameters as Grep } from "../../src/tool/grep" import { Parameters as Invalid } from "../../src/tool/invalid" import { Parameters as Lsp } from "../../src/tool/lsp" import { Parameters as Plan } from "../../src/tool/plan" import { Parameters as Question } from "../../src/tool/question" import { Parameters as Read } from "../../src/tool/read" import { Parameters as Skill } from "../../src/tool/skill" import { Parameters as Task } from "../../src/tool/task" import { Parameters as Todo } from "../../src/tool/todo" import { Parameters as WebFetch } from "../../src/tool/webfetch" import { Parameters as WebSearch } from "../../src/tool/websearch" import { Parameters as Write } from "../../src/tool/write" const parse = >(schema: S, input: unknown): S["Type"] => Schema.decodeUnknownSync(schema)(input) const accepts = (schema: Schema.Decoder, input: unknown): boolean => Result.isSuccess(Schema.decodeUnknownResult(schema)(input)) describe("tool parameters", () => { describe("JSON Schema (wire shape)", () => { test("apply_patch", () => expect(toJsonSchema(ApplyPatch)).toMatchSnapshot()) test("bash", () => expect(toJsonSchema(Bash)).toMatchSnapshot()) test("codesearch", () => expect(toJsonSchema(CodeSearch)).toMatchSnapshot()) test("edit", () => expect(toJsonSchema(Edit)).toMatchSnapshot()) test("glob", () => expect(toJsonSchema(Glob)).toMatchSnapshot()) test("grep", () => expect(toJsonSchema(Grep)).toMatchSnapshot()) test("invalid", () => expect(toJsonSchema(Invalid)).toMatchSnapshot()) test("lsp", () => expect(toJsonSchema(Lsp)).toMatchSnapshot()) test("plan", () => expect(toJsonSchema(Plan)).toMatchSnapshot()) test("question", () => expect(toJsonSchema(Question)).toMatchSnapshot()) test("read", () => expect(toJsonSchema(Read)).toMatchSnapshot()) test("skill", () => expect(toJsonSchema(Skill)).toMatchSnapshot()) test("task", () => expect(toJsonSchema(Task)).toMatchSnapshot()) test("todo", () => expect(toJsonSchema(Todo)).toMatchSnapshot()) test("webfetch", () => expect(toJsonSchema(WebFetch)).toMatchSnapshot()) test("websearch", () => expect(toJsonSchema(WebSearch)).toMatchSnapshot()) test("write", () => expect(toJsonSchema(Write)).toMatchSnapshot()) }) describe("apply_patch", () => { test("accepts patchText", () => { expect(parse(ApplyPatch, { patchText: "*** Begin Patch\n*** End Patch" })).toEqual({ patchText: "*** Begin Patch\n*** End Patch", }) }) test("rejects missing patchText", () => { expect(accepts(ApplyPatch, {})).toBe(false) }) test("rejects non-string patchText", () => { expect(accepts(ApplyPatch, { patchText: 123 })).toBe(false) }) }) describe("bash", () => { test("accepts minimum: command + description", () => { expect(parse(Bash, { command: "ls", description: "list" })).toEqual({ command: "ls", description: "list" }) }) test("accepts optional timeout + workdir", () => { const parsed = parse(Bash, { command: "ls", description: "list", timeout: 5000, workdir: "/tmp" }) expect(parsed.timeout).toBe(5000) expect(parsed.workdir).toBe("/tmp") }) test("rejects missing description (required by zod)", () => { expect(accepts(Bash, { command: "ls" })).toBe(false) }) test("rejects missing command", () => { expect(accepts(Bash, { description: "list" })).toBe(false) }) }) describe("codesearch", () => { test("accepts query; tokensNum defaults to 5000", () => { expect(parse(CodeSearch, { query: "hooks" })).toEqual({ query: "hooks", tokensNum: 5000 }) }) test("accepts override tokensNum", () => { expect(parse(CodeSearch, { query: "hooks", tokensNum: 10000 }).tokensNum).toBe(10000) }) test("rejects tokensNum under 1000", () => { expect(accepts(CodeSearch, { query: "x", tokensNum: 500 })).toBe(false) }) test("rejects tokensNum over 50000", () => { expect(accepts(CodeSearch, { query: "x", tokensNum: 60000 })).toBe(false) }) }) describe("edit", () => { test("accepts all four fields", () => { expect(parse(Edit, { filePath: "/a", oldString: "x", newString: "y", replaceAll: true })).toEqual({ filePath: "/a", oldString: "x", newString: "y", replaceAll: true, }) }) test("replaceAll is optional", () => { const parsed = parse(Edit, { filePath: "/a", oldString: "x", newString: "y" }) expect(parsed.replaceAll).toBeUndefined() }) test("rejects missing filePath", () => { expect(accepts(Edit, { oldString: "x", newString: "y" })).toBe(false) }) }) describe("glob", () => { test("accepts pattern-only", () => { expect(parse(Glob, { pattern: "**/*.ts" })).toEqual({ pattern: "**/*.ts" }) }) test("accepts optional path", () => { expect(parse(Glob, { pattern: "**/*.ts", path: "/tmp" }).path).toBe("/tmp") }) test("rejects missing pattern", () => { expect(accepts(Glob, {})).toBe(false) }) }) describe("grep", () => { test("accepts pattern-only", () => { expect(parse(Grep, { pattern: "TODO" })).toEqual({ pattern: "TODO" }) }) test("accepts optional path + include", () => { const parsed = parse(Grep, { pattern: "TODO", path: "/tmp", include: "*.ts" }) expect(parsed.path).toBe("/tmp") expect(parsed.include).toBe("*.ts") }) test("rejects missing pattern", () => { expect(accepts(Grep, {})).toBe(false) }) }) describe("invalid", () => { test("accepts tool + error", () => { expect(parse(Invalid, { tool: "foo", error: "bar" })).toEqual({ tool: "foo", error: "bar" }) }) test("rejects missing fields", () => { expect(accepts(Invalid, { tool: "foo" })).toBe(false) expect(accepts(Invalid, { error: "bar" })).toBe(false) }) }) describe("lsp", () => { test("accepts all fields", () => { const parsed = parse(Lsp, { operation: "hover", filePath: "/a.ts", line: 1, character: 1 }) expect(parsed.operation).toBe("hover") }) test("rejects line < 1", () => { expect(accepts(Lsp, { operation: "hover", filePath: "/a.ts", line: 0, character: 1 })).toBe(false) }) test("rejects character < 1", () => { expect(accepts(Lsp, { operation: "hover", filePath: "/a.ts", line: 1, character: 0 })).toBe(false) }) test("rejects unknown operation", () => { expect(accepts(Lsp, { operation: "bogus", filePath: "/a.ts", line: 1, character: 1 })).toBe(false) }) }) describe("plan", () => { test("accepts empty object", () => { expect(parse(Plan, {})).toEqual({}) }) }) describe("question", () => { test("accepts questions array", () => { const parsed = parse(Question, { questions: [ { question: "pick one", header: "Header", custom: false, options: [{ label: "a", description: "desc" }], }, ], }) expect(parsed.questions.length).toBe(1) }) test("rejects missing questions", () => { expect(accepts(Question, {})).toBe(false) }) }) describe("read", () => { test("accepts filePath-only", () => { expect(parse(Read, { filePath: "/a" }).filePath).toBe("/a") }) test("accepts optional offset + limit", () => { const parsed = parse(Read, { filePath: "/a", offset: 10, limit: 100 }) expect(parsed.offset).toBe(10) expect(parsed.limit).toBe(100) }) }) describe("skill", () => { test("accepts name", () => { expect(parse(Skill, { name: "foo" }).name).toBe("foo") }) test("rejects missing name", () => { expect(accepts(Skill, {})).toBe(false) }) }) describe("task", () => { test("accepts description + prompt + subagent_type", () => { const parsed = parse(Task, { description: "d", prompt: "p", subagent_type: "general" }) expect(parsed.subagent_type).toBe("general") }) test("rejects missing prompt", () => { expect(accepts(Task, { description: "d", subagent_type: "general" })).toBe(false) }) }) describe("todo", () => { test("accepts todos array", () => { const parsed = parse(Todo, { todos: [{ id: "t1", content: "do x", status: "pending", priority: "medium" }], }) expect(parsed.todos.length).toBe(1) }) test("rejects missing todos", () => { expect(accepts(Todo, {})).toBe(false) }) }) describe("webfetch", () => { test("accepts url-only", () => { expect(parse(WebFetch, { url: "https://example.com" }).url).toBe("https://example.com") }) }) describe("websearch", () => { test("accepts query", () => { expect(parse(WebSearch, { query: "opencode" }).query).toBe("opencode") }) }) describe("write", () => { test("accepts content + filePath", () => { expect(parse(Write, { content: "hi", filePath: "/a" })).toEqual({ content: "hi", filePath: "/a" }) }) test("rejects missing filePath", () => { expect(accepts(Write, { content: "hi" })).toBe(false) }) }) })