Skip to main content
Glama
claude-code-structured-output.test.js10.1 kB
import { jest } from '@jest/globals'; import { z } from 'zod'; /** * Tests for Claude Code native structured output support (v2.2.0+) * * ai-sdk-provider-claude-code v2.2.0 introduced native structured outputs via * the Claude Agent SDK's outputFormat option. When using generateObject() with * a schema, the SDK now guarantees schema-compliant JSON through constrained decoding. * * Key behaviors tested: * 1. Schema is passed correctly to the SDK * 2. mode: 'json' is used (which enables native outputFormat in the SDK) * 3. SDK error handling for schema validation failures */ // Mock generateObject from 'ai' SDK const mockGenerateObject = jest.fn(); jest.unstable_mockModule('ai', () => ({ generateObject: mockGenerateObject, generateText: jest.fn(), streamText: jest.fn(), streamObject: jest.fn(), zodSchema: jest.fn((schema) => schema), JSONParseError: class JSONParseError extends Error { constructor(message, text) { super(message); this.text = text; } }, NoObjectGeneratedError: class NoObjectGeneratedError extends Error { static isInstance(error) { return error instanceof NoObjectGeneratedError; } } })); // Mock jsonrepair jest.unstable_mockModule('jsonrepair', () => ({ jsonrepair: jest.fn((text) => text) })); // Mock the ai-sdk-provider-claude-code package jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({ createClaudeCode: jest.fn(() => { const provider = (modelId) => ({ id: modelId, specificationVersion: 'v1', provider: 'claude-code', modelId }); provider.languageModel = provider; provider.chat = provider; return provider; }) })); // Mock config getters jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({ getClaudeCodeSettingsForCommand: jest.fn(() => ({})), getSupportedModelsForProvider: jest.fn(() => ['opus', 'sonnet', 'haiku']), getDebugFlag: jest.fn(() => false), getLogLevel: jest.fn(() => 'info'), isProxyEnabled: jest.fn(() => false), getAnonymousTelemetryEnabled: jest.fn(() => true), setSuppressConfigWarnings: jest.fn(), isConfigWarningSuppressed: jest.fn(() => false) })); // Mock utils jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({ log: jest.fn(), findProjectRoot: jest.fn(() => '/test/project'), resolveEnvVariable: jest.fn((key) => process.env[key]) })); // Import after mocking const { ClaudeCodeProvider } = await import( '../../../src/ai-providers/claude-code.js' ); describe('ClaudeCodeProvider structured outputs (v2.2.0+)', () => { let provider; beforeEach(() => { provider = new ClaudeCodeProvider(); jest.clearAllMocks(); }); describe('needsExplicitJsonSchema flag', () => { it('should have needsExplicitJsonSchema set to true', () => { // This flag triggers mode: 'json' in base-provider.js generateObject() // which in turn enables the SDK's native outputFormat with constrained decoding expect(provider.needsExplicitJsonSchema).toBe(true); }); it('should not support temperature parameter', () => { // Claude Code SDK doesn't support temperature expect(provider.supportsTemperature).toBe(false); }); }); describe('generateObject with schema', () => { const testSchema = z.object({ name: z.string(), age: z.number(), email: z.string().email() }); const testMessages = [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: 'Generate a user profile' } ]; beforeEach(() => { // Mock successful generateObject response mockGenerateObject.mockResolvedValue({ object: { name: 'Test User', age: 25, email: 'test@example.com' }, usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 } }); }); it('should pass schema to generateObject call', async () => { await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: testMessages, schema: testSchema, objectName: 'user_profile' }); expect(mockGenerateObject).toHaveBeenCalledWith( expect.objectContaining({ schema: testSchema }) ); }); it('should use json mode for Claude Code (enables native outputFormat)', async () => { await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: testMessages, schema: testSchema, objectName: 'user_profile' }); // mode: 'json' is set when needsExplicitJsonSchema is true // This triggers the SDK to use outputFormat: { type: 'json_schema', schema: ... } expect(mockGenerateObject).toHaveBeenCalledWith( expect.objectContaining({ mode: 'json' }) ); }); it('should pass schemaName for better SDK context', async () => { await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: testMessages, schema: testSchema, objectName: 'user_profile' }); expect(mockGenerateObject).toHaveBeenCalledWith( expect.objectContaining({ schemaName: 'user_profile' }) ); }); it('should return structured object from SDK', async () => { const result = await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: testMessages, schema: testSchema, objectName: 'user_profile' }); expect(result.object).toEqual({ name: 'Test User', age: 25, email: 'test@example.com' }); }); it('should return usage information', async () => { const result = await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: testMessages, schema: testSchema, objectName: 'user_profile' }); expect(result.usage).toEqual({ inputTokens: 100, outputTokens: 50, totalTokens: 150 }); }); }); describe('complex schemas', () => { it('should handle nested object schemas', async () => { const complexSchema = z.object({ tasks: z.array( z.object({ id: z.number(), title: z.string(), subtasks: z.array( z.object({ id: z.number(), title: z.string() }) ) }) ) }); mockGenerateObject.mockResolvedValue({ object: { tasks: [ { id: 1, title: 'Main Task', subtasks: [{ id: 1, title: 'Subtask 1' }] } ] }, usage: { promptTokens: 50, completionTokens: 30, totalTokens: 80 } }); const result = await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: [{ role: 'user', content: 'Generate tasks' }], schema: complexSchema, objectName: 'task_list' }); expect(result.object.tasks).toHaveLength(1); expect(result.object.tasks[0].subtasks).toHaveLength(1); }); it('should handle enum schemas (like task priority)', async () => { const prioritySchema = z.object({ priority: z.enum(['high', 'medium', 'low']), title: z.string() }); mockGenerateObject.mockResolvedValue({ object: { priority: 'high', title: 'Important Task' }, usage: { promptTokens: 30, completionTokens: 20, totalTokens: 50 } }); const result = await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: [{ role: 'user', content: 'Create a task' }], schema: prioritySchema, objectName: 'task' }); expect(result.object.priority).toBe('high'); }); }); describe('error handling', () => { it('should throw error when schema is missing', async () => { await expect( provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: [{ role: 'user', content: 'test' }], objectName: 'test' // schema is missing }) ).rejects.toThrow('Schema is required'); }); it('should throw error when objectName is missing', async () => { await expect( provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: [{ role: 'user', content: 'test' }], schema: z.object({ name: z.string() }) // objectName is missing }) ).rejects.toThrow('Object name is required'); }); it('should handle SDK errors gracefully', async () => { mockGenerateObject.mockRejectedValue( new Error('SDK error: Failed to generate') ); await expect( provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: [{ role: 'user', content: 'test' }], schema: z.object({ name: z.string() }), objectName: 'test' }) ).rejects.toThrow(); }); }); describe('v2.2.0 native structured output benefits', () => { /** * These tests document the expected behavior with v2.2.0's native schema support. * The SDK now handles schema validation internally through constrained decoding, * so the jsonrepair fallback in base-provider.js should rarely be triggered * for Claude Code operations. */ it('should work with Task Master command schemas', async () => { // This simulates the expand-task schema pattern const expandTaskSchema = z.object({ subtasks: z.array( z.object({ id: z.number().int().positive(), title: z.string().min(1), description: z.string().min(1), dependencies: z.array(z.number().int()), details: z.string(), testStrategy: z.string() }) ) }); mockGenerateObject.mockResolvedValue({ object: { subtasks: [ { id: 1, title: 'Implement feature X', description: 'Description for feature X', dependencies: [], details: 'Implementation details', testStrategy: 'Unit tests for feature X' } ] }, usage: { promptTokens: 200, completionTokens: 100, totalTokens: 300 } }); const result = await provider.generateObject({ apiKey: 'test-key', modelId: 'sonnet', messages: [{ role: 'user', content: 'Expand task into subtasks' }], schema: expandTaskSchema, objectName: 'subtasks' }); expect(result.object.subtasks).toHaveLength(1); expect(result.object.subtasks[0].id).toBe(1); expect(result.object.subtasks[0].title).toBe('Implement feature X'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server