recorded-test.ts 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. import { NodeFileSystem } from "@effect/platform-node"
  2. import { HttpRecorder } from "@opencode-ai/http-recorder"
  3. import { Layer } from "effect"
  4. import { FetchHttpClient } from "effect/unstable/http"
  5. import * as path from "node:path"
  6. import { fileURLToPath } from "node:url"
  7. import { LLMClient, RequestExecutor } from "../src/route"
  8. import type { Service as LLMClientService } from "../src/route/client"
  9. import type { Service as RequestExecutorService } from "../src/route/executor"
  10. import type { Service as WebSocketExecutorService } from "../src/route/transport/websocket"
  11. import {
  12. recordedEffectGroup,
  13. type RecordedCaseOptions as RunnerCaseOptions,
  14. type RecordedGroupOptions,
  15. } from "./recorded-runner"
  16. import { webSocketCassetteLayer } from "./recorded-websocket"
  17. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  18. const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings")
  19. type RecordedEnv = RequestExecutorService | WebSocketExecutorService | LLMClientService
  20. type RecordedTestsOptions = RecordedGroupOptions & {
  21. readonly options?: HttpRecorder.RecordReplayOptions
  22. }
  23. type RecordedCaseOptions = RunnerCaseOptions & {
  24. readonly options?: HttpRecorder.RecordReplayOptions
  25. }
  26. const mergeOptions = (
  27. base: HttpRecorder.RecordReplayOptions | undefined,
  28. override: HttpRecorder.RecordReplayOptions | undefined,
  29. ) => {
  30. if (!base) return override
  31. if (!override) return base
  32. return {
  33. ...base,
  34. ...override,
  35. metadata: base.metadata || override.metadata ? { ...base.metadata, ...override.metadata } : undefined,
  36. }
  37. }
  38. export const recordedTests = (options: RecordedTestsOptions) =>
  39. recordedEffectGroup<RecordedEnv, never, RecordedTestsOptions, RecordedCaseOptions>({
  40. duplicateLabel: "recorded cassette",
  41. options,
  42. cassetteExists: (cassette) => HttpRecorder.hasCassetteSync(cassette, { directory: FIXTURES_DIR }),
  43. layer: ({ cassette, metadata, options, caseOptions, recording }) => {
  44. const recorderOptions = mergeOptions(options.options, caseOptions.options)
  45. const recorderMetadata = {
  46. ...recorderOptions?.metadata,
  47. ...metadata,
  48. }
  49. const mode = recorderOptions?.mode ?? (recording ? "record" : "replay")
  50. const cassetteService = HttpRecorder.Cassette.fileSystem({ directory: FIXTURES_DIR }).pipe(
  51. Layer.provide(NodeFileSystem.layer),
  52. )
  53. const requestExecutor = RequestExecutor.layer.pipe(
  54. Layer.provide(
  55. HttpRecorder.recordingLayer(cassette, {
  56. ...recorderOptions,
  57. mode,
  58. metadata: recorderMetadata,
  59. }).pipe(Layer.provide(FetchHttpClient.layer)),
  60. ),
  61. )
  62. const deps = Layer.mergeAll(
  63. requestExecutor,
  64. webSocketCassetteLayer(cassette, { metadata: recorderMetadata, mode }),
  65. )
  66. return Layer.mergeAll(deps, LLMClient.layer.pipe(Layer.provide(deps))).pipe(Layer.provide(cassetteService))
  67. },
  68. })