recorded-test.ts 3.8 KB

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