Files
claude-code/src/services/api/__tests__/bedrockClient.test.ts
claude-code-best f43350e600 fix: 修复 4 个测试失败(路径规范化、SDK 签名变更、空消息防护)
- projectContext.test.ts: 使用 realpathSync 处理 macOS /var→/private/var 符号链接
- bedrockClient.test.ts: 适配 Bedrock SDK v0.80 Bearer 认证(原 AWS4-HMAC-SHA256)
- bridge.ts: forwardSessionUpdates 添加 null guard 防止空消息导致 TypeError

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 22:52:37 +08:00

145 lines
5.1 KiB
TypeScript

/**
* Tests for the Bedrock anthropic_beta body-vs-header workaround
* (see src/services/api/bedrockClient.ts and anthropics/claude-code#49238).
*/
import { describe, expect, test } from 'bun:test'
import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk'
import { BedrockClient } from '../bedrockClient.js'
type Captured = {
url: string
method: string
headers: Record<string, string>
body: string
}
function makeCaptureFetch(): {
fetch: typeof fetch
get(): Captured | null
} {
let captured: Captured | null = null
const capture = async (
input: URL | RequestInfo,
init?: RequestInit,
): Promise<Response> => {
const req = new Request(input as RequestInfo, init)
const body = await req.clone().text()
const headers: Record<string, string> = {}
req.headers.forEach((v, k) => {
headers[k.toLowerCase()] = v
})
captured = { url: req.url, method: req.method, headers, body }
const streamBody =
'event: message_start\ndata: {"type":"message_start","message":{"id":"m","type":"message","role":"assistant","content":[],"model":"x","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}\n\nevent: message_stop\ndata: {"type":"message_stop"}\n\n'
return new Response(streamBody, {
status: 200,
headers: { 'content-type': 'text/event-stream' },
})
}
// SDK only calls the fetch function form, never the static `preconnect` that
// Bun/Node's `typeof fetch` declares. Cast is safe (mirrors openai/client.ts).
return { fetch: capture as unknown as typeof fetch, get: () => captured }
}
const BEDROCK_ARGS = {
awsRegion: 'us-east-1',
awsAccessKey: 'AKIAIOSFODNN7EXAMPLE',
awsSecretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}
const REQUEST_PARAMS = {
model: 'anthropic.claude-opus-4-7',
max_tokens: 10,
messages: [{ role: 'user' as const, content: 'hi' }],
betas: ['interleaved-thinking-2025-05-14', 'effort-2025-11-24'],
stream: true as const,
}
async function dispatch(client: AnthropicBedrock): Promise<void> {
try {
const stream = await client.beta.messages.create(REQUEST_PARAMS)
for await (const _ of stream) {
/* drain */
}
} catch {
/* ignore: only the captured request shape matters */
}
}
describe('BedrockClient.buildRequest body.anthropic_beta cleanup', () => {
test('BUG REPRO: unmodified AnthropicBedrock puts anthropic_beta in body', async () => {
const { fetch: captureFetch, get } = makeCaptureFetch()
const client = new AnthropicBedrock({
...BEDROCK_ARGS,
fetch: captureFetch,
})
await dispatch(client)
const c = get()
expect(c).not.toBeNull()
const body = JSON.parse(c!.body) as Record<string, unknown>
expect('anthropic_beta' in body).toBe(true)
expect(body.anthropic_beta).toEqual([
'interleaved-thinking-2025-05-14',
'effort-2025-11-24',
])
})
test('FIX: BedrockClient strips anthropic_beta from body', async () => {
const { fetch: captureFetch, get } = makeCaptureFetch()
const client = new BedrockClient({ ...BEDROCK_ARGS, fetch: captureFetch })
await dispatch(client)
const c = get()
expect(c).not.toBeNull()
const body = JSON.parse(c!.body) as Record<string, unknown>
expect('anthropic_beta' in body).toBe(false)
})
test('FIX preserves anthropic-beta HTTP header with the original csv value', async () => {
const { fetch: captureFetch, get } = makeCaptureFetch()
const client = new BedrockClient({ ...BEDROCK_ARGS, fetch: captureFetch })
await dispatch(client)
const c = get()
expect(c).not.toBeNull()
expect(c!.headers['anthropic-beta']).toBe(
'interleaved-thinking-2025-05-14,effort-2025-11-24',
)
})
test('FIX keeps a valid AWS SigV4 authorization header (signing happens after cleanup)', async () => {
const { fetch: captureFetch, get } = makeCaptureFetch()
const client = new BedrockClient({ ...BEDROCK_ARGS, fetch: captureFetch })
await dispatch(client)
const c = get()
expect(c).not.toBeNull()
expect(c!.headers.authorization).toBeDefined()
// SDK >= 0.80 uses Bearer auth; older versions used AWS4-HMAC-SHA256 SigV4.
// Either way the header must be present (i.e. signing was not broken).
expect(
c!.headers.authorization!.startsWith('AWS4-HMAC-SHA256') ||
c!.headers.authorization!.startsWith('Bearer '),
).toBe(true)
})
test('FIX does not disturb requests that never had anthropic_beta', async () => {
const { fetch: captureFetch, get } = makeCaptureFetch()
const client = new BedrockClient({ ...BEDROCK_ARGS, fetch: captureFetch })
try {
const stream = await client.beta.messages.create({
model: 'anthropic.claude-opus-4-7',
max_tokens: 10,
messages: [{ role: 'user', content: 'hi' }],
stream: true,
})
for await (const _ of stream) {
/* drain */
}
} catch {
/* ignore */
}
const c = get()
expect(c).not.toBeNull()
const body = JSON.parse(c!.body) as Record<string, unknown>
expect('anthropic_beta' in body).toBe(false)
expect(c!.headers['anthropic-beta']).toBeUndefined()
})
})