import { validateToolArguments } from "@earendil-works/pi-ai"; import { describe, expect, it } from "vitest"; import { builtinAgentTools } from "./builtin-tools.js"; import { parseConnectorToolArgs } from "./lazy-tool-catalog.js"; import { jsonSchemaParameters, parametersFor } from "./pi-runtime.js"; describe("jsonSchemaParameters", () => { it("keeps model-facing nullable parameters compatible with connector validation", () => { const tool = { name: "catalog_lookup", description: "Look up catalog entries", inputSchema: { type: "object", properties: { enabled: { type: ["boolean", "null"] } }, required: ["enabled"], }, }; const wire = JSON.parse(JSON.stringify(parametersFor(tool))); for (const enabled of [true, false, null]) { expect(parseConnectorToolArgs(wire, { enabled })).toEqual( parseConnectorToolArgs(tool.inputSchema, { enabled }), ); } expect(() => parseConnectorToolArgs(wire, { enabled: "false" })).toThrow(); expect(() => parseConnectorToolArgs(wire, {})).toThrow(); }); it("preserves nullable object fields instead of advertising strings", () => { const schema = jsonSchemaParameters({ type: "object", properties: { filter: { type: ["object", "null"], properties: { enabled: { type: "boolean" } }, required: ["enabled"], additionalProperties: false, }, }, required: ["filter"], }); expect(JSON.parse(JSON.stringify(schema))).toEqual({ type: "object", required: ["filter"], properties: { filter: { anyOf: [ { type: "object", properties: { enabled: { type: "boolean" } }, required: ["enabled"], additionalProperties: false, }, { type: "null" }, ], }, }, }); }); it("preserves null branches in anyOf", () => { const schema = jsonSchemaParameters({ type: "object", properties: { cursor: { anyOf: [{ type: "string" }, { type: "null" }] } }, }); expect(JSON.parse(JSON.stringify(schema)).properties.cursor).toEqual({ anyOf: [{ type: "string" }, { type: "null" }], }); }); it("preserves nullable array items and array constraints", () => { const schema = jsonSchemaParameters({ type: "object", properties: { values: { type: "array", items: { type: ["boolean", "null"] }, minItems: 1 }, }, }); expect(JSON.parse(JSON.stringify(schema)).properties.values).toEqual({ type: "array", minItems: 1, items: { anyOf: [{ type: "boolean" }, { type: "null" }] }, }); }); it("keeps primitive enums as literal unions", () => { const schema = jsonSchemaParameters({ type: "object", properties: { mode: { type: "string", enum: ["fast", "slow"] } }, required: ["mode"], }) as unknown as { properties: { mode: { anyOf: { const: unknown }[] } } }; expect(schema.properties.mode.anyOf.map((member) => member.const)).toEqual(["fast", "slow"]); }); it("accepts a nullable enum without throwing", () => { expect(() => jsonSchemaParameters({ type: "object", properties: { cursor: { type: ["string", "null"], enum: ["a", "b", null] } }, }), ).not.toThrow(); }); it("accepts enums whose members are objects or arrays", () => { expect(() => jsonSchemaParameters({ type: "object", properties: { filter: { type: "object", enum: [{ kind: "all" }, ["x"]] } }, }), ).not.toThrow(); }); }); describe("run_subagent parameters", () => { const tool = builtinAgentTools.find((entry) => entry.name === "run_subagent")!; const parameters = parametersFor(tool); const call = (args: Record) => ({ type: "toolCall" as const, id: "call-1", name: "run_subagent", arguments: { name: "scout", task: "look", ...args }, }); it("takes an optional model tier and only the known tiers", () => { expect(validateToolArguments({ name: "run_subagent", parameters } as never, call({}))).toEqual({ name: "scout", task: "look", }); expect( validateToolArguments( { name: "run_subagent", parameters } as never, call({ model_tier: "code" }), ), ).toMatchObject({ model_tier: "code" }); expect(() => validateToolArguments( { name: "run_subagent", parameters } as never, call({ model_tier: "huge" }), ), ).toThrow(/model_tier/); }); });