import { HttpRecorder } from "@opencode/http-recorder" import { NodeWS } from "@effect/platform-node" import { Effect, Layer } from "effect" import { Socket } from "effect/socket" import * as path from "node:path" import { fileURLToPath } from "node:url" import { LLMClient, RequestExecutor } from "../src/route.js" import { ImageClient } from "../src/image-client.js" import { VideoClient } from "../src/video-client.js" import { SpeechClient } from "../src/speech-client.js" import { TranscriptionClient } from "../src/transcription-client.js" import { DecisionClient } from "../src/decision-client.js" import type { Service as DecisionClientService } from "../src/decision-client.js" import { EmbeddingClient } from "../src/embedding-client.js" import type { Service as EmbeddingClientService } from "../src/embedding-client.js" import type { Service as ImageClientService } from "../src/image-client.js" import type { Service as VideoClientService } from "../src/video-client.js" import type { Service as SpeechClientService } from "../src/speech-client.js" import type { Service as TranscriptionClientService } from "../src/transcription-client.js" import type { Service as LLMClientService } from "../src/route/client.js" import type { Service as RequestExecutorService } from "../src/route/executor.js" import { recordedEffectGroup, type RecordedCaseOptions as RunnerCaseOptions, type RecordedGroupOptions, } from "./recorded-runner.js" const __dirname = path.dirname(fileURLToPath(import.meta.url)) const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings") type RecordedEnv = | RequestExecutorService | LLMClientService | ImageClientService | VideoClientService | SpeechClientService | TranscriptionClientService | DecisionClientService | EmbeddingClientService | Socket.WebSocketConstructor type RecordedTestsOptions = RecordedGroupOptions & { readonly options?: HttpRecorder.RecorderOptions } type RecordedCaseOptions = RunnerCaseOptions & { readonly options?: HttpRecorder.RecorderOptions } const mergeOptions = ( base: HttpRecorder.RecorderOptions | undefined, override: HttpRecorder.RecorderOptions | undefined, ) => { if (!base) return override if (!override) return base return { ...base, ...override, metadata: base.metadata || override.metadata ? { ...base.metadata, ...override.metadata } : undefined, redact: base.redact || override.redact ? { ...base.redact, ...override.redact, headers: [...(base.redact?.headers ?? []), ...(override.redact?.headers ?? [])], allowRequestHeaders: [ ...(base.redact?.allowRequestHeaders ?? []), ...(override.redact?.allowRequestHeaders ?? []), ], allowResponseHeaders: [ ...(base.redact?.allowResponseHeaders ?? []), ...(override.redact?.allowResponseHeaders ?? []), ], queryParameters: [...(base.redact?.queryParameters ?? []), ...(override.redact?.queryParameters ?? [])], jsonFields: [...(base.redact?.jsonFields ?? []), ...(override.redact?.jsonFields ?? [])], } : undefined, } } export const recordedTests = (options: RecordedTestsOptions) => recordedEffectGroup({ duplicateLabel: "recorded cassette", options, cassetteExists: (cassette) => HttpRecorder.hasCassetteSync(cassette, { directory: FIXTURES_DIR }), layer: ({ cassette, metadata, options, caseOptions, recording }) => { const recorderOptions = mergeOptions(options.options, caseOptions.options) const recorderMetadata = { ...recorderOptions?.metadata, ...metadata, } if (recording && process.env.CI !== undefined) throw new Error("Unset CI before recording cassettes") const requestExecutor = RequestExecutor.layer.pipe( Layer.provide( HttpRecorder.layerFetch(cassette, { ...recorderOptions, directory: FIXTURES_DIR, metadata: recorderMetadata, }), ), ) const webSocket = HttpRecorder.layerWebSocketConstructor(cassette, { ...recorderOptions, directory: FIXTURES_DIR, metadata: recorderMetadata, }).pipe(Layer.provide(NodeWS.layerWebSocketConstructor)) const layer = Layer.mergeAll( requestExecutor, LLMClient.layer.pipe(Layer.provide(requestExecutor)), ImageClient.layer.pipe(Layer.provide(requestExecutor)), VideoClient.layer.pipe(Layer.provide(requestExecutor)), SpeechClient.layer.pipe(Layer.provide(requestExecutor)), TranscriptionClient.layer.pipe(Layer.provide(requestExecutor)), DecisionClient.layer.pipe(Layer.provide(requestExecutor)), EmbeddingClient.layer.pipe(Layer.provide(requestExecutor)), webSocket, ) if (!recording) return layer // Remove the cassette when the test runs, not when it is registered, so tests filtered out by `-t` keep theirs. return Layer.unwrap( Effect.sync(() => { HttpRecorder.removeCassetteSync(cassette, { directory: FIXTURES_DIR }) return layer }), ) }, })