recorded-test.ts 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import { HttpRecorder } from "@opencode-ai/http-recorder"
  2. import { Layer } from "effect"
  3. import * as path from "node:path"
  4. import { fileURLToPath } from "node:url"
  5. import { LLMClient, RequestExecutor, WebSocketExecutor } from "../src/route"
  6. import { ImageClient } from "../src/image-client"
  7. import type { Service as ImageClientService } from "../src/image-client"
  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. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  17. const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings")
  18. type RecordedEnv = RequestExecutorService | WebSocketExecutorService | LLMClientService | ImageClientService
  19. type RecordedTestsOptions = RecordedGroupOptions & {
  20. readonly options?: HttpRecorder.RecorderOptions
  21. }
  22. type RecordedCaseOptions = RunnerCaseOptions & {
  23. readonly options?: HttpRecorder.RecorderOptions
  24. }
  25. const mergeOptions = (
  26. base: HttpRecorder.RecorderOptions | undefined,
  27. override: HttpRecorder.RecorderOptions | undefined,
  28. ) => {
  29. if (!base) return override
  30. if (!override) return base
  31. return {
  32. ...base,
  33. ...override,
  34. metadata: base.metadata || override.metadata ? { ...base.metadata, ...override.metadata } : undefined,
  35. redact:
  36. base.redact || override.redact
  37. ? {
  38. ...base.redact,
  39. ...override.redact,
  40. headers: [...(base.redact?.headers ?? []), ...(override.redact?.headers ?? [])],
  41. allowRequestHeaders: [
  42. ...(base.redact?.allowRequestHeaders ?? []),
  43. ...(override.redact?.allowRequestHeaders ?? []),
  44. ],
  45. allowResponseHeaders: [
  46. ...(base.redact?.allowResponseHeaders ?? []),
  47. ...(override.redact?.allowResponseHeaders ?? []),
  48. ],
  49. queryParameters: [...(base.redact?.queryParameters ?? []), ...(override.redact?.queryParameters ?? [])],
  50. jsonFields: [...(base.redact?.jsonFields ?? []), ...(override.redact?.jsonFields ?? [])],
  51. }
  52. : undefined,
  53. }
  54. }
  55. export const recordedTests = (options: RecordedTestsOptions) =>
  56. recordedEffectGroup<RecordedEnv, never, RecordedTestsOptions, RecordedCaseOptions>({
  57. duplicateLabel: "recorded cassette",
  58. options,
  59. cassetteExists: (cassette) => HttpRecorder.hasCassetteSync(cassette, { directory: FIXTURES_DIR }),
  60. layer: ({ cassette, metadata, options, caseOptions, recording }) => {
  61. const recorderOptions = mergeOptions(options.options, caseOptions.options)
  62. const recorderMetadata = {
  63. ...recorderOptions?.metadata,
  64. ...metadata,
  65. }
  66. if (recording) {
  67. if (process.env.CI !== undefined) throw new Error("Unset CI before recording HTTP cassettes")
  68. HttpRecorder.removeCassetteSync(cassette, { directory: FIXTURES_DIR })
  69. }
  70. const requestExecutor = RequestExecutor.layer.pipe(
  71. Layer.provide(
  72. HttpRecorder.layerFetch(cassette, {
  73. ...recorderOptions,
  74. directory: FIXTURES_DIR,
  75. metadata: recorderMetadata,
  76. }),
  77. ),
  78. )
  79. const deps = Layer.mergeAll(requestExecutor, WebSocketExecutor.layer)
  80. return Layer.mergeAll(
  81. deps,
  82. LLMClient.layer.pipe(Layer.provide(deps)),
  83. ImageClient.layer.pipe(Layer.provide(deps)),
  84. )
  85. },
  86. })