refactor(tool): migrate tool framework + all 18 built-in tools to Effect Schema (#23244)
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) apply_patch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"patchText": {
|
||||
"description": "The full patch text that describes all changes to be made",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"patchText",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) bash 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "The command to execute",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"description":
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:
|
||||
Input: ls
|
||||
Output: Lists files in current directory
|
||||
|
||||
Input: git status
|
||||
Output: Shows working tree status
|
||||
|
||||
Input: npm install
|
||||
Output: Installs package dependencies
|
||||
|
||||
Input: mkdir foo
|
||||
Output: Creates directory 'foo'"
|
||||
,
|
||||
"type": "string",
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Optional timeout in milliseconds",
|
||||
"type": "number",
|
||||
},
|
||||
"workdir": {
|
||||
"description": "The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands.",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"description",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) codesearch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"query": {
|
||||
"description": "Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'",
|
||||
"type": "string",
|
||||
},
|
||||
"tokensNum": {
|
||||
"default": 5000,
|
||||
"description": "Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.",
|
||||
"maximum": 50000,
|
||||
"minimum": 1000,
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"query",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) edit 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"description": "The absolute path to the file to modify",
|
||||
"type": "string",
|
||||
},
|
||||
"newString": {
|
||||
"description": "The text to replace it with (must be different from oldString)",
|
||||
"type": "string",
|
||||
},
|
||||
"oldString": {
|
||||
"description": "The text to replace",
|
||||
"type": "string",
|
||||
},
|
||||
"replaceAll": {
|
||||
"description": "Replace all occurrences of oldString (default false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"filePath",
|
||||
"oldString",
|
||||
"newString",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) glob 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"path": {
|
||||
"description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.",
|
||||
"type": "string",
|
||||
},
|
||||
"pattern": {
|
||||
"description": "The glob pattern to match files against",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) grep 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"include": {
|
||||
"description": "File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")",
|
||||
"type": "string",
|
||||
},
|
||||
"path": {
|
||||
"description": "The directory to search in. Defaults to the current working directory.",
|
||||
"type": "string",
|
||||
},
|
||||
"pattern": {
|
||||
"description": "The regex pattern to search for in file contents",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) invalid 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
},
|
||||
"tool": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"tool",
|
||||
"error",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) lsp 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"character": {
|
||||
"description": "The character offset (1-based, as shown in editors)",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"filePath": {
|
||||
"description": "The absolute or relative path to the file",
|
||||
"type": "string",
|
||||
},
|
||||
"line": {
|
||||
"description": "The line number (1-based, as shown in editors)",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"operation": {
|
||||
"description": "The LSP operation to perform",
|
||||
"enum": [
|
||||
"goToDefinition",
|
||||
"findReferences",
|
||||
"hover",
|
||||
"documentSymbol",
|
||||
"workspaceSymbol",
|
||||
"goToImplementation",
|
||||
"prepareCallHierarchy",
|
||||
"incomingCalls",
|
||||
"outgoingCalls",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"operation",
|
||||
"filePath",
|
||||
"line",
|
||||
"character",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) plan 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) question 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"questions": {
|
||||
"description": "Questions to ask",
|
||||
"items": {
|
||||
"properties": {
|
||||
"header": {
|
||||
"description": "Very short label (max 30 chars)",
|
||||
"type": "string",
|
||||
},
|
||||
"multiple": {
|
||||
"description": "Allow selecting multiple choices",
|
||||
"type": "boolean",
|
||||
},
|
||||
"options": {
|
||||
"description": "Available choices",
|
||||
"items": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Explanation of choice",
|
||||
"type": "string",
|
||||
},
|
||||
"label": {
|
||||
"description": "Display text (1-5 words, concise)",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"ref": "QuestionOption",
|
||||
"required": [
|
||||
"label",
|
||||
"description",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"question": {
|
||||
"description": "Complete question",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"ref": "QuestionPrompt",
|
||||
"required": [
|
||||
"question",
|
||||
"header",
|
||||
"options",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"questions",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) read 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"description": "The absolute path to the file or directory to read",
|
||||
"type": "string",
|
||||
},
|
||||
"limit": {
|
||||
"description": "The maximum number of lines to read (defaults to 2000)",
|
||||
"type": "number",
|
||||
},
|
||||
"offset": {
|
||||
"description": "The line number to start reading from (1-indexed)",
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"filePath",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) skill 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name of the skill from available_skills",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) task 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "The command that triggered this task",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"description": "A short (3-5 words) description of the task",
|
||||
"type": "string",
|
||||
},
|
||||
"prompt": {
|
||||
"description": "The task for the agent to perform",
|
||||
"type": "string",
|
||||
},
|
||||
"subagent_type": {
|
||||
"description": "The type of specialized agent to use for this task",
|
||||
"type": "string",
|
||||
},
|
||||
"task_id": {
|
||||
"description": "This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"prompt",
|
||||
"subagent_type",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) todo 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"todos": {
|
||||
"description": "The updated todo list",
|
||||
"items": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "Brief description of the task",
|
||||
"type": "string",
|
||||
},
|
||||
"priority": {
|
||||
"description": "Priority level of the task: high, medium, low",
|
||||
"type": "string",
|
||||
},
|
||||
"status": {
|
||||
"description": "Current status of the task: pending, in_progress, completed, cancelled",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"status",
|
||||
"priority",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"todos",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) webfetch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"format": {
|
||||
"default": "markdown",
|
||||
"description": "The format to return the content in (text, markdown, or html). Defaults to markdown.",
|
||||
"enum": [
|
||||
"text",
|
||||
"markdown",
|
||||
"html",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Optional timeout in seconds (max 120)",
|
||||
"type": "number",
|
||||
},
|
||||
"url": {
|
||||
"description": "The URL to fetch content from",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) websearch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"contextMaxCharacters": {
|
||||
"description": "Maximum characters for context string optimized for LLMs (default: 10000)",
|
||||
"type": "number",
|
||||
},
|
||||
"livecrawl": {
|
||||
"description": "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
|
||||
"enum": [
|
||||
"fallback",
|
||||
"preferred",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"numResults": {
|
||||
"description": "Number of search results to return (default: 8)",
|
||||
"type": "number",
|
||||
},
|
||||
"query": {
|
||||
"description": "Websearch query",
|
||||
"type": "string",
|
||||
},
|
||||
"type": {
|
||||
"description": "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
|
||||
"enum": [
|
||||
"auto",
|
||||
"fast",
|
||||
"deep",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"query",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) write 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "The content to write to the file",
|
||||
"type": "string",
|
||||
},
|
||||
"filePath": {
|
||||
"description": "The absolute path to the file to write (must be absolute, not relative)",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"filePath",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
260
packages/opencode/test/tool/parameters.test.ts
Normal file
260
packages/opencode/test/tool/parameters.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
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 = <S extends Schema.Decoder<unknown>>(schema: S, input: unknown): S["Type"] =>
|
||||
Schema.decodeUnknownSync(schema)(input)
|
||||
|
||||
const accepts = (schema: Schema.Decoder<unknown>, 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import z from "zod"
|
||||
import { Effect, Layer, ManagedRuntime, Schema } from "effect"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { Tool } from "../../src/tool"
|
||||
import { Truncate } from "../../src/tool"
|
||||
|
||||
const runtime = ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))
|
||||
|
||||
const params = z.object({ input: z.string() })
|
||||
const params = Schema.Struct({ input: Schema.String })
|
||||
|
||||
function makeTool(id: string, executeFn?: () => void) {
|
||||
return {
|
||||
@@ -56,4 +56,44 @@ describe("Tool.define", () => {
|
||||
|
||||
expect(first).not.toBe(second)
|
||||
})
|
||||
|
||||
test("execute receives decoded parameters", async () => {
|
||||
const parameters = Schema.Struct({
|
||||
count: Schema.NumberFromString.pipe(Schema.optional, Schema.withDecodingDefaultType(Effect.succeed(5))),
|
||||
})
|
||||
const calls: Array<Schema.Schema.Type<typeof parameters>> = []
|
||||
const info = await runtime.runPromise(
|
||||
Tool.define(
|
||||
"test-decoded",
|
||||
Effect.succeed({
|
||||
description: "test tool",
|
||||
parameters,
|
||||
execute(args: Schema.Schema.Type<typeof parameters>) {
|
||||
calls.push(args)
|
||||
return Effect.succeed({ title: "test", output: "ok", metadata: { truncated: false } })
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
const ctx: Tool.Context = {
|
||||
sessionID: SessionID.descending(),
|
||||
messageID: MessageID.ascending(),
|
||||
agent: "build",
|
||||
abort: new AbortController().signal,
|
||||
messages: [],
|
||||
metadata() {
|
||||
return Effect.void
|
||||
},
|
||||
ask() {
|
||||
return Effect.void
|
||||
},
|
||||
}
|
||||
const tool = await Effect.runPromise(info.init())
|
||||
const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType<typeof tool.execute>
|
||||
|
||||
await Effect.runPromise(execute({}, ctx))
|
||||
await Effect.runPromise(execute({ count: "7" }, ctx))
|
||||
|
||||
expect(calls).toEqual([{ count: 5 }, { count: 7 }])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user