takt/src/__tests__/claude-executor-structured-output.test.ts
2026-02-13 04:21:51 +09:00

165 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Claude SDK layer structured output tests.
*
* Tests two internal components:
* 1. SdkOptionsBuilder — outputSchema → outputFormat conversion
* 2. QueryExecutor — structured_output extraction from SDK result messages
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
// ===== SdkOptionsBuilder tests (no mock needed) =====
import { buildSdkOptions } from '../infra/claude/options-builder.js';
describe('SdkOptionsBuilder — outputFormat 変換', () => {
it('outputSchema が outputFormat に変換される', () => {
const schema = { type: 'object', properties: { step: { type: 'integer' } } };
const sdkOptions = buildSdkOptions({ cwd: '/tmp', outputSchema: schema });
expect((sdkOptions as Record<string, unknown>).outputFormat).toEqual({
type: 'json_schema',
schema,
});
});
it('outputSchema 未設定なら outputFormat は含まれない', () => {
const sdkOptions = buildSdkOptions({ cwd: '/tmp' });
expect(sdkOptions).not.toHaveProperty('outputFormat');
});
});
// ===== QueryExecutor tests (mock @anthropic-ai/claude-agent-sdk) =====
const { mockQuery } = vi.hoisted(() => ({
mockQuery: vi.fn(),
}));
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
query: mockQuery,
AbortError: class AbortError extends Error {
constructor(message?: string) {
super(message);
this.name = 'AbortError';
}
},
}));
// QueryExecutor は executor.ts 内で query() を使うため、mock 後にインポート
const { QueryExecutor } = await import('../infra/claude/executor.js');
/**
* query() が返す Query オブジェクトasync iterable + interruptのモック
*/
function createMockQuery(messages: Array<Record<string, unknown>>) {
return {
[Symbol.asyncIterator]: async function* () {
for (const msg of messages) {
yield msg;
}
},
interrupt: vi.fn(),
};
}
describe('QueryExecutor — structuredOutput 抽出', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('result メッセージの structured_output (snake_case) を抽出する', async () => {
mockQuery.mockReturnValue(createMockQuery([
{ type: 'result', subtype: 'success', result: 'done', structured_output: { step: 2 } },
]));
const executor = new QueryExecutor();
const result = await executor.execute('test', { cwd: '/tmp' });
expect(result.success).toBe(true);
expect(result.structuredOutput).toEqual({ step: 2 });
});
it('result メッセージの structuredOutput (camelCase) を抽出する', async () => {
mockQuery.mockReturnValue(createMockQuery([
{ type: 'result', subtype: 'success', result: 'done', structuredOutput: { step: 3 } },
]));
const executor = new QueryExecutor();
const result = await executor.execute('test', { cwd: '/tmp' });
expect(result.structuredOutput).toEqual({ step: 3 });
});
it('structured_output が snake_case 優先 (snake_case と camelCase 両方ある場合)', async () => {
mockQuery.mockReturnValue(createMockQuery([
{
type: 'result',
subtype: 'success',
result: 'done',
structured_output: { step: 1 },
structuredOutput: { step: 9 },
},
]));
const executor = new QueryExecutor();
const result = await executor.execute('test', { cwd: '/tmp' });
expect(result.structuredOutput).toEqual({ step: 1 });
});
it('structuredOutput がない場合は undefined', async () => {
mockQuery.mockReturnValue(createMockQuery([
{ type: 'result', subtype: 'success', result: 'plain text' },
]));
const executor = new QueryExecutor();
const result = await executor.execute('test', { cwd: '/tmp' });
expect(result.structuredOutput).toBeUndefined();
});
it('structured_output が配列の場合は無視する', async () => {
mockQuery.mockReturnValue(createMockQuery([
{ type: 'result', subtype: 'success', result: 'done', structured_output: [1, 2, 3] },
]));
const executor = new QueryExecutor();
const result = await executor.execute('test', { cwd: '/tmp' });
expect(result.structuredOutput).toBeUndefined();
});
it('structured_output が null の場合は無視する', async () => {
mockQuery.mockReturnValue(createMockQuery([
{ type: 'result', subtype: 'success', result: 'done', structured_output: null },
]));
const executor = new QueryExecutor();
const result = await executor.execute('test', { cwd: '/tmp' });
expect(result.structuredOutput).toBeUndefined();
});
it('assistant テキストと structured_output を同時に取得する', async () => {
mockQuery.mockReturnValue(createMockQuery([
{
type: 'assistant',
message: { content: [{ type: 'text', text: 'thinking...' }] },
},
{
type: 'result',
subtype: 'success',
result: 'final text',
structured_output: { step: 1, reason: 'approved' },
},
]));
const executor = new QueryExecutor();
const result = await executor.execute('test', { cwd: '/tmp' });
expect(result.success).toBe(true);
expect(result.content).toBe('final text');
expect(result.structuredOutput).toEqual({ step: 1, reason: 'approved' });
});
});