Ctrl K
ComunidadGuías…

packages/ai/test/recorded-test.ts

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<RecordedEnv, never, RecordedTestsOptions, RecordedCaseOptions>({
    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
        }),
      )
    },
  })