import { describe, it, expect, vi, beforeEach } from 'vitest'; import { BedrockClient } from './bedrock.js'; import type { ChatStreamEvent } from './types.js'; const mockSend = vi.fn().mockResolvedValue({ output: { message: { content: [{ text: 'Hello from Bedrock!' }], }, }, stopReason: 'end_turn', usage: { inputTokens: 10, outputTokens: 5 }, }); vi.mock('@aws-sdk/client-bedrock-runtime', () => ({ BedrockRuntimeClient: vi.fn().mockImplementation(() => ({ send: mockSend, })), ConverseCommand: vi.fn().mockImplementation((params) => params), ConverseStreamCommand: vi.fn().mockImplementation((params) => params), })); describe('BedrockClient', () => { beforeEach(() => { vi.clearAllMocks(); mockSend.mockResolvedValue({ output: { message: { content: [{ text: 'Hello from Bedrock!' }], }, }, stopReason: 'end_turn', usage: { inputTokens: 10, outputTokens: 5 }, }); }); it('sends messages and returns response', async () => { const client = new BedrockClient({ model: 'anthropic.claude-3-sonnet', region: 'us-east-1', }); const response = await client.chat({ messages: [{ role: 'user', content: 'Hello' }], }); expect(response.content).toBe('Hello from Bedrock!'); expect(response.stopReason).toBe('end_turn'); expect(response.usage.inputTokens).toBe(10); expect(response.usage.outputTokens).toBe(5); }); it('parses tool use response', async () => { mockSend.mockResolvedValueOnce({ output: { message: { content: [{ toolUse: { toolUseId: 'tool_01', name: 'shell.exec', input: { command: 'ls' }, }, }], }, }, stopReason: 'tool_use', usage: { inputTokens: 20, outputTokens: 15 }, }); const client = new BedrockClient({ model: 'anthropic.claude-3-sonnet', region: 'us-east-1', }); const response = await client.chat({ messages: [{ role: 'user', content: 'list files' }], tools: [{ name: 'shell.exec', description: 'Run shell command', input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] }, }], }); expect(response.stopReason).toBe('tool_use'); expect(response.toolCalls).toHaveLength(1); const firstToolCall = response.toolCalls?.[0]; expect(firstToolCall?.name).toBe('shell.exec'); expect(firstToolCall?.args).toEqual({ command: 'ls' }); }); it('uses default region when none provided', async () => { const client = new BedrockClient({ model: 'anthropic.claude-3-sonnet', }); const response = await client.chat({ messages: [{ role: 'user', content: 'Hello' }], }); expect(response.content).toBe('Hello from Bedrock!'); }); it('passes system prompt to API', async () => { const client = new BedrockClient({ model: 'anthropic.claude-3-sonnet', region: 'us-east-1', }); await client.chat({ messages: [{ role: 'user', content: 'Hello' }], system: 'You are a helpful assistant.', }); expect(mockSend).toHaveBeenCalledTimes(1); // ConverseCommand is called with params that include system const { ConverseCommand } = await import('@aws-sdk/client-bedrock-runtime'); expect(ConverseCommand).toHaveBeenCalledWith( expect.objectContaining({ system: [{ text: 'You are a helpful assistant.' }], }), ); }); }); describe('BedrockClient streaming', () => { beforeEach(() => { vi.clearAllMocks(); }); it('streams content events', async () => { mockSend.mockResolvedValueOnce({ stream: (async function* () { yield { contentBlockDelta: { delta: { text: 'Hello ' } } }; yield { contentBlockDelta: { delta: { text: 'from Bedrock!' } } }; yield { metadata: { usage: { inputTokens: 10, outputTokens: 5 } } }; })(), }); const client = new BedrockClient({ model: 'anthropic.claude-3-sonnet', region: 'us-east-1', }); const chunks: string[] = []; let finalUsage: { inputTokens: number; outputTokens: number } | undefined; for await (const event of client.chatStream({ messages: [{ role: 'user', content: 'Hello' }], })) { if (event.type === 'content' && event.content) { chunks.push(event.content); } if (event.type === 'done' && event.usage) { finalUsage = event.usage; } } expect(chunks.join('')).toBe('Hello from Bedrock!'); expect(finalUsage).toEqual({ inputTokens: 10, outputTokens: 5 }); }); it('yields error event on failure', async () => { mockSend.mockRejectedValueOnce(new Error('Service unavailable')); const client = new BedrockClient({ model: 'anthropic.claude-3-sonnet', region: 'us-east-1', }); const events: ChatStreamEvent[] = []; for await (const event of client.chatStream({ messages: [{ role: 'user', content: 'Hello' }], })) { events.push(event); } expect(events).toHaveLength(1); expect(events[0].type).toBe('error'); expect(events[0].error?.message).toBe('Service unavailable'); }); });