mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
653 lines
22 KiB
TypeScript
653 lines
22 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Bedrock Provider Unit Tests
|
|
* ADR-043: Vendor-Independent LLM Support (Milestone 6)
|
|
*
|
|
* Tests AWS Bedrock provider implementation including:
|
|
* - AWS credential configuration
|
|
* - ARN model ID mapping
|
|
* - Chat completion
|
|
* - Streaming (mocked)
|
|
* - Error handling
|
|
* - Region configuration
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
BedrockProvider,
|
|
DEFAULT_BEDROCK_CONFIG,
|
|
BEDROCK_MODEL_MAPPING,
|
|
BEDROCK_MODEL_REVERSE_MAPPING,
|
|
type BedrockConfig,
|
|
} from '../../../../../src/shared/llm/providers/bedrock';
|
|
import { CostTracker } from '../../../../../src/shared/llm/cost-tracker';
|
|
|
|
// Mock fetch globally
|
|
const mockFetch = vi.fn();
|
|
global.fetch = mockFetch;
|
|
|
|
// Mock TokenMetricsCollector to avoid side effects
|
|
vi.mock('../../../../../src/learning/token-tracker.js', () => ({
|
|
TokenMetricsCollector: {
|
|
recordTokenUsage: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('BedrockProvider', () => {
|
|
let provider: BedrockProvider;
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
// Reset environment
|
|
process.env = { ...originalEnv };
|
|
process.env.AWS_ACCESS_KEY_ID = 'test-access-key-id';
|
|
process.env.AWS_SECRET_ACCESS_KEY = 'test-secret-access-key';
|
|
process.env.AWS_REGION = 'us-east-1';
|
|
|
|
// Reset mocks
|
|
mockFetch.mockReset();
|
|
|
|
// Create provider instance
|
|
provider = new BedrockProvider();
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
// =========================================================================
|
|
// 1. Constructor and Configuration Tests
|
|
// =========================================================================
|
|
|
|
describe('constructor and configuration', () => {
|
|
it('should use default configuration when no config provided', () => {
|
|
const config = provider.getConfig();
|
|
|
|
expect(config.model).toBe(DEFAULT_BEDROCK_CONFIG.model);
|
|
expect(config.region).toBe(DEFAULT_BEDROCK_CONFIG.region);
|
|
expect(config.maxTokens).toBe(DEFAULT_BEDROCK_CONFIG.maxTokens);
|
|
expect(config.temperature).toBe(DEFAULT_BEDROCK_CONFIG.temperature);
|
|
});
|
|
|
|
it('should override defaults with provided configuration', () => {
|
|
const customConfig: Partial<BedrockConfig> = {
|
|
model: 'anthropic.claude-3-opus-20240229-v1:0',
|
|
region: 'eu-west-1',
|
|
maxTokens: 8192,
|
|
temperature: 0.5,
|
|
};
|
|
|
|
const customProvider = new BedrockProvider(customConfig);
|
|
const config = customProvider.getConfig();
|
|
|
|
expect(config.model).toBe(customConfig.model);
|
|
expect(config.region).toBe(customConfig.region);
|
|
expect(config.maxTokens).toBe(customConfig.maxTokens);
|
|
expect(config.temperature).toBe(customConfig.temperature);
|
|
});
|
|
|
|
it('should have correct provider type and name', () => {
|
|
expect(provider.type).toBe('bedrock');
|
|
expect(provider.name).toBe('AWS Bedrock');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 2. AWS Credential Configuration Tests
|
|
// =========================================================================
|
|
|
|
describe('AWS credential configuration', () => {
|
|
it('should use environment variables for credentials', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
id: 'msg-123',
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'Hello' }],
|
|
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
|
stop_reason: 'end_turn',
|
|
usage: { input_tokens: 10, output_tokens: 5 },
|
|
}),
|
|
});
|
|
|
|
const result = await provider.isAvailable();
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should use config credentials over environment variables', () => {
|
|
const customProvider = new BedrockProvider({
|
|
accessKeyId: 'config-access-key',
|
|
secretAccessKey: 'config-secret-key',
|
|
region: 'us-west-2',
|
|
});
|
|
|
|
const config = customProvider.getConfig();
|
|
expect(config.accessKeyId).toBe('config-access-key');
|
|
expect(config.secretAccessKey).toBe('config-secret-key');
|
|
expect(config.region).toBe('us-west-2');
|
|
});
|
|
|
|
it('should return false when credentials are missing', async () => {
|
|
delete process.env.AWS_ACCESS_KEY_ID;
|
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
|
|
|
const unconfiguredProvider = new BedrockProvider();
|
|
const result = await unconfiguredProvider.isAvailable();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should support session token for temporary credentials', () => {
|
|
const customProvider = new BedrockProvider({
|
|
accessKeyId: 'access-key',
|
|
secretAccessKey: 'secret-key',
|
|
sessionToken: 'session-token',
|
|
});
|
|
|
|
const config = customProvider.getConfig();
|
|
expect(config.sessionToken).toBe('session-token');
|
|
});
|
|
|
|
it('should use AWS_SESSION_TOKEN from environment', () => {
|
|
process.env.AWS_SESSION_TOKEN = 'env-session-token';
|
|
|
|
const envProvider = new BedrockProvider();
|
|
// The session token is retrieved dynamically during signing
|
|
// Just verify the provider can be created without errors
|
|
expect(envProvider).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 3. Region Configuration Tests
|
|
// =========================================================================
|
|
|
|
describe('region configuration', () => {
|
|
it('should use config region first', () => {
|
|
const customProvider = new BedrockProvider({ region: 'ap-southeast-1' });
|
|
const config = customProvider.getConfig();
|
|
expect(config.region).toBe('ap-southeast-1');
|
|
});
|
|
|
|
it('should fall back to AWS_REGION environment variable', () => {
|
|
process.env.AWS_REGION = 'eu-central-1';
|
|
const envProvider = new BedrockProvider();
|
|
const config = envProvider.getConfig();
|
|
// Default config has us-east-1, but internal getRegion() would use env
|
|
expect(config.region).toBe('us-east-1'); // From DEFAULT_BEDROCK_CONFIG
|
|
});
|
|
|
|
it('should fall back to AWS_DEFAULT_REGION environment variable', () => {
|
|
delete process.env.AWS_REGION;
|
|
process.env.AWS_DEFAULT_REGION = 'eu-north-1';
|
|
|
|
// When region is not specified (not explicitly undefined), it uses the default
|
|
const envProvider = new BedrockProvider();
|
|
// Region lookup happens internally during API calls via getRegion()
|
|
// The config stores the default, but getRegion() checks env vars at runtime
|
|
expect(envProvider.getConfig().region).toBe('us-east-1');
|
|
});
|
|
|
|
it('should default to us-east-1 when no region specified', () => {
|
|
delete process.env.AWS_REGION;
|
|
delete process.env.AWS_DEFAULT_REGION;
|
|
|
|
// Don't pass region at all to use default
|
|
const noRegionProvider = new BedrockProvider();
|
|
expect(noRegionProvider.getConfig().region).toBe('us-east-1');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 4. ARN Model ID Mapping Tests
|
|
// =========================================================================
|
|
|
|
describe('ARN model ID mapping', () => {
|
|
it('should map claude-3-5-sonnet to Bedrock format', () => {
|
|
const bedrockId = BEDROCK_MODEL_MAPPING['claude-3-5-sonnet-20241022'];
|
|
expect(bedrockId).toBe('anthropic.claude-3-5-sonnet-20241022-v2:0');
|
|
});
|
|
|
|
it('should map claude-opus-4-5 to Bedrock format', () => {
|
|
const bedrockId = BEDROCK_MODEL_MAPPING['claude-opus-4-5-20251101'];
|
|
expect(bedrockId).toBe('anthropic.claude-opus-4-5-v1:0');
|
|
});
|
|
|
|
it('should map claude-haiku-3-5 to Bedrock format', () => {
|
|
const bedrockId = BEDROCK_MODEL_MAPPING['claude-3-5-haiku-20241022'];
|
|
expect(bedrockId).toBe('anthropic.claude-3-5-haiku-v1:0');
|
|
});
|
|
|
|
it('should have reverse mapping for all Bedrock model IDs', () => {
|
|
// Verify all Bedrock model IDs have a reverse mapping
|
|
// Note: Multiple canonical IDs may map to the same Bedrock ID (e.g., claude-opus-4-5 and claude-opus-4-5-20251101)
|
|
// The reverse mapping picks one of them
|
|
const bedrockIds = new Set(Object.values(BEDROCK_MODEL_MAPPING));
|
|
for (const bedrockId of bedrockIds) {
|
|
expect(BEDROCK_MODEL_REVERSE_MAPPING[bedrockId]).toBeDefined();
|
|
// The reverse mapping should point to one of the canonical IDs
|
|
expect(BEDROCK_MODEL_MAPPING[BEDROCK_MODEL_REVERSE_MAPPING[bedrockId]]).toBe(bedrockId);
|
|
}
|
|
});
|
|
|
|
it('should support legacy Claude 3 models', () => {
|
|
expect(BEDROCK_MODEL_MAPPING['claude-3-opus-20240229']).toBe('anthropic.claude-3-opus-20240229-v1:0');
|
|
expect(BEDROCK_MODEL_MAPPING['claude-3-sonnet-20240229']).toBe('anthropic.claude-3-sonnet-20240229-v1:0');
|
|
expect(BEDROCK_MODEL_MAPPING['claude-3-haiku-20240307']).toBe('anthropic.claude-3-haiku-20240307-v1:0');
|
|
});
|
|
|
|
it('should list all supported models', () => {
|
|
const supportedModels = provider.getSupportedModels();
|
|
|
|
expect(supportedModels).toContain('claude-3-5-sonnet-20241022');
|
|
expect(supportedModels).toContain('claude-opus-4-5-20251101');
|
|
expect(supportedModels).toContain('claude-3-5-haiku-20241022');
|
|
expect(supportedModels.length).toBeGreaterThanOrEqual(10);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 5. Chat Completion Tests
|
|
// =========================================================================
|
|
|
|
describe('chat completion', () => {
|
|
const mockSuccessResponse = {
|
|
id: 'msg-abc123',
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'Hello! How can I help you today?' }],
|
|
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
|
stop_reason: 'end_turn',
|
|
usage: { input_tokens: 15, output_tokens: 10 },
|
|
};
|
|
|
|
it('should generate response from string input', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockSuccessResponse),
|
|
});
|
|
|
|
const response = await provider.generate('Hello, Claude!');
|
|
|
|
expect(response.content).toBe('Hello! How can I help you today?');
|
|
expect(response.provider).toBe('bedrock');
|
|
expect(response.usage.promptTokens).toBe(15);
|
|
expect(response.usage.completionTokens).toBe(10);
|
|
expect(response.finishReason).toBe('stop');
|
|
});
|
|
|
|
it('should generate response from message array', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockSuccessResponse),
|
|
});
|
|
|
|
const messages = [
|
|
{ role: 'user' as const, content: 'What is 2+2?' },
|
|
];
|
|
|
|
const response = await provider.generate(messages);
|
|
|
|
expect(response.content).toBe('Hello! How can I help you today?');
|
|
expect(response.provider).toBe('bedrock');
|
|
});
|
|
|
|
it('should include system prompt in request', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockSuccessResponse),
|
|
});
|
|
|
|
await provider.generate('Hello', {
|
|
systemPrompt: 'You are a helpful assistant.',
|
|
});
|
|
|
|
// Verify the request body included system prompt
|
|
const callArgs = mockFetch.mock.calls[0];
|
|
const requestBody = JSON.parse(callArgs[1].body);
|
|
expect(requestBody.system).toBe('You are a helpful assistant.');
|
|
});
|
|
|
|
it('should respect temperature setting', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockSuccessResponse),
|
|
});
|
|
|
|
await provider.generate('Hello', { temperature: 0.3 });
|
|
|
|
const callArgs = mockFetch.mock.calls[0];
|
|
const requestBody = JSON.parse(callArgs[1].body);
|
|
expect(requestBody.temperature).toBe(0.3);
|
|
});
|
|
|
|
it('should clamp temperature to 0-1 range', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockSuccessResponse),
|
|
});
|
|
|
|
await provider.generate('Hello', { temperature: 2.0 });
|
|
|
|
const callArgs = mockFetch.mock.calls[0];
|
|
const requestBody = JSON.parse(callArgs[1].body);
|
|
expect(requestBody.temperature).toBe(1);
|
|
});
|
|
|
|
it('should include stop sequences when provided', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockSuccessResponse),
|
|
});
|
|
|
|
await provider.generate('Hello', { stopSequences: ['STOP', 'END'] });
|
|
|
|
const callArgs = mockFetch.mock.calls[0];
|
|
const requestBody = JSON.parse(callArgs[1].body);
|
|
expect(requestBody.stop_sequences).toEqual(['STOP', 'END']);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 6. Error Handling Tests
|
|
// =========================================================================
|
|
|
|
describe('error handling', () => {
|
|
it('should throw API_KEY_MISSING when credentials not configured', async () => {
|
|
delete process.env.AWS_ACCESS_KEY_ID;
|
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
|
|
|
const unconfiguredProvider = new BedrockProvider();
|
|
|
|
await expect(unconfiguredProvider.generate('Hello')).rejects.toMatchObject({
|
|
code: 'API_KEY_MISSING',
|
|
provider: 'bedrock',
|
|
});
|
|
});
|
|
|
|
it('should throw API_KEY_INVALID on 401 response', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
json: () => Promise.resolve({ message: 'Invalid credentials' }),
|
|
});
|
|
|
|
await expect(provider.generate('Hello')).rejects.toMatchObject({
|
|
code: 'API_KEY_INVALID',
|
|
provider: 'bedrock',
|
|
});
|
|
});
|
|
|
|
it('should throw API_KEY_INVALID on 403 response', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 403,
|
|
json: () => Promise.resolve({ message: 'Access denied' }),
|
|
});
|
|
|
|
await expect(provider.generate('Hello')).rejects.toMatchObject({
|
|
code: 'API_KEY_INVALID',
|
|
provider: 'bedrock',
|
|
});
|
|
});
|
|
|
|
it('should throw RATE_LIMITED on 429 response', async () => {
|
|
// Provider with maxRetries=1 to avoid retry delays in tests
|
|
const noRetryProvider = new BedrockProvider({ maxRetries: 1 });
|
|
|
|
mockFetch.mockResolvedValue({
|
|
ok: false,
|
|
status: 429,
|
|
json: () => Promise.resolve({ message: 'Too many requests' }),
|
|
});
|
|
|
|
await expect(noRetryProvider.generate('Hello')).rejects.toMatchObject({
|
|
code: 'RATE_LIMITED',
|
|
provider: 'bedrock',
|
|
retryable: true,
|
|
});
|
|
});
|
|
|
|
it('should throw CONTEXT_LENGTH_EXCEEDED on validation error', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 400,
|
|
json: () => Promise.resolve({
|
|
message: 'Input token count exceeds maximum',
|
|
__type: 'ValidationException',
|
|
}),
|
|
});
|
|
|
|
await expect(provider.generate('Hello')).rejects.toMatchObject({
|
|
code: 'CONTEXT_LENGTH_EXCEEDED',
|
|
provider: 'bedrock',
|
|
retryable: false,
|
|
});
|
|
});
|
|
|
|
it('should throw PROVIDER_UNAVAILABLE on 500 response', async () => {
|
|
// Provider with maxRetries=1 to avoid retry delays in tests
|
|
const noRetryProvider = new BedrockProvider({ maxRetries: 1 });
|
|
|
|
mockFetch.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
json: () => Promise.resolve({ message: 'Internal server error' }),
|
|
});
|
|
|
|
await expect(noRetryProvider.generate('Hello')).rejects.toMatchObject({
|
|
code: 'PROVIDER_UNAVAILABLE',
|
|
provider: 'bedrock',
|
|
retryable: true,
|
|
});
|
|
});
|
|
|
|
it('should throw TIMEOUT on request timeout', async () => {
|
|
// Provider with maxRetries=1 to avoid retry delays in tests
|
|
const noRetryProvider = new BedrockProvider({ maxRetries: 1 });
|
|
|
|
mockFetch.mockImplementation(() => {
|
|
const error = new Error('Request timed out');
|
|
error.name = 'AbortError';
|
|
return Promise.reject(error);
|
|
});
|
|
|
|
await expect(noRetryProvider.generate('Hello', { timeoutMs: 100 })).rejects.toMatchObject({
|
|
code: 'TIMEOUT',
|
|
provider: 'bedrock',
|
|
retryable: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 7. Health Check Tests
|
|
// =========================================================================
|
|
|
|
describe('health check', () => {
|
|
it('should return healthy when API responds successfully', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
id: 'msg-health',
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'Hi' }],
|
|
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
|
stop_reason: 'end_turn',
|
|
usage: { input_tokens: 2, output_tokens: 1 },
|
|
}),
|
|
});
|
|
|
|
const result = await provider.healthCheck();
|
|
|
|
expect(result.healthy).toBe(true);
|
|
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
|
|
expect(result.models).toContain('claude-3-5-sonnet-20241022');
|
|
expect(result.details?.region).toBe('us-east-1');
|
|
});
|
|
|
|
it('should return unhealthy when credentials missing', async () => {
|
|
delete process.env.AWS_ACCESS_KEY_ID;
|
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
|
|
|
const unconfiguredProvider = new BedrockProvider();
|
|
const result = await unconfiguredProvider.healthCheck();
|
|
|
|
expect(result.healthy).toBe(false);
|
|
expect(result.error).toContain('AWS credentials not configured');
|
|
});
|
|
|
|
it('should return unhealthy on API error', async () => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
text: () => Promise.resolve('Internal Server Error'),
|
|
});
|
|
|
|
const result = await provider.healthCheck();
|
|
|
|
expect(result.healthy).toBe(false);
|
|
expect(result.error).toContain('API error: 500');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 8. Cost Calculation Tests
|
|
// =========================================================================
|
|
|
|
describe('cost calculation', () => {
|
|
it('should calculate cost for Bedrock models', () => {
|
|
const cost = CostTracker.calculateCost('anthropic.claude-3-5-sonnet-20241022-v2:0', {
|
|
promptTokens: 1000,
|
|
completionTokens: 500,
|
|
totalTokens: 1500,
|
|
});
|
|
|
|
// $3/1M input, $15/1M output
|
|
expect(cost.inputCost).toBeCloseTo(0.003, 5);
|
|
expect(cost.outputCost).toBeCloseTo(0.0075, 5);
|
|
expect(cost.totalCost).toBeCloseTo(0.0105, 5);
|
|
expect(cost.currency).toBe('USD');
|
|
});
|
|
|
|
it('should return cost per token for current model', () => {
|
|
const costPerToken = provider.getCostPerToken();
|
|
|
|
expect(costPerToken.input).toBeGreaterThan(0);
|
|
expect(costPerToken.output).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should calculate cost for opus model correctly', () => {
|
|
const cost = CostTracker.calculateCost('anthropic.claude-opus-4-5-v1:0', {
|
|
promptTokens: 1000,
|
|
completionTokens: 500,
|
|
totalTokens: 1500,
|
|
});
|
|
|
|
// $15/1M input, $75/1M output
|
|
expect(cost.inputCost).toBeCloseTo(0.015, 5);
|
|
expect(cost.outputCost).toBeCloseTo(0.0375, 5);
|
|
expect(cost.totalCost).toBeCloseTo(0.0525, 5);
|
|
});
|
|
|
|
it('should calculate cost for haiku model correctly', () => {
|
|
const cost = CostTracker.calculateCost('anthropic.claude-3-5-haiku-v1:0', {
|
|
promptTokens: 1000,
|
|
completionTokens: 500,
|
|
totalTokens: 1500,
|
|
});
|
|
|
|
// $1/1M input, $5/1M output
|
|
expect(cost.inputCost).toBeCloseTo(0.001, 5);
|
|
expect(cost.outputCost).toBeCloseTo(0.0025, 5);
|
|
expect(cost.totalCost).toBeCloseTo(0.0035, 5);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 9. Embedding Tests
|
|
// =========================================================================
|
|
|
|
describe('embeddings', () => {
|
|
it('should throw MODEL_NOT_FOUND for embed requests', async () => {
|
|
await expect(provider.embed('test text')).rejects.toMatchObject({
|
|
code: 'MODEL_NOT_FOUND',
|
|
provider: 'bedrock',
|
|
});
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 10. Completion Tests
|
|
// =========================================================================
|
|
|
|
describe('completion', () => {
|
|
it('should complete text with code-optimized settings', async () => {
|
|
const mockResponse = {
|
|
id: 'msg-complete',
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'function completed() {}' }],
|
|
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
|
stop_reason: 'end_turn',
|
|
usage: { input_tokens: 20, output_tokens: 10 },
|
|
};
|
|
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockResponse),
|
|
});
|
|
|
|
const result = await provider.complete('function incomplete() {');
|
|
|
|
expect(result.completion).toBe('function completed() {}');
|
|
expect(result.provider).toBe('bedrock');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 11. Dispose Tests
|
|
// =========================================================================
|
|
|
|
describe('dispose', () => {
|
|
it('should dispose without errors', async () => {
|
|
await expect(provider.dispose()).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 12. Default Configuration Tests
|
|
// =========================================================================
|
|
|
|
describe('default configuration', () => {
|
|
it('should have correct default model', () => {
|
|
expect(DEFAULT_BEDROCK_CONFIG.model).toBe('anthropic.claude-3-5-sonnet-20241022-v2:0');
|
|
});
|
|
|
|
it('should have correct default region', () => {
|
|
expect(DEFAULT_BEDROCK_CONFIG.region).toBe('us-east-1');
|
|
});
|
|
|
|
it('should have correct default maxTokens', () => {
|
|
expect(DEFAULT_BEDROCK_CONFIG.maxTokens).toBe(4096);
|
|
});
|
|
|
|
it('should have correct default temperature', () => {
|
|
expect(DEFAULT_BEDROCK_CONFIG.temperature).toBe(0.7);
|
|
});
|
|
|
|
it('should have correct default timeout', () => {
|
|
expect(DEFAULT_BEDROCK_CONFIG.timeoutMs).toBe(60000);
|
|
});
|
|
|
|
it('should enable cache by default', () => {
|
|
expect(DEFAULT_BEDROCK_CONFIG.enableCache).toBe(true);
|
|
});
|
|
|
|
it('should enable circuit breaker by default', () => {
|
|
expect(DEFAULT_BEDROCK_CONFIG.enableCircuitBreaker).toBe(true);
|
|
});
|
|
});
|
|
});
|