Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 406 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407import type { BetaContentBlock } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'import { createHash, randomUUID, type UUID } from 'crypto'import { mkdir, readFile, writeFile } from 'fs/promises'import isPlainObject from 'lodash-es/isPlainObject.js'import mapValues from 'lodash-es/mapValues.js'import { dirname, join } from 'path'import { addToTotalSessionCost } from 'src/cost-tracker.js'import { calculateUSDCost } from 'src/utils/modelCost.js'import type { AssistantMessage, Message, StreamEvent, SystemAPIErrorMessage, UserMessage,} from '../types/message.js'import { getCwd } from '../utils/cwd.js'import { env } from '../utils/env.js'import { getClaudeConfigHomeDir, isEnvTruthy } from '../utils/envUtils.js'import { getErrnoCode } from '../utils/errors.js'import { normalizeMessagesForAPI } from '../utils/messages.js'import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
function shouldUseVCR(): boolean { if (process.env.NODE_ENV === 'test') { return true }
if (process.env.USER_TYPE === 'ant' && isEnvTruthy(process.env.FORCE_VCR)) { return true }
return false}
/** * Generic fixture management helper * Handles caching, reading, writing fixtures for any data type */async function withFixture<T>( input: unknown, fixtureName: string, f: () => Promise<T>,): Promise<T> { if (!shouldUseVCR()) { return await f() }
// Create hash of input for fixture filename const hash = createHash('sha1') .update(jsonStringify(input)) .digest('hex') .slice(0, 12) const filename = join( process.env.CLAUDE_CODE_TEST_FIXTURES_ROOT ?? getCwd(), `fixtures/${fixtureName}-${hash}.json`, )
// Fetch cached fixture try { const cached = jsonParse( await readFile(filename, { encoding: 'utf8' }), ) as T return cached } catch (e: unknown) { const code = getErrnoCode(e) if (code !== 'ENOENT') { throw e } }
if ((env.isCI || process.env.CI) && !isEnvTruthy(process.env.VCR_RECORD)) { throw new Error( `Fixture missing: ${filename}. Re-run tests with VCR_RECORD=1, then commit the result.`, ) }
// Create & write new fixture const result = await f()
await mkdir(dirname(filename), { recursive: true }) await writeFile(filename, jsonStringify(result, null, 2), { encoding: 'utf8', })
return result}
export async function withVCR( messages: Message[], f: () => Promise<(AssistantMessage | StreamEvent | SystemAPIErrorMessage)[]>,): Promise<(AssistantMessage | StreamEvent | SystemAPIErrorMessage)[]> { if (!shouldUseVCR()) { return await f() }
const messagesForAPI = normalizeMessagesForAPI( messages.filter(_ => { if (_.type !== 'user') { return true } if (_.isMeta) { return false } return true }), )
const dehydratedInput = mapMessages( messagesForAPI.map(_ => _.message.content), dehydrateValue, ) const filename = join( process.env.CLAUDE_CODE_TEST_FIXTURES_ROOT ?? getCwd(), `fixtures/${dehydratedInput.map(_ => createHash('sha1').update(jsonStringify(_)).digest('hex').slice(0, 6)).join('-')}.json`, )
// Fetch cached fixture try { const cached = jsonParse( await readFile(filename, { encoding: 'utf8' }), ) as { output: (AssistantMessage | StreamEvent)[] } cached.output.forEach(addCachedCostToTotalSessionCost) return cached.output.map((message, index) => mapMessage(message, hydrateValue, index, randomUUID()), ) } catch (e: unknown) { const code = getErrnoCode(e) if (code !== 'ENOENT') { throw e } }
if (env.isCI && !isEnvTruthy(process.env.VCR_RECORD)) { throw new Error( `Anthropic API fixture missing: ${filename}. Re-run tests with VCR_RECORD=1, then commit the result. Input messages:\n${jsonStringify(dehydratedInput, null, 2)}`, ) }
// Create & write new fixture const results = await f() if (env.isCI && !isEnvTruthy(process.env.VCR_RECORD)) { return results }
await mkdir(dirname(filename), { recursive: true }) await writeFile( filename, jsonStringify( { input: dehydratedInput, output: results.map((message, index) => mapMessage(message, dehydrateValue, index), ), }, null, 2, ), { encoding: 'utf8' }, ) return results}
function addCachedCostToTotalSessionCost( message: AssistantMessage | StreamEvent,): void { if (message.type === 'stream_event') { return } const model = message.message.model const usage = message.message.usage const costUSD = calculateUSDCost(model, usage) addToTotalSessionCost(costUSD, usage, model)}
function mapMessages( messages: (UserMessage | AssistantMessage)['message']['content'][], f: (s: unknown) => unknown,): (UserMessage | AssistantMessage)['message']['content'][] { return messages.map(_ => { if (typeof _ === 'string') { return f(_) } return _.map(_ => { switch (_.type) { case 'tool_result': if (typeof _.content === 'string') { return { ..._, content: f(_.content) } } if (Array.isArray(_.content)) { return { ..._, content: _.content.map(_ => { switch (_.type) { case 'text': return { ..._, text: f(_.text) } case 'image': return _ default: return undefined } }), } } return _ case 'text': return { ..._, text: f(_.text) } case 'tool_use': return { ..._, input: mapValuesDeep(_.input as Record<string, unknown>, f), } case 'image': return _ default: return undefined } }) }) as (UserMessage | AssistantMessage)['message']['content'][]}
function mapValuesDeep( obj: { [x: string]: unknown }, f: (val: unknown, key: string, obj: Record<string, unknown>) => unknown,): Record<string, unknown> { return mapValues(obj, (val, key) => { if (Array.isArray(val)) { return val.map(_ => mapValuesDeep(_, f)) } if (isPlainObject(val)) { return mapValuesDeep(val as Record<string, unknown>, f) } return f(val, key, obj) })}
function mapAssistantMessage( message: AssistantMessage, f: (s: unknown) => unknown, index: number, uuid?: UUID,): AssistantMessage { return { // Use provided UUID if given (hydrate path uses randomUUID for globally unique IDs), // otherwise fall back to deterministic index-based UUID (dehydrate/fixture path). // sessionStorage.ts deduplicates messages by UUID, so without unique UUIDs across // VCR calls, resumed sessions would treat different responses as duplicates. uuid: uuid ?? (`UUID-${index}` as unknown as UUID), requestId: 'REQUEST_ID', timestamp: message.timestamp, message: { ...message.message, content: message.message.content .map(_ => { switch (_.type) { case 'text': return { ..._, text: f(_.text) as string, citations: _.citations || [], } // Ensure citations case 'tool_use': return { ..._, input: mapValuesDeep(_.input as Record<string, unknown>, f), } default: return _ // Handle other block types unchanged } }) .filter(Boolean) as BetaContentBlock[], }, type: 'assistant', }}
function mapMessage( message: AssistantMessage | SystemAPIErrorMessage | StreamEvent, f: (s: unknown) => unknown, index: number, uuid?: UUID,): AssistantMessage | SystemAPIErrorMessage | StreamEvent { if (message.type === 'assistant') { return mapAssistantMessage(message, f, index, uuid) } else { return message }}
function dehydrateValue(s: unknown): unknown { if (typeof s !== 'string') { return s } const cwd = getCwd() const configHome = getClaudeConfigHomeDir() let s1 = s .replace(/num_files="\d+"/g, 'num_files="[NUM]"') .replace(/duration_ms="\d+"/g, 'duration_ms="[DURATION]"') .replace(/cost_usd="\d+"/g, 'cost_usd="[COST]"') // Note: We intentionally don't replace all forward slashes with path.sep here. // That would corrupt XML-like tags (e.g., </system-reminder> -> <\system-reminder>). // The [CONFIG_HOME] and [CWD] replacements below handle path normalization. .replaceAll(configHome, '[CONFIG_HOME]') .replaceAll(cwd, '[CWD]') .replace(/Available commands:.+/, 'Available commands: [COMMANDS]') // On Windows, paths may appear in multiple forms: // 1. Forward-slash variants (Git, some Node APIs) // 2. JSON-escaped variants (backslashes doubled in serialized JSON within messages) if (process.platform === 'win32') { const cwdFwd = cwd.replaceAll('\\', '/') const configHomeFwd = configHome.replaceAll('\\', '/') // jsonStringify escapes \ to \\ - match paths embedded in JSON strings const cwdJsonEscaped = jsonStringify(cwd).slice(1, -1) const configHomeJsonEscaped = jsonStringify(configHome).slice(1, -1) s1 = s1 .replaceAll(cwdJsonEscaped, '[CWD]') .replaceAll(configHomeJsonEscaped, '[CONFIG_HOME]') .replaceAll(cwdFwd, '[CWD]') .replaceAll(configHomeFwd, '[CONFIG_HOME]') } // Normalize backslash path separators after placeholders so VCR fixture // hashes match across platforms (e.g., [CWD]\foo\bar -> [CWD]/foo/bar) // Handle both single backslashes and JSON-escaped double backslashes (\\) s1 = s1 .replace(/\[CWD\][^\s"'<>]*/g, match => match.replaceAll('\\\\', '/').replaceAll('\\', '/'), ) .replace(/\[CONFIG_HOME\][^\s"'<>]*/g, match => match.replaceAll('\\\\', '/').replaceAll('\\', '/'), ) if (s1.includes('Files modified by user:')) { return 'Files modified by user: [FILES]' } return s1}
function hydrateValue(s: unknown): unknown { if (typeof s !== 'string') { return s } return s .replaceAll('[NUM]', '1') .replaceAll('[DURATION]', '100') .replaceAll('[CONFIG_HOME]', getClaudeConfigHomeDir()) .replaceAll('[CWD]', getCwd())}
export async function* withStreamingVCR( messages: Message[], f: () => AsyncGenerator< StreamEvent | AssistantMessage | SystemAPIErrorMessage, void >,): AsyncGenerator< StreamEvent | AssistantMessage | SystemAPIErrorMessage, void> { if (!shouldUseVCR()) { return yield* f() }
// Compute and yield messages const buffer: (StreamEvent | AssistantMessage | SystemAPIErrorMessage)[] = []
// Record messages (or fetch from cache) const cachedBuffer = await withVCR(messages, async () => { for await (const message of f()) { buffer.push(message) } return buffer })
if (cachedBuffer.length > 0) { yield* cachedBuffer return }
yield* buffer}
export async function withTokenCountVCR( messages: unknown[], tools: unknown[], f: () => Promise<number | null>,): Promise<number | null> { // Dehydrate before hashing so fixture keys survive cwd/config-home/tempdir // variation and message UUID/timestamp churn. System prompts embed the // working directory (both raw and as a slash→dash project slug in the // auto-memory path) and messages carry fresh UUIDs per run; without this, // every test run produces a new hash and fixtures never hit in CI. const cwdSlug = getCwd().replace(/[^a-zA-Z0-9]/g, '-') const dehydrated = ( dehydrateValue(jsonStringify({ messages, tools })) as string ) .replaceAll(cwdSlug, '[CWD_SLUG]') .replace( /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '[UUID]', ) .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?/g, '[TIMESTAMP]') const result = await withFixture(dehydrated, 'token-count', async () => ({ tokenCount: await f(), })) return result.tokenCount}