opencode/packages/core/test/tool-schema.test.ts
opencode-agent[bot] 33e3d1ebca
fix(core): tolerate missing tool input schemas (#39130)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-07-27 11:21:47 -04:00

160 lines
4.8 KiB
TypeScript

import { expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import type { Info } from "@opencode-ai/schema/tool"
import { Tool } from "../src/tool"
import { definition, execute } from "../src/tool/runtime"
test("tools are structural values", async () => {
const config = {
name: "foreign",
description: "Foreign tool",
input: Schema.Struct({ value: Schema.String }),
output: Schema.Struct({ ok: Schema.Boolean }),
execute: () => Effect.succeed({ output: { ok: true } }),
}
const tool: Info = config
expect(definition(tool)).toEqual({
name: "foreign",
description: "Foreign tool",
inputSchema: {
type: "object",
properties: { value: { type: "string" } },
required: ["value"],
additionalProperties: false,
},
outputSchema: {
type: "object",
properties: { ok: { type: "boolean" } },
required: ["ok"],
additionalProperties: false,
},
})
})
test("portable schemas validate and describe typed tools", async () => {
const input = {
"~standard": {
version: 1,
vendor: "test",
validate: (value: unknown) => {
if (typeof value !== "object" || value === null || !("count" in value) || typeof value.count !== "string")
return { issues: [{ message: "count must be numeric" }] }
const count = Number(value.count)
return Number.isFinite(count) ? { value: { count } } : { issues: [{ message: "count must be numeric" }] }
},
jsonSchema: {
input: () => ({ type: "object", properties: { count: { type: "string" } } }),
output: () => ({ type: "object", properties: { count: { type: "number" } } }),
},
},
}
const output = {
"~standard": {
version: 1,
vendor: "test",
validate: (value: unknown) => ({ value: String(value) }),
jsonSchema: {
input: () => ({ type: "number" }),
output: () => ({ type: "string" }),
},
},
}
const tool: Info = ({
name: "portable",
description: "Portable tool",
input,
output,
execute: ({ count }) => Effect.succeed({ output: count + 1 }),
})
expect(definition(tool)).toEqual({
name: "portable",
description: "Portable tool",
inputSchema: { type: "object", properties: { count: { type: "string" } } },
outputSchema: { type: "string" },
})
const result = await Effect.runPromise(execute(tool, { count: "41" }, {} as Tool.Context))
expect(result.output).toBe("42")
})
test("portable schema failures become tool failures", async () => {
const input = {
"~standard": {
version: 1,
vendor: "test",
validate: (_value: unknown) => ({ issues: [{ message: "expected a string" }] }),
jsonSchema: {
input: () => ({ type: "string" }),
output: () => ({ type: "string" }),
},
},
}
const error = await Effect.runPromiseExit(
execute(
{
name: "invalid",
description: "Invalid",
input,
execute: () => Effect.succeed({ content: "unused" }),
},
1,
{} as Tool.Context,
),
)
expect(error.toString()).toContain("Invalid tool input: expected a string")
})
test("canonical results carry metadata with typed output", async () => {
const input = Schema.Struct({ value: Schema.String })
const output = Schema.Struct({ value: Schema.String, internal: Schema.Boolean })
const tool: Info = ({
name: "annotated",
description: "Annotated tool",
input,
output,
execute: ({ value }) => Effect.succeed({ output: { value, internal: true }, metadata: { value }, content: value }),
})
expect(await Effect.runPromise(tool.execute({ value: "out" }, {} as Tool.Context))).toEqual({
output: { value: "out", internal: true },
metadata: { value: "out" },
content: "out",
})
})
test("raw JSON schemas are render-only and omitted output means model-only", async () => {
const input = { type: "object", properties: { value: { type: "string" } } }
const tool: Info = ({
name: "raw",
description: "Raw tool",
input,
execute: (input) => Effect.succeed({ content: JSON.stringify(input) }),
})
expect(definition(tool)).toEqual({
name: "raw",
description: "Raw tool",
inputSchema: { type: "object", properties: { value: { type: "string" } } },
})
expect(await Effect.runPromise(execute(tool, { value: 1 }, {} as Tool.Context))).toEqual({
output: undefined,
content: [{ type: "text", text: '{"value":1}' }],
})
})
test("missing external input schemas fall back to an empty schema", () => {
const tool = {
name: "external",
description: "External tool",
input: undefined,
execute: () => Effect.succeed({ content: "unused" }),
} as unknown as Info
expect(definition(tool)).toEqual({
name: "external",
description: "External tool",
inputSchema: {},
})
})