import { describe, expect, it } from 'vitest'; import { TOOL_DEFS, executeTool } from './brainstorm.js'; import type { ToolContext } from './core.js'; function makeCtx(): ToolContext { return { workspacePath: '/tmp/no-such', editAllowed: false }; } describe('Brainstorm tool', () => { it('exports the Brainstorm tool definition', () => { expect(TOOL_DEFS).toHaveProperty('Brainstorm'); const def = TOOL_DEFS['Brainstorm']!; expect(def.function.name).toBe('Brainstorm'); const params = def.function.parameters as { required?: string[] }; expect(params.required).toEqual(expect.arrayContaining(['task', 'approaches', 'chosen', 'rationale'])); }); it('returns null for other tool names (dispatch isolation)', async () => { const result = await executeTool('SomethingElse', {}, makeCtx()); expect(result).toBeNull(); }); it('requires task field', async () => { const result = await executeTool('Brainstorm', { approaches: [{ name: 'a', description: 'x' }, { name: 'b', description: 'y' }], chosen: 'a', rationale: 'r' }, makeCtx()); expect(result?.isError).toBe(true); expect(result?.output).toContain('task'); }); it('rejects single approach (needs 2+ for comparison)', async () => { const result = await executeTool('Brainstorm', { task: 't', approaches: [{ name: 'only', description: 'one' }], chosen: 'only', rationale: 'r', }, makeCtx()); expect(result?.isError).toBe(true); expect(result?.output).toContain('2 個以上'); }); it('rejects when chosen does not match any approach name', async () => { const result = await executeTool('Brainstorm', { task: 't', approaches: [{ name: 'a', description: 'x' }, { name: 'b', description: 'y' }], chosen: 'nonexistent', rationale: 'r', }, makeCtx()); expect(result?.isError).toBe(true); expect(result?.output).toContain('一致しません'); }); it('formats the approaches and marks the chosen one', async () => { const result = await executeTool('Brainstorm', { task: 'input/data.xlsx の中身を要約したい', approaches: [ { name: 'ReadExcel 直接', description: 'ReadExcel で読む', reliability: 'high', speed: 'fast' }, { name: 'CSV エクスポート経由', description: 'CSV に変換してから Read', reliability: 'medium', speed: 'medium' }, { name: 'ファイル拡張子確認後判断', description: 'Bash file で本体形式を確認', reliability: 'high', speed: 'slow' }, ], chosen: 'ReadExcel 直接', rationale: '通常はこれが最速で確実', }, makeCtx()); expect(result?.isError).toBe(false); expect(result?.output).toContain('# Brainstorm: input/data.xlsx'); expect(result?.output).toContain('検討した 3 個のアプローチ'); expect(result?.output).toMatch(/✓\s+\*\*ReadExcel 直接\*\*/); expect(result?.output).toContain('採用: ReadExcel 直接'); expect(result?.output).toContain('通常はこれが最速で確実'); expect(result?.output).toContain('確実性: high'); }); it('preserves optional context field for stuck-recovery use case', async () => { const result = await executeTool('Brainstorm', { task: 'output/foo.xlsx を読みたい', context: 'ReadExcel が JSZip エラー、ReadPdf も拡張子 mismatch で reject 済み', approaches: [ { name: 'Glob で実在確認', description: 'Glob output/* で実際のファイル一覧を取る' }, { name: 'ユーザーに ASK', description: '正しいパスを確認' }, ], chosen: 'Glob で実在確認', rationale: '能動的に状況を取りに行ける', }, makeCtx()); expect(result?.isError).toBe(false); expect(result?.output).toContain('背景 / これまでの試行'); expect(result?.output).toContain('JSZip エラー'); }); });