export const FIXTURE_BEHAVIOR_VERSION = 1 as const; export const FIXTURE_BEHAVIOR_RESPONSE_VERSION = 2 as const; export const FIXTURE_BEHAVIOR_REPLAY_VERSION = 3 as const; export const MAX_FIXTURE_RESPONSE_DELAY_SAMPLES = 256; export const MAX_FIXTURE_RESPONSE_DELAY_MS = 8 * 60_000; export const MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH = 500; export type FixtureBehaviorV1 = { version: typeof FIXTURE_BEHAVIOR_VERSION; responseDelaySamplesMs: number[]; }; export type FixtureResponseSample = { delayMs: number; when?: { provider?: string; operation?: string; }; httpError?: { status: number; message: string; }; }; export type FixtureBehaviorV2 = { version: typeof FIXTURE_BEHAVIOR_RESPONSE_VERSION; responseSamples: FixtureResponseSample[]; }; export type FixtureReplayBundle = { bundleId: string; manifestSha256: string; syntheticFallbackToolIds?: string[]; }; export type FixtureBehaviorV3 = { version: typeof FIXTURE_BEHAVIOR_REPLAY_VERSION; responseSamples: FixtureResponseSample[]; replayBundle: FixtureReplayBundle; }; export type FixtureBehavior = | FixtureBehaviorV1 | FixtureBehaviorV2 | FixtureBehaviorV3; /** Providers whose fixture-mode path intentionally remains a real local/test adapter. */ const FIXTURE_MODE_BYPASS_PROVIDERS = new Set([ 'neon', 'local', 'test', 'test_high', 'test_low', 'test_unhinted', ]); type FixtureBehaviorValidationResult = | { ok: true; behavior: FixtureBehavior | null } | { ok: false; error: string }; export function validateFixtureBehavior( value: unknown, ): FixtureBehaviorValidationResult { if (value === undefined || value === null) { return { ok: true, behavior: null }; } if (!value || typeof value !== 'object' || Array.isArray(value)) { return { ok: false, error: 'fixtureBehavior must be a JSON object.' }; } const record = value as Record; const supportedKeys = record.version === FIXTURE_BEHAVIOR_RESPONSE_VERSION || record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION ? new Set([ 'version', 'responseSamples', ...(record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION ? ['replayBundle'] : []), ]) : new Set(['version', 'responseDelaySamplesMs']); const unknownKeys = Object.keys(record).filter( (key) => !supportedKeys.has(key), ); if (unknownKeys.length > 0) { return { ok: false, error: `Unsupported fixtureBehavior field "${unknownKeys[0]}".`, }; } if ( record.version !== FIXTURE_BEHAVIOR_VERSION && record.version !== FIXTURE_BEHAVIOR_RESPONSE_VERSION && record.version !== FIXTURE_BEHAVIOR_REPLAY_VERSION ) { return { ok: false, error: `fixtureBehavior.version must be ${FIXTURE_BEHAVIOR_VERSION} or ` + `${FIXTURE_BEHAVIOR_RESPONSE_VERSION}, or ` + `${FIXTURE_BEHAVIOR_REPLAY_VERSION}.`, }; } if ( record.version === FIXTURE_BEHAVIOR_RESPONSE_VERSION || record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION ) { if ( !Array.isArray(record.responseSamples) || record.responseSamples.length === 0 || record.responseSamples.length > MAX_FIXTURE_RESPONSE_DELAY_SAMPLES ) { return { ok: false, error: 'fixtureBehavior.responseSamples must contain between 1 and ' + `${MAX_FIXTURE_RESPONSE_DELAY_SAMPLES} values.`, }; } const samples: FixtureResponseSample[] = []; for (let index = 0; index < record.responseSamples.length; index += 1) { const value = record.responseSamples[index]; if (!value || typeof value !== 'object' || Array.isArray(value)) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}] must be an object.`, }; } const sample = value as Record; const sampleUnknownKeys = Object.keys(sample).filter( (key) => key !== 'delayMs' && key !== 'when' && key !== 'httpError', ); if (sampleUnknownKeys.length > 0) { return { ok: false, error: `Unsupported fixtureBehavior.responseSamples[${index}] field ` + `"${sampleUnknownKeys[0]}".`, }; } if ( typeof sample.delayMs !== 'number' || !Number.isSafeInteger(sample.delayMs) || sample.delayMs < 0 || sample.delayMs > MAX_FIXTURE_RESPONSE_DELAY_MS ) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}].delayMs must be an ` + `integer between 0 and ${MAX_FIXTURE_RESPONSE_DELAY_MS}.`, }; } let when: FixtureResponseSample['when']; if (sample.when !== undefined) { if ( !sample.when || typeof sample.when !== 'object' || Array.isArray(sample.when) ) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}].when must be an ` + 'object.', }; } const condition = sample.when as Record; const conditionUnknownKeys = Object.keys(condition).filter( (key) => key !== 'provider' && key !== 'operation', ); if (conditionUnknownKeys.length > 0) { return { ok: false, error: `Unsupported fixtureBehavior.responseSamples[${index}].when ` + `field "${conditionUnknownKeys[0]}".`, }; } const provider = normalizeFixtureScopeValue(condition.provider, 100); const operation = normalizeFixtureScopeValue(condition.operation, 200); if (provider.error || operation.error) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}].when ` + (provider.error ?? operation.error), }; } if (!provider.value && !operation.value) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}].when must select a ` + 'provider or operation.', }; } when = { ...(provider.value ? { provider: provider.value } : {}), ...(operation.value ? { operation: operation.value } : {}), }; } let httpError: FixtureResponseSample['httpError']; if (sample.httpError !== undefined) { if ( !sample.httpError || typeof sample.httpError !== 'object' || Array.isArray(sample.httpError) ) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}].httpError must be ` + 'an object.', }; } const error = sample.httpError as Record; const errorUnknownKeys = Object.keys(error).filter( (key) => key !== 'status' && key !== 'message', ); if (errorUnknownKeys.length > 0) { return { ok: false, error: `Unsupported fixtureBehavior.responseSamples[${index}]` + `.httpError field "${errorUnknownKeys[0]}".`, }; } if ( typeof error.status !== 'number' || !Number.isSafeInteger(error.status) || error.status < 400 || error.status > 599 ) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}].httpError.status ` + 'must be an integer between 400 and 599.', }; } if ( typeof error.message !== 'string' || !error.message.trim() || error.message.length > MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH ) { return { ok: false, error: `fixtureBehavior.responseSamples[${index}].httpError.message ` + `must contain 1-${MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH} characters.`, }; } httpError = { status: error.status, message: error.message.trim() }; } samples.push({ delayMs: sample.delayMs, ...(when ? { when } : {}), ...(httpError ? { httpError } : {}), }); } let replayBundle: FixtureReplayBundle | undefined; if (record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION) { if ( !record.replayBundle || typeof record.replayBundle !== 'object' || Array.isArray(record.replayBundle) ) { return { ok: false, error: 'fixtureBehavior.replayBundle must be an object.', }; } const replay = record.replayBundle as Record; const replayUnknownKeys = Object.keys(replay).filter( (key) => key !== 'bundleId' && key !== 'manifestSha256' && key !== 'syntheticFallbackToolIds', ); if (replayUnknownKeys.length > 0) { return { ok: false, error: 'Unsupported fixtureBehavior.replayBundle field ' + `"${replayUnknownKeys[0]}".`, }; } const digestPattern = /^[a-f0-9]{64}$/; if ( typeof replay.bundleId !== 'string' || !digestPattern.test(replay.bundleId) ) { return { ok: false, error: 'fixtureBehavior.replayBundle.bundleId must be a lowercase SHA-256 digest.', }; } if ( typeof replay.manifestSha256 !== 'string' || !digestPattern.test(replay.manifestSha256) ) { return { ok: false, error: 'fixtureBehavior.replayBundle.manifestSha256 must be a lowercase SHA-256 digest.', }; } let syntheticFallbackToolIds: string[] | undefined; if (replay.syntheticFallbackToolIds !== undefined) { if ( !Array.isArray(replay.syntheticFallbackToolIds) || replay.syntheticFallbackToolIds.length === 0 || replay.syntheticFallbackToolIds.length > 32 ) { return { ok: false, error: 'fixtureBehavior.replayBundle.syntheticFallbackToolIds must contain 1-32 tool ids.', }; } syntheticFallbackToolIds = []; for (const rawToolId of replay.syntheticFallbackToolIds) { if ( typeof rawToolId !== 'string' || rawToolId !== rawToolId.trim().toLowerCase() || !/^[a-z0-9][a-z0-9_.-]*$/.test(rawToolId) || rawToolId.length > 250 || syntheticFallbackToolIds.includes(rawToolId) ) { return { ok: false, error: 'fixtureBehavior.replayBundle.syntheticFallbackToolIds contains an invalid tool id.', }; } syntheticFallbackToolIds.push(rawToolId); } } replayBundle = { bundleId: replay.bundleId, manifestSha256: replay.manifestSha256, ...(syntheticFallbackToolIds ? { syntheticFallbackToolIds } : {}), }; } if (record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION) { return { ok: true, behavior: { version: FIXTURE_BEHAVIOR_REPLAY_VERSION, responseSamples: samples, replayBundle: replayBundle!, }, }; } return { ok: true, behavior: { version: FIXTURE_BEHAVIOR_RESPONSE_VERSION, responseSamples: samples, }, }; } if ( !Array.isArray(record.responseDelaySamplesMs) || record.responseDelaySamplesMs.length === 0 || record.responseDelaySamplesMs.length > MAX_FIXTURE_RESPONSE_DELAY_SAMPLES ) { return { ok: false, error: 'fixtureBehavior.responseDelaySamplesMs must contain between 1 and ' + `${MAX_FIXTURE_RESPONSE_DELAY_SAMPLES} values.`, }; } const samples: number[] = []; for ( let index = 0; index < record.responseDelaySamplesMs.length; index += 1 ) { const value = record.responseDelaySamplesMs[index]; if ( typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0 || value > MAX_FIXTURE_RESPONSE_DELAY_MS ) { return { ok: false, error: `fixtureBehavior.responseDelaySamplesMs[${index}] must be an integer ` + `between 0 and ${MAX_FIXTURE_RESPONSE_DELAY_MS}.`, }; } samples.push(value); } return { ok: true, behavior: { version: FIXTURE_BEHAVIOR_VERSION, responseDelaySamplesMs: samples, }, }; } function normalizeFixtureScopeValue( value: unknown, maxLength: number, ): { value?: string; error?: string } { if (value === undefined) return {}; if (typeof value !== 'string' || !value.trim()) { return { error: 'values must be non-empty strings.' }; } const normalized = value.trim().toLowerCase(); if (normalized.length > maxLength) { return { error: `values must be at most ${maxLength} characters.` }; } return { value: normalized }; } export function requireFixtureBehavior(value: unknown): FixtureBehavior { const result = validateFixtureBehavior(value); if (result.ok === false) throw new Error(result.error); if (!result.behavior) { throw new Error('fixtureBehavior is required.'); } return result.behavior; } export function shouldRouteFixtureProvider(provider: string): boolean { const normalized = provider.trim().toLowerCase(); return Boolean(normalized) && !FIXTURE_MODE_BYPASS_PROVIDERS.has(normalized); } export function shouldRouteFixtureToolId(toolId: string): boolean { const normalized = toolId.trim().toLowerCase(); if (!normalized) return false; return ![...FIXTURE_MODE_BYPASS_PROVIDERS].some( (provider) => normalized === provider || normalized.startsWith(`${provider}_`), ); } function stableFixtureRequestHash(value: string): number { let hash = 0x811c9dc5; for (let index = 0; index < value.length; index += 1) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 0x01000193) >>> 0; } return hash >>> 0; } export function selectFixtureReplaySampleIndex(input: { stableRequestKey: string; sampleCount: number; }): number { const stableRequestKey = input.stableRequestKey.trim(); if (!stableRequestKey) { throw new Error('Fixture replay requires a stable request key.'); } if (!Number.isSafeInteger(input.sampleCount) || input.sampleCount <= 0) { throw new Error('Fixture replay requires a positive sample count.'); } return ( stableFixtureRequestHash(`fixture-replay-v1:${stableRequestKey}`) % input.sampleCount ); } export function selectFixtureResponseDelay(input: { behavior: FixtureBehavior; stableRequestKey: string; }): { delayMs: number; sampleIndex: number } { const selected = selectFixtureResponseSample(input); return { delayMs: selected.delayMs, sampleIndex: selected.sampleIndex }; } export function selectFixtureResponseSample(input: { behavior: FixtureBehavior; stableRequestKey: string; }): FixtureResponseSample & { sampleIndex: number } { const stableRequestKey = input.stableRequestKey.trim(); if (!stableRequestKey) { throw new Error( 'Configured fixture response timing requires a stable request key.', ); } const samples = input.behavior.version === FIXTURE_BEHAVIOR_VERSION ? input.behavior.responseDelaySamplesMs.map((delayMs, sampleIndex) => ({ delayMs, sampleIndex, })) : selectEligibleResponseSamples( input.behavior.responseSamples, stableRequestKey, ); const eligibleIndex = stableFixtureRequestHash( `fixture-behavior-v${input.behavior.version}:${stableRequestKey}`, ) % samples.length; return { ...samples[eligibleIndex]! }; } function selectEligibleResponseSamples( samples: FixtureResponseSample[], stableRequestKey: string, ): Array { const [provider = '', operation = ''] = stableRequestKey .toLowerCase() .split(':', 3); const indexed = samples.map((sample, sampleIndex) => ({ ...sample, sampleIndex, })); const scoped = indexed.filter( (sample) => sample.when && (!sample.when.provider || sample.when.provider === provider) && (!sample.when.operation || sample.when.operation === operation), ); if (scoped.length > 0) return scoped; const fallback = indexed.filter((sample) => !sample.when); if (fallback.length > 0) return fallback; throw new Error( `Configured fixture behavior has no response samples for ${provider}:${operation}.`, ); } export async function waitForFixtureResponseDelay(input: { behavior: FixtureBehavior; stableRequestKey: string; signal?: AbortSignal | null; onSelected?: (selected: { delayMs: number; sampleIndex: number }) => void; }): Promise<{ delayMs: number; sampleIndex: number }> { const selected = selectFixtureResponseDelay(input); input.onSelected?.(selected); if (selected.delayMs === 0) return selected; await new Promise((resolve, reject) => { if (input.signal?.aborted) { reject(input.signal.reason ?? new DOMException('Aborted', 'AbortError')); return; } const cleanup = () => { input.signal?.removeEventListener('abort', onAbort); }; const timer = setTimeout(() => { cleanup(); resolve(); }, selected.delayMs); const onAbort = () => { clearTimeout(timer); cleanup(); reject(input.signal?.reason ?? new DOMException('Aborted', 'AbortError')); }; input.signal?.addEventListener('abort', onAbort, { once: true }); }); return selected; }