import { HttpRecorder } from "@opencode/http-recorder" import { NodeSocket } from "@effect/platform-node" import { Layer } from "effect" import { Socket } from "effect/unstable/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 { EvaluationClient } from "../src/experimental/evaluation-client.js" import type { Service as EvaluationClientService } from "../src/experimental/evaluation-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 | EvaluationClientService | 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) { if (process.env.CI !== undefined) throw new Error("Unset CI before recording cassettes") HttpRecorder.removeCassetteSync(cassette, { directory: FIXTURES_DIR }) } 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(NodeSocket.layerWebSocketConstructorWS)) return 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)), EvaluationClient.layer.pipe(Layer.provide(requestExecutor)), webSocket, ) }, })