evOo-local/packages/adapters/src/openai-tool-parameters.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

115 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { builtinAgentTools } from "./builtin-tools.js";
import {
normalizeOpenAiToolParameters,
openAiToolParametersNeedNormalization,
} from "./openai-tool-parameters.js";
import { parametersFor } from "./pi-runtime.js";
describe("normalizeOpenAiToolParameters", () => {
it("fills properties for a zero-argument object schema", () => {
expect(normalizeOpenAiToolParameters({ type: "object" })).toEqual({
type: "object",
properties: {},
});
});
it("keeps an empty properties map and does not invent parameters", () => {
expect(normalizeOpenAiToolParameters({ type: "object", properties: {} })).toEqual({
type: "object",
properties: {},
});
});
it("forces type object when a union discriminator is missing", () => {
const normalized = normalizeOpenAiToolParameters({
anyOf: [
{ type: "object", properties: { a: { type: "string" } }, required: ["a"] },
{ type: "object", properties: { b: { type: "string" } }, required: ["b"] },
],
});
expect(normalized.type).toBe("object");
expect(normalized.properties).toEqual({});
expect(normalized.anyOf).toHaveLength(2);
});
it("preserves required, additionalProperties, and existing properties", () => {
expect(
normalizeOpenAiToolParameters({
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
additionalProperties: false,
}),
).toEqual({
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
additionalProperties: false,
});
});
it("replaces a non-object type with object without inventing fields", () => {
expect(normalizeOpenAiToolParameters({ type: "string" })).toEqual({
type: "object",
properties: {},
});
});
it("treats nullish or non-object input as an empty object schema", () => {
expect(normalizeOpenAiToolParameters(undefined)).toEqual({
type: "object",
properties: {},
});
expect(normalizeOpenAiToolParameters(null)).toEqual({
type: "object",
properties: {},
});
expect(normalizeOpenAiToolParameters([])).toEqual({
type: "object",
properties: {},
});
});
});
describe("openAiToolParametersNeedNormalization", () => {
it("returns false for a complete object schema", () => {
expect(openAiToolParametersNeedNormalization({ type: "object", properties: {} })).toBe(false);
});
it("returns true when type or properties are missing or malformed", () => {
expect(openAiToolParametersNeedNormalization({ type: "object" })).toBe(true);
expect(openAiToolParametersNeedNormalization({ anyOf: [] })).toBe(true);
expect(openAiToolParametersNeedNormalization({ type: "object", properties: [] })).toBe(true);
expect(openAiToolParametersNeedNormalization({ type: "string", properties: {} })).toBe(true);
});
});
describe("parametersFor OpenAI wire fidelity", () => {
it("serializes zero-argument tools with type object and empty properties", () => {
const tool = builtinAgentTools.find((entry) => entry.name === "list_secrets");
if (!tool) throw new Error("missing list_secrets");
const wire = JSON.parse(JSON.stringify(parametersFor(tool))) as {
type?: unknown;
properties?: unknown;
};
expect(wire.type).toBe("object");
expect(wire.properties).toEqual({});
});
it("serializes request_secret union with type object and empty properties", () => {
const tool = builtinAgentTools.find((entry) => entry.name === "request_secret");
if (!tool) throw new Error("missing request_secret");
const wire = JSON.parse(JSON.stringify(parametersFor(tool))) as {
type?: unknown;
properties?: unknown;
anyOf?: unknown[];
oneOf?: unknown[];
};
expect(wire.type).toBe("object");
expect(wire.properties).toEqual({});
expect((wire.anyOf ?? wire.oneOf ?? []).length).toBe(2);
});
});