Compare commits
2 commits
dev
...
kit/repro-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fa8ad4ec2 | ||
|
|
463af5e079 |
4 changed files with 337 additions and 9 deletions
|
|
@ -20,6 +20,7 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { EffectBridge } from "@/effect/bridge"
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
|
|
||||||
const log = Log.create({ service: "session.tools" })
|
const log = Log.create({ service: "session.tools" })
|
||||||
|
const schemaInternalKeys = new Set(["_def", "def", "_zod", "~standard", "_cached", "typeName"])
|
||||||
|
|
||||||
export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||||
agent: Agent.Info
|
agent: Agent.Info
|
||||||
|
|
@ -78,6 +79,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
})) {
|
})) {
|
||||||
const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item))
|
const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item))
|
||||||
|
assertNoSchemaInternals(item.id, schema)
|
||||||
tools[item.id] = tool({
|
tools[item.id] = tool({
|
||||||
description: item.description,
|
description: item.description,
|
||||||
inputSchema: jsonSchema(schema),
|
inputSchema: jsonSchema(schema),
|
||||||
|
|
@ -121,6 +123,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||||
|
|
||||||
const schema = yield* Effect.promise(() => Promise.resolve(asSchema(item.inputSchema).jsonSchema))
|
const schema = yield* Effect.promise(() => Promise.resolve(asSchema(item.inputSchema).jsonSchema))
|
||||||
const transformed = ProviderTransform.schema(input.model, schema)
|
const transformed = ProviderTransform.schema(input.model, schema)
|
||||||
|
assertNoSchemaInternals(key, transformed)
|
||||||
item.inputSchema = jsonSchema(transformed)
|
item.inputSchema = jsonSchema(transformed)
|
||||||
item.execute = (args, opts) =>
|
item.execute = (args, opts) =>
|
||||||
run.promise(
|
run.promise(
|
||||||
|
|
@ -205,4 +208,31 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||||
return tools
|
return tools
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function assertNoSchemaInternals(toolID: string, schema: unknown) {
|
||||||
|
const path = schemaInternalPath(schema)
|
||||||
|
if (!path) return
|
||||||
|
throw new Error(`Tool ${toolID} input schema contains non-JSON-Schema Zod internals at ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaInternalPath(value: unknown, path = "$", skipKeys = false): string | undefined {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map((item, index) => schemaInternalPath(item, `${path}[${index}]`))
|
||||||
|
.find((item): item is string => item !== undefined)
|
||||||
|
}
|
||||||
|
if (typeof value !== "object" || value === null) return undefined
|
||||||
|
|
||||||
|
for (const [key, item] of Object.entries(value)) {
|
||||||
|
const nextPath = /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`
|
||||||
|
if (!skipKeys && schemaInternalKeys.has(key)) return nextPath
|
||||||
|
const found = schemaInternalPath(
|
||||||
|
item,
|
||||||
|
nextPath,
|
||||||
|
key === "properties" || key === "$defs" || key === "definitions" || key === "patternProperties",
|
||||||
|
)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export * as SessionTools from "./tools"
|
export * as SessionTools from "./tools"
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,13 @@ import { Reference } from "@/reference/reference"
|
||||||
import { BackgroundJob } from "@/background/job"
|
import { BackgroundJob } from "@/background/job"
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
import {
|
||||||
|
objectFromShape,
|
||||||
|
safeParse,
|
||||||
|
type AnyObjectSchema,
|
||||||
|
type AnySchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/server/zod-compat.js"
|
||||||
|
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js"
|
||||||
|
|
||||||
const log = Log.create({ service: "tool.registry" })
|
const log = Log.create({ service: "tool.registry" })
|
||||||
|
|
||||||
|
|
@ -150,10 +157,10 @@ export const layer: Layer.Layer<
|
||||||
const args = def.args ?? {}
|
const args = def.args ?? {}
|
||||||
const entries = Object.entries(args)
|
const entries = Object.entries(args)
|
||||||
const allZod = entries.every((entry) => isZodType(entry[1]))
|
const allZod = entries.every((entry) => isZodType(entry[1]))
|
||||||
const zodParams = allZod ? z.object(args) : undefined
|
const zodParams = allZod ? objectFromShape(args as Record<string, AnySchema>) : undefined
|
||||||
const jsonSchema = zodParams ? zodJsonSchema(zodParams) : legacyJsonSchema(entries)
|
const jsonSchema = zodParams ? zodJsonSchema(zodParams) : legacyJsonSchema(entries)
|
||||||
const parameters = zodParams
|
const parameters = zodParams
|
||||||
? Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success)
|
? Schema.declare<unknown>((u): u is unknown => safeParse(zodParams, u).success)
|
||||||
: Schema.Unknown
|
: Schema.Unknown
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|
@ -402,7 +409,11 @@ export const defaultLayer = Layer.suspend(() =>
|
||||||
.pipe(Layer.provide(RuntimeFlags.defaultLayer)),
|
.pipe(Layer.provide(RuntimeFlags.defaultLayer)),
|
||||||
)
|
)
|
||||||
|
|
||||||
function isZodType(value: unknown): value is z.ZodType {
|
function isZodType(value: unknown): value is AnySchema {
|
||||||
|
return typeof value === "object" && value !== null && ("_zod" in value || "_def" in value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isZod4Type(value: unknown): value is z.ZodType {
|
||||||
return typeof value === "object" && value !== null && "_zod" in value
|
return typeof value === "object" && value !== null && "_zod" in value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,8 +436,12 @@ function legacyJsonSchema(entries: [string, unknown][]): JSONSchema7 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function zodJsonSchema(schema: z.ZodType): JSONSchema7 {
|
function zodJsonSchema(schema: AnyObjectSchema): JSONSchema7 {
|
||||||
const result = normalizeZodJsonSchema(z.toJSONSchema(schema, { io: "input", metadata: zodMetadataRegistry(schema) }))
|
const result = normalizeZodJsonSchema(
|
||||||
|
isZod4Type(schema)
|
||||||
|
? z.toJSONSchema(schema, { io: "input", metadata: zodMetadataRegistry(schema) })
|
||||||
|
: toJsonSchemaCompat(schema, { pipeStrategy: "input" }),
|
||||||
|
)
|
||||||
if (!isJsonSchemaObject(result)) throw new Error("plugin tool Zod schema produced a non-object JSON Schema")
|
if (!isJsonSchemaObject(result)) throw new Error("plugin tool Zod schema produced a non-object JSON Schema")
|
||||||
const { $defs, ...rest } = result
|
const { $defs, ...rest } = result
|
||||||
return (
|
return (
|
||||||
|
|
@ -434,7 +449,7 @@ function zodJsonSchema(schema: z.ZodType): JSONSchema7 {
|
||||||
) as JSONSchema7
|
) as JSONSchema7
|
||||||
}
|
}
|
||||||
|
|
||||||
function zodMetadataRegistry(schema: z.ZodType) {
|
function zodMetadataRegistry(schema: AnyObjectSchema) {
|
||||||
const registry = z.registry<Record<string, unknown>>()
|
const registry = z.registry<Record<string, unknown>>()
|
||||||
const seen = new WeakSet<object>()
|
const seen = new WeakSet<object>()
|
||||||
const collect = (value: unknown) => {
|
const collect = (value: unknown) => {
|
||||||
|
|
@ -442,7 +457,7 @@ function zodMetadataRegistry(schema: z.ZodType) {
|
||||||
if (seen.has(value)) return
|
if (seen.has(value)) return
|
||||||
seen.add(value)
|
seen.add(value)
|
||||||
|
|
||||||
if (isZodType(value)) {
|
if (isZod4Type(value)) {
|
||||||
const metadata = typeof value.meta === "function" ? value.meta() : undefined
|
const metadata = typeof value.meta === "function" ? value.meta() : undefined
|
||||||
const description = typeof value.description === "string" ? value.description : undefined
|
const description = typeof value.description === "string" ? value.description : undefined
|
||||||
const merged = {
|
const merged = {
|
||||||
|
|
|
||||||
201
packages/opencode/test/session/tools-schema.test.ts
Normal file
201
packages/opencode/test/session/tools-schema.test.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { jsonSchema } from "ai"
|
||||||
|
import { Effect, Exit, Layer } from "effect"
|
||||||
|
import { Agent } from "@/agent/agent"
|
||||||
|
import { MCP } from "@/mcp"
|
||||||
|
import { Permission } from "@/permission"
|
||||||
|
import { ProjectID } from "@/project/schema"
|
||||||
|
import { ModelID, ProviderID } from "@/provider/schema"
|
||||||
|
import type { Provider } from "@/provider/provider"
|
||||||
|
import { SessionTools } from "@/session/tools"
|
||||||
|
import { MessageV2 } from "@/session/message-v2"
|
||||||
|
import { MessageID, SessionID } from "@/session/schema"
|
||||||
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
|
import { Truncate } from "@/tool/truncate"
|
||||||
|
import { Plugin } from "@/plugin"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
|
const it = testEffect(
|
||||||
|
Layer.mergeAll(
|
||||||
|
Layer.succeed(
|
||||||
|
ToolRegistry.Service,
|
||||||
|
ToolRegistry.Service.of({
|
||||||
|
ids: () => Effect.succeed([]),
|
||||||
|
all: () => Effect.succeed([]),
|
||||||
|
named: () => Effect.die("unexpected named tool lookup"),
|
||||||
|
tools: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Layer.succeed(
|
||||||
|
MCP.Service,
|
||||||
|
MCP.Service.of({
|
||||||
|
status: () => Effect.succeed({}),
|
||||||
|
clients: () => Effect.succeed({}),
|
||||||
|
tools: () =>
|
||||||
|
Effect.succeed({
|
||||||
|
ctx_batch_execute: {
|
||||||
|
description: "context tool",
|
||||||
|
inputSchema: jsonSchema({
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
batch: {
|
||||||
|
type: "array",
|
||||||
|
items: schemaWithZodInternals(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
execute: () => Promise.resolve({ content: [{ type: "text" as const, text: "ok" }] }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prompts: () => Effect.succeed({}),
|
||||||
|
resources: () => Effect.succeed({}),
|
||||||
|
add: () => Effect.succeed({ status: { status: "disabled" as const } }),
|
||||||
|
connect: () => Effect.void,
|
||||||
|
disconnect: () => Effect.void,
|
||||||
|
getPrompt: () => Effect.succeed(undefined),
|
||||||
|
readResource: () => Effect.succeed(undefined),
|
||||||
|
startAuth: () => Effect.die("unexpected MCP auth"),
|
||||||
|
authenticate: () => Effect.die("unexpected MCP auth"),
|
||||||
|
finishAuth: () => Effect.die("unexpected MCP auth"),
|
||||||
|
removeAuth: () => Effect.void,
|
||||||
|
supportsOAuth: () => Effect.succeed(false),
|
||||||
|
hasStoredTokens: () => Effect.succeed(false),
|
||||||
|
getAuthStatus: () => Effect.succeed("not_authenticated" as const),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Layer.succeed(
|
||||||
|
Plugin.Service,
|
||||||
|
Plugin.Service.of({
|
||||||
|
trigger: (_name, _input, output) => Effect.succeed(output),
|
||||||
|
list: () => Effect.succeed([]),
|
||||||
|
init: () => Effect.void,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Layer.succeed(
|
||||||
|
Permission.Service,
|
||||||
|
Permission.Service.of({
|
||||||
|
ask: () => Effect.void,
|
||||||
|
reply: () => Effect.void,
|
||||||
|
list: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Layer.succeed(
|
||||||
|
Truncate.Service,
|
||||||
|
Truncate.Service.of({
|
||||||
|
cleanup: () => Effect.void,
|
||||||
|
write: () => Effect.succeed("/tmp/tool-output"),
|
||||||
|
output: (text) => Effect.succeed({ content: text, truncated: false as const }),
|
||||||
|
limits: () => Effect.succeed({ maxLines: 2000, maxBytes: 50 * 1024 }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe("SessionTools.resolve", () => {
|
||||||
|
it.effect("fails locally when MCP schemas contain Zod internals", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exit = yield* SessionTools.resolve({
|
||||||
|
agent: agentInfo(),
|
||||||
|
model: kimiModel(),
|
||||||
|
session: sessionInfo(),
|
||||||
|
processor: processor(),
|
||||||
|
bypassAgentCheck: false,
|
||||||
|
messages: [],
|
||||||
|
promptOps: promptOps(),
|
||||||
|
}).pipe(Effect.exit)
|
||||||
|
|
||||||
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
|
if (!Exit.isFailure(exit)) return
|
||||||
|
|
||||||
|
expect(String(exit.cause)).toContain("ctx_batch_execute")
|
||||||
|
expect(String(exit.cause)).toContain("non-JSON-Schema Zod internals")
|
||||||
|
expect(String(exit.cause)).toContain("$.properties.batch.items._zod")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function agentInfo(): Agent.Info {
|
||||||
|
return {
|
||||||
|
name: "build",
|
||||||
|
mode: "primary",
|
||||||
|
permission: [],
|
||||||
|
options: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaWithZodInternals() {
|
||||||
|
return JSON.parse(
|
||||||
|
JSON.stringify({
|
||||||
|
_zod: { def: { type: "object" } },
|
||||||
|
def: { type: "object" },
|
||||||
|
typeName: "ZodObject",
|
||||||
|
"~standard": { vendor: "zod" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function kimiModel(): Provider.Model {
|
||||||
|
return {
|
||||||
|
id: ModelID.make("kimi-k2.6"),
|
||||||
|
providerID: ProviderID.make("moonshotai"),
|
||||||
|
name: "Kimi K2.6",
|
||||||
|
limit: { context: 128_000, output: 32_000 },
|
||||||
|
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||||
|
capabilities: {
|
||||||
|
toolcall: true,
|
||||||
|
attachment: false,
|
||||||
|
reasoning: false,
|
||||||
|
temperature: true,
|
||||||
|
input: { text: true, image: false, audio: false, video: false, pdf: false },
|
||||||
|
output: { text: true, image: false, audio: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
|
},
|
||||||
|
api: { id: "kimi-k2.6", url: "https://api.moonshot.example/v1", npm: "@ai-sdk/openai-compatible" },
|
||||||
|
options: {},
|
||||||
|
headers: {},
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
status: "active",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionInfo() {
|
||||||
|
return {
|
||||||
|
id: SessionID.descending(),
|
||||||
|
slug: "test",
|
||||||
|
projectID: ProjectID.global,
|
||||||
|
directory: "/tmp/test",
|
||||||
|
title: "test",
|
||||||
|
version: "test",
|
||||||
|
time: { created: Date.now(), updated: Date.now() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processor() {
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
id: MessageID.ascending(),
|
||||||
|
sessionID: SessionID.descending(),
|
||||||
|
role: "assistant",
|
||||||
|
parentID: MessageID.ascending(),
|
||||||
|
modelID: ModelID.make("kimi-k2.6"),
|
||||||
|
providerID: ProviderID.make("moonshotai"),
|
||||||
|
mode: "build",
|
||||||
|
agent: "build",
|
||||||
|
path: { cwd: "/tmp/test", root: "/tmp/test" },
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
time: { created: Date.now() },
|
||||||
|
} satisfies MessageV2.Assistant,
|
||||||
|
updateToolCall: () => Effect.succeed(undefined),
|
||||||
|
completeToolCall: () => Effect.void,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptOps() {
|
||||||
|
return {
|
||||||
|
cancel: () => Effect.void,
|
||||||
|
resolvePromptParts: (template: string) => Effect.succeed([{ type: "text" as const, text: template }]),
|
||||||
|
prompt: () => Effect.die("unexpected prompt call"),
|
||||||
|
loop: () => Effect.die("unexpected loop call"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ import { ProviderID, ModelID } from "@/provider/schema"
|
||||||
import { ToolJsonSchema } from "@/tool/json-schema"
|
import { ToolJsonSchema } from "@/tool/json-schema"
|
||||||
import { MessageID, SessionID } from "@/session/schema"
|
import { MessageID, SessionID } from "@/session/schema"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
import z3 from "zod/v3"
|
||||||
|
|
||||||
const node = CrossSpawnSpawner.defaultLayer
|
const node = CrossSpawnSpawner.defaultLayer
|
||||||
const configLayer = TestConfig.layer({
|
const configLayer = TestConfig.layer({
|
||||||
|
|
@ -80,7 +81,7 @@ const brokenPluginLayer = Layer.succeed(
|
||||||
init: () => Effect.void,
|
init: () => Effect.void,
|
||||||
trigger: ((_name: unknown, _input: unknown, output: unknown) =>
|
trigger: ((_name: unknown, _input: unknown, output: unknown) =>
|
||||||
Effect.succeed(output)) as Plugin.Interface["trigger"],
|
Effect.succeed(output)) as Plugin.Interface["trigger"],
|
||||||
list: () =>
|
list: (() =>
|
||||||
Effect.succeed([
|
Effect.succeed([
|
||||||
{
|
{
|
||||||
tool: {
|
tool: {
|
||||||
|
|
@ -91,7 +92,41 @@ const brokenPluginLayer = Layer.succeed(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])) as unknown as Plugin.Interface["list"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const zod3PluginLayer = Layer.succeed(
|
||||||
|
Plugin.Service,
|
||||||
|
Plugin.Service.of({
|
||||||
|
init: () => Effect.void,
|
||||||
|
trigger: ((_name: unknown, _input: unknown, output: unknown) =>
|
||||||
|
Effect.succeed(output)) as Plugin.Interface["trigger"],
|
||||||
|
list: (() =>
|
||||||
|
Effect.succeed([
|
||||||
|
{
|
||||||
|
tool: {
|
||||||
|
ctx_batch_execute: {
|
||||||
|
description: "context-mode batch executor",
|
||||||
|
args: {
|
||||||
|
batch: z3
|
||||||
|
.preprocess(
|
||||||
|
(value) => (typeof value === "string" ? JSON.parse(value) : value),
|
||||||
|
z3
|
||||||
|
.array(
|
||||||
|
z3.object({
|
||||||
|
command: z3.string().describe("Command to execute"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.min(1),
|
||||||
|
)
|
||||||
|
.describe("Commands to execute as a batch"),
|
||||||
|
},
|
||||||
|
execute: async () => "ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])) as unknown as Plugin.Interface["list"],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -105,6 +140,7 @@ const background = testEffect(
|
||||||
const withBrokenPlugin = testEffect(
|
const withBrokenPlugin = testEffect(
|
||||||
Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer),
|
Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer),
|
||||||
)
|
)
|
||||||
|
const withZod3Plugin = testEffect(Layer.mergeAll(registryLayer({ plugin: zod3PluginLayer }), node, Agent.defaultLayer))
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
|
|
@ -349,6 +385,52 @@ describe("tool.registry", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
withZod3Plugin.instance("loads plugin tools with Zod 3 args as JSON Schema", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const loaded = (yield* registry.all()).find((tool) => tool.id === "ctx_batch_execute")
|
||||||
|
if (!loaded) throw new Error("ctx_batch_execute tool was not loaded")
|
||||||
|
|
||||||
|
expect(loaded.jsonSchema).toMatchObject({
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
batch: {
|
||||||
|
type: "array",
|
||||||
|
description: "Commands to execute as a batch",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
command: { type: "string", description: "Command to execute" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minItems: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["batch"],
|
||||||
|
})
|
||||||
|
expect(JSON.stringify(loaded.jsonSchema)).not.toContain("_def")
|
||||||
|
expect(JSON.stringify(loaded.jsonSchema)).not.toContain("_zod")
|
||||||
|
expect(Result.isSuccess(Schema.decodeUnknownResult(loaded.parameters)({ batch: [{ command: "pwd" }] }))).toBe(
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
expect(Result.isSuccess(Schema.decodeUnknownResult(loaded.parameters)({}))).toBe(false)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
withZod3Plugin.instance("validates plugin tools with Zod 3 preprocessors", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const loaded = (yield* registry.all()).find((tool) => tool.id === "ctx_batch_execute")
|
||||||
|
if (!loaded) throw new Error("ctx_batch_execute tool was not loaded")
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Result.isSuccess(
|
||||||
|
Schema.decodeUnknownResult(loaded.parameters)({ batch: JSON.stringify([{ command: "pwd" }]) }),
|
||||||
|
),
|
||||||
|
).toBe(true)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"preserves Zod arg descriptions from older config-scoped plugin packages",
|
"preserves Zod arg descriptions from older config-scoped plugin packages",
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue