Files
oh-my-pi/packages/coding-agent/test/tools/provider-schema-compatibility.test.ts
T
vmcall 2aaa639b69 fix(task): marked dynamic task schema non-strict
Caller-provided output schemas are free-form JSON and cannot be represented by OpenAI strict tool schemas. Keep todo strict while explicitly sending task as non-strict.
2026-07-17 17:38:12 +02:00

166 lines
5.3 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import {
adaptSchemaForStrict,
normalizeSchemaForCCA,
normalizeSchemaForGoogle,
type SchemaCompatibilityProvider,
type SchemaCompatibilityResult,
toolWireSchema,
validateSchemaCompatibility,
validateStrictSchemaEnforcement,
} from "@oh-my-pi/pi-ai/utils/schema";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { createTools, HIDDEN_TOOLS, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { createVibeTools } from "@oh-my-pi/pi-coding-agent/tools/vibe";
interface ToolSchemaEntry {
name: string;
schema: Record<string, unknown>;
}
function createTestSession(): ToolSession {
return {
cwd: "/tmp/test",
hasUI: true,
getSessionFile: () => null,
getSessionSpawns: () => "*",
settings: Settings.isolated({ "tools.xdev": false }),
};
}
function asSchemaObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
async function collectToolSchemas(): Promise<ToolSchemaEntry[]> {
const session = createTestSession();
const byToolName = new Map<string, Record<string, unknown>>();
for (const tool of await createTools(session)) {
const schema = toolWireSchema(tool);
if (!asSchemaObject(schema)) {
continue;
}
byToolName.set(tool.name, schema);
}
for (const [name, factory] of Object.entries(HIDDEN_TOOLS)) {
const tool = await factory(session);
if (!tool) {
continue;
}
const schema = toolWireSchema(tool);
if (!asSchemaObject(schema)) {
continue;
}
byToolName.set(name, schema);
}
for (const tool of createVibeTools(session)) {
const schema = toolWireSchema(tool);
if (!asSchemaObject(schema)) {
continue;
}
byToolName.set(tool.name, schema);
}
return [...byToolName.entries()]
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, schema]) => ({ name, schema }));
}
function formatCompatibilityIssues(
toolName: string,
provider: SchemaCompatibilityProvider,
result: SchemaCompatibilityResult,
): string {
if (result.compatible) {
return "";
}
const details = result.violations
.map(violation => ` - ${violation.rule} at ${violation.path}: ${violation.message}`)
.join("\n");
return `${toolName} (${provider}):\n${details}`;
}
describe("builtin tool schemas provider compatibility", () => {
it("keeps todo strict and marks task non-strict for free-form output schemas", async () => {
const tools = await createTools(createTestSession());
const task = tools.find(tool => tool.name === "task");
const todo = tools.find(tool => tool.name === "todo");
expect(task).toBeDefined();
expect(todo).toBeDefined();
if (!task || !todo) {
return;
}
expect(task.strict).toBe(false);
expect(adaptSchemaForStrict(toolWireSchema(task), task.strict !== false).strict).toBe(false);
expect(adaptSchemaForStrict(toolWireSchema(todo), todo.strict !== false).strict).toBe(true);
});
it("keeps all builtin and hidden tool schemas valid after provider enforcement", async () => {
const toolSchemas = await collectToolSchemas();
const failures: string[] = [];
for (const { name, schema } of toolSchemas) {
const strictResult = adaptSchemaForStrict(schema, true);
const strictCompatibility = validateStrictSchemaEnforcement(schema, strictResult);
if (!strictCompatibility.compatible) {
failures.push(formatCompatibilityIssues(name, "openai-strict", strictCompatibility));
}
try {
const googleSchema = normalizeSchemaForGoogle(schema);
const googleCompatibility = validateSchemaCompatibility(googleSchema, "google");
if (!googleCompatibility.compatible) {
failures.push(formatCompatibilityIssues(name, "google", googleCompatibility));
}
} catch (error) {
failures.push(`${name} (google): normalizeSchemaForGoogle threw: ${String(error)}`);
}
const cloudCodeAssistSchema = normalizeSchemaForCCA(schema);
const cloudCodeAssistCompatibility = validateSchemaCompatibility(
cloudCodeAssistSchema,
"cloud-code-assist-claude",
);
if (!cloudCodeAssistCompatibility.compatible) {
failures.push(formatCompatibilityIssues(name, "cloud-code-assist-claude", cloudCodeAssistCompatibility));
}
}
if (failures.length > 0) {
throw new Error(`Provider compatibility failures:\n\n${failures.join("\n\n")}`);
}
expect(failures).toEqual([]);
});
it("preserves the yield result schema for Cloud Code Assist", async () => {
const toolSchemas = await collectToolSchemas();
const yieldEntry = toolSchemas.find(tool => tool.name === "yield");
expect(yieldEntry).toBeDefined();
if (!yieldEntry) return;
const normalized = asSchemaObject(normalizeSchemaForCCA(yieldEntry.schema));
const properties = asSchemaObject(normalized?.properties);
const typeSchema = asSchemaObject(properties?.type);
expect(normalized?.type).toBe("object");
expect(properties?.result).toBeDefined();
expect(typeSchema?.type).toBe("string");
expect(typeSchema?.anyOf).toBeUndefined();
});
it('asserts that browser tool schema root stays `type: "object"` when discoverable tools are mounted', async () => {
const toolSchemas = await collectToolSchemas();
const browserEntry = toolSchemas.find(tool => tool.name === "browser");
expect(browserEntry).toBeDefined();
expect(asSchemaObject(browserEntry?.schema)?.type).toBe("object");
});
});