182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
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');
|
|
});
|
|
});
|