recorded-test.ts 3.4 KB

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