|
| 1 | +/** |
| 2 | + * brain_briefing — v4 Move 2: proactive briefing. |
| 3 | + * |
| 4 | + * Verifies: |
| 5 | + * - rejects invalid event_type |
| 6 | + * - rejects empty context |
| 7 | + * - shows "no risk" message when no warnings returned |
| 8 | + * - renders ranked warning table with risk level |
| 9 | + * - graceful fallback when API unreachable |
| 10 | + */ |
| 11 | + |
| 12 | +import { describe, it, expect } from 'vitest'; |
| 13 | +import { handleSyndicateTool } from '../handlers/syndicate.js'; |
| 14 | + |
| 15 | +const INSTANCE = 'test-instance'; |
| 16 | + |
| 17 | +function makeConn() { |
| 18 | + return (async (_id: string) => ({})) as unknown as Parameters<typeof handleSyndicateTool>[2]; |
| 19 | +} |
| 20 | + |
| 21 | +async function callTool( |
| 22 | + name: string, |
| 23 | + args: Record<string, unknown>, |
| 24 | + apiFetch: Parameters<typeof handleSyndicateTool>[3], |
| 25 | +) { |
| 26 | + return handleSyndicateTool(name, { instance_id: INSTANCE, ...args }, makeConn(), apiFetch); |
| 27 | +} |
| 28 | + |
| 29 | +function mockFetch(response: unknown) { |
| 30 | + return (async () => response) as unknown as Parameters<typeof handleSyndicateTool>[3]; |
| 31 | +} |
| 32 | + |
| 33 | +function failFetch() { |
| 34 | + return (async () => { throw new Error('API down'); }) as unknown as Parameters<typeof handleSyndicateTool>[3]; |
| 35 | +} |
| 36 | + |
| 37 | +describe('brain_briefing', () => { |
| 38 | + it('rejects invalid event_type', async () => { |
| 39 | + await expect(callTool('brain_briefing', { event_type: 'on_save', context: 'auth.ts' }, mockFetch({}))) |
| 40 | + .rejects.toThrow(/event_type must be/); |
| 41 | + }); |
| 42 | + |
| 43 | + it('rejects empty context', async () => { |
| 44 | + await expect(callTool('brain_briefing', { event_type: 'file_open', context: ' ' }, mockFetch({}))) |
| 45 | + .rejects.toThrow(/context is required/); |
| 46 | + }); |
| 47 | + |
| 48 | + it('shows no-risk message when no warnings match', async () => { |
| 49 | + const fetch = mockFetch({ event_type: 'file_open', risk_level: 'low', warnings: [], matched_lessons: 3 }); |
| 50 | + const result = await callTool('brain_briefing', { event_type: 'file_open', context: 'src/auth/login.ts' }, fetch); |
| 51 | + expect(result).toContain('no known risk patterns matched'); |
| 52 | + expect(result).toContain('3 related lessons'); |
| 53 | + }); |
| 54 | + |
| 55 | + it('renders ranked warning table with risk level', async () => { |
| 56 | + const fetch = mockFetch({ |
| 57 | + event_type: 'pr_open', |
| 58 | + risk_level: 'high', |
| 59 | + matched_lessons: 4, |
| 60 | + warnings: [ |
| 61 | + { topic: 'auth:jwt', confidence: 0.85, severity: 'critical', message: 'JWT refresh race condition', fix: 'Lock token refresh with a mutex' }, |
| 62 | + { topic: 'deploy:k8s', confidence: 0.62, severity: 'major', message: 'Readiness probe too aggressive', fix: 'Increase failureThreshold to 10' }, |
| 63 | + ], |
| 64 | + }); |
| 65 | + const result = await callTool('brain_briefing', { event_type: 'pr_open', context: 'Refactor auth + deploy pipeline' }, fetch); |
| 66 | + expect(result).toContain('risk: HIGH'); |
| 67 | + expect(result).toContain('auth:jwt'); |
| 68 | + expect(result).toContain('85%'); |
| 69 | + expect(result).toContain('Lock token refresh with a mutex'); |
| 70 | + }); |
| 71 | + |
| 72 | + it('passes threshold through to the API when provided', async () => { |
| 73 | + let capturedBody: string | undefined; |
| 74 | + const fetch = (async (_path: string, opts?: RequestInit) => { |
| 75 | + capturedBody = opts?.body as string; |
| 76 | + return { event_type: 'deploy', risk_level: 'low', warnings: [], matched_lessons: 0 }; |
| 77 | + }) as unknown as Parameters<typeof handleSyndicateTool>[3]; |
| 78 | + |
| 79 | + await callTool('brain_briefing', { event_type: 'deploy', context: 'Deploy v2.4 to production', threshold: 0.8 }, fetch); |
| 80 | + expect(capturedBody).toBeDefined(); |
| 81 | + expect(JSON.parse(capturedBody as string)).toMatchObject({ event_type: 'deploy', threshold: 0.8 }); |
| 82 | + }); |
| 83 | + |
| 84 | + it('falls back gracefully when API is unreachable', async () => { |
| 85 | + const result = await callTool('brain_briefing', { event_type: 'file_open', context: 'src/db/migrate.go' }, failFetch()); |
| 86 | + expect(result).toContain('Could not reach Brain API'); |
| 87 | + }); |
| 88 | +}); |
0 commit comments