recorded-test.ts 3.3 KB

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