Files
archy/packages/app/src/__tests__/proxy.test.ts
T
DorianandClaude Opus 4.6 cc875d1c43 fix(app): update haiku model ID from retired claude-3-5-haiku to claude-haiku-4-5
The old claude-3-5-haiku-20241022 model ID returns 404 from the Anthropic API.
Updated proxy mapping and test to use claude-haiku-4-5-20251001.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:45:48 +00:00

189 lines
6.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
// Mock proxy request/response logic without actually spawning processes
describe('Proxy Integration', () => {
describe('SSE streaming format', () => {
it('should produce valid SSE content_block_delta events', () => {
const text = 'Hello, world!'
const sseData = {
type: 'content_block_delta',
delta: { type: 'text_delta', text },
}
const sseString = `data: ${JSON.stringify(sseData)}\n\n`
expect(sseString).toMatch(/^data: /)
expect(sseString).toMatch(/\n\n$/)
const parsed = JSON.parse(sseString.replace('data: ', '').trim())
expect(parsed.type).toBe('content_block_delta')
expect(parsed.delta.type).toBe('text_delta')
expect(parsed.delta.text).toBe(text)
})
it('should produce valid DONE event', () => {
const done = 'data: [DONE]\n\n'
expect(done).toBe('data: [DONE]\n\n')
})
it('should produce valid error events', () => {
const errData = {
type: 'error',
error: { message: 'Anthropic API 401: Unauthorized' },
}
const sseString = `data: ${JSON.stringify(errData)}\n\n`
const parsed = JSON.parse(sseString.replace('data: ', '').trim())
expect(parsed.type).toBe('error')
expect(parsed.error.message).toContain('401')
})
})
describe('Model mapping', () => {
function mapModelToApi(model: string): string {
if (model?.includes('opus')) return 'claude-opus-4-20250514'
if (model?.includes('haiku')) return 'claude-haiku-4-5-20251001'
return 'claude-sonnet-4-20250514'
}
it('should map sonnet model correctly', () => {
expect(mapModelToApi('sonnet')).toBe('claude-sonnet-4-20250514')
expect(mapModelToApi('claude-sonnet')).toBe('claude-sonnet-4-20250514')
})
it('should map opus model correctly', () => {
expect(mapModelToApi('opus')).toBe('claude-opus-4-20250514')
expect(mapModelToApi('claude-opus')).toBe('claude-opus-4-20250514')
})
it('should map haiku model correctly', () => {
expect(mapModelToApi('haiku')).toBe('claude-haiku-4-5-20251001')
})
it('should default to sonnet for unknown models', () => {
expect(mapModelToApi('unknown')).toBe('claude-sonnet-4-20250514')
})
})
describe('Request validation', () => {
it('should reject non-POST requests', () => {
const method = 'GET' as string
const isValid = method === 'POST'
expect(isValid).toBe(false)
})
it('should reject unknown paths', () => {
const validPaths = ['/v1/messages', '/v1/openrouter']
expect(validPaths.includes('/v1/unknown')).toBe(false)
expect(validPaths.includes('/v1/messages')).toBe(true)
expect(validPaths.includes('/v1/openrouter')).toBe(true)
})
it('should parse request body correctly', () => {
const body = JSON.stringify({
model: 'sonnet',
messages: [{ role: 'user', content: 'Hello' }],
system: 'You are helpful.',
webSearch: true,
})
const parsed = JSON.parse(body)
expect(parsed.model).toBe('sonnet')
expect(parsed.messages).toHaveLength(1)
expect(parsed.system).toBe('You are helpful.')
expect(parsed.webSearch).toBe(true)
})
it('should handle malformed JSON', () => {
const badBody = 'not json'
expect(() => JSON.parse(badBody)).toThrow()
})
})
describe('Tool use round-trips', () => {
it('should format search_web tool correctly', () => {
const tool = {
name: 'search_web',
description: 'Search the web for current information.',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
},
required: ['query'],
},
}
expect(tool.name).toBe('search_web')
expect(tool.input_schema.properties.query.type).toBe('string')
})
it('should construct tool_result messages correctly', () => {
const toolResult = {
type: 'tool_result',
tool_use_id: 'toolu_123',
content: '1. [Bitcoin price](https://example.com) — Current price is...',
}
expect(toolResult.type).toBe('tool_result')
expect(toolResult.tool_use_id).toBe('toolu_123')
expect(toolResult.content).toContain('Bitcoin')
})
it('should limit tool rounds to 5', () => {
const maxToolRounds = 5
let rounds = 0
while (rounds < maxToolRounds) {
rounds++
}
expect(rounds).toBe(5)
})
})
describe('Error handling', () => {
it('should handle 401 unauthorized', () => {
const status = 401
const errMsg = `Anthropic API ${status}: Unauthorized`
expect(errMsg).toContain('401')
})
it('should handle 429 rate limit', () => {
const status = 429
const errMsg = `Anthropic API ${status}: Rate limited`
expect(errMsg).toContain('429')
})
it('should handle 500 server error', () => {
const status = 500
const errMsg = `Anthropic API ${status}: Internal Server Error`
expect(errMsg).toContain('500')
})
})
describe('OAuth token detection', () => {
const isOAuthToken = (s: string) => /^sk-ant-oat/.test(s)
it('should detect OAuth tokens', () => {
expect(isOAuthToken('sk-ant-oat-abc123')).toBe(true)
})
it('should not flag API keys as OAuth', () => {
expect(isOAuthToken('sk-ant-api03-abc123')).toBe(false)
})
})
describe('Client disconnect handling', () => {
it('should track client disconnection', () => {
const state = { clientDisconnected: false }
// Simulate disconnect
state.clientDisconnected = true
expect(state.clientDisconnected).toBe(true)
})
it('should not write after disconnect', () => {
const clientDisconnected = true
const writes: string[] = []
const write = (data: string) => {
if (!clientDisconnected) writes.push(data)
}
write('should not appear')
expect(writes).toHaveLength(0)
})
})
})