co-mono/packages/ai/test/tool-validation.test.ts
Mario Zechner 84dcab219b Add image support in tool results across all providers
Tool results now use content blocks and can include both text and images.
All providers (Anthropic, Google, OpenAI Completions, OpenAI Responses)
correctly pass images from tool results to LLMs.

- Update ToolResultMessage type to use content blocks
- Add placeholder text for image-only tool results in Google/Anthropic
- OpenAI providers send tool result + follow-up user message with images
- Fix Anthropic JSON parsing for empty tool arguments
- Add comprehensive tests for image-only and text+image tool results
- Update README with tool result content blocks API
2025-11-12 10:45:56 +01:00

140 lines
3.8 KiB
TypeScript

import { type Static, Type } from "@sinclair/typebox";
import AjvModule from "ajv";
import addFormatsModule from "ajv-formats";
// Handle both default and named exports
const Ajv = (AjvModule as any).default || AjvModule;
const addFormats = (addFormatsModule as any).default || addFormatsModule;
import { describe, expect, it } from "vitest";
import type { AgentTool } from "../src/agent/types.js";
describe("Tool Validation with TypeBox and AJV", () => {
// Define a test tool with TypeBox schema
const testSchema = Type.Object({
name: Type.String({ minLength: 1 }),
age: Type.Integer({ minimum: 0, maximum: 150 }),
email: Type.String({ format: "email" }),
tags: Type.Optional(Type.Array(Type.String())),
});
type TestParams = Static<typeof testSchema>;
const testTool: AgentTool<typeof testSchema, void> = {
label: "Test Tool",
name: "test_tool",
description: "A test tool for validation",
parameters: testSchema,
execute: async (_toolCallId, args) => {
return {
content: [{ type: "text", text: `Processed: ${args.name}, ${args.age}, ${args.email}` }],
details: undefined,
};
},
};
// Create AJV instance for validation
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
it("should validate correct input", () => {
const validInput = {
name: "John Doe",
age: 30,
email: "john@example.com",
tags: ["developer", "typescript"],
};
// Validate with AJV
const validate = ajv.compile(testTool.parameters);
const isValid = validate(validInput);
expect(isValid).toBe(true);
});
it("should reject invalid email", () => {
const invalidInput = {
name: "John Doe",
age: 30,
email: "not-an-email",
};
const validate = ajv.compile(testTool.parameters);
const isValid = validate(invalidInput);
expect(isValid).toBe(false);
expect(validate.errors).toBeDefined();
});
it("should reject missing required fields", () => {
const invalidInput = {
age: 30,
email: "john@example.com",
};
const validate = ajv.compile(testTool.parameters);
const isValid = validate(invalidInput);
expect(isValid).toBe(false);
expect(validate.errors).toBeDefined();
});
it("should reject invalid age", () => {
const invalidInput = {
name: "John Doe",
age: -5,
email: "john@example.com",
};
const validate = ajv.compile(testTool.parameters);
const isValid = validate(invalidInput);
expect(isValid).toBe(false);
expect(validate.errors).toBeDefined();
});
it("should format validation errors nicely", () => {
const invalidInput = {
name: "",
age: 200,
email: "invalid",
};
const validate = ajv.compile(testTool.parameters);
const isValid = validate(invalidInput);
expect(isValid).toBe(false);
expect(validate.errors).toBeDefined();
if (validate.errors) {
const errors = validate.errors
.map((err: any) => {
const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root";
return ` - ${path}: ${err.message}`;
})
.join("\n");
// AJV error messages are different from Zod
expect(errors).toContain("name: must NOT have fewer than 1 characters");
expect(errors).toContain("age: must be <= 150");
expect(errors).toContain('email: must match format "email"');
}
});
it("should have type-safe execute function", async () => {
const validInput = {
name: "John Doe",
age: 30,
email: "john@example.com",
};
// Validate and execute
const validate = ajv.compile(testTool.parameters);
const isValid = validate(validInput);
expect(isValid).toBe(true);
const result = await testTool.execute("test-id", validInput as TestParams);
const textOutput = result.content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text)
.join("\n");
expect(textOutput).toBe("Processed: John Doe, 30, john@example.com");
expect(result.details).toBeUndefined();
});
});