recorded-runner.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import type { HttpRecorder } from "@opencode-ai/http-recorder"
  2. import { test, type TestOptions } from "bun:test"
  3. import { Effect, type Layer } from "effect"
  4. import { testEffect } from "./lib/effect"
  5. import { cassetteName, classifiedTags, matchesSelected, missingEnv, unique } from "./recorded-utils"
  6. export type RecordedBody<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
  7. export type RecordedGroupOptions = {
  8. readonly prefix: string
  9. readonly provider?: string
  10. readonly protocol?: string
  11. readonly requires?: ReadonlyArray<string>
  12. readonly tags?: ReadonlyArray<string>
  13. readonly metadata?: HttpRecorder.CassetteMetadata
  14. }
  15. export type RecordedCaseOptions = {
  16. readonly cassette?: string
  17. readonly id?: string
  18. readonly provider?: string
  19. readonly protocol?: string
  20. readonly requires?: ReadonlyArray<string>
  21. readonly tags?: ReadonlyArray<string>
  22. readonly metadata?: HttpRecorder.CassetteMetadata
  23. }
  24. export const recordedEffectGroup = <
  25. R,
  26. E,
  27. Options extends RecordedGroupOptions,
  28. CaseOptions extends RecordedCaseOptions,
  29. >(input: {
  30. readonly duplicateLabel: string
  31. readonly options: Options
  32. readonly cassetteExists: (cassette: string) => boolean
  33. readonly layer: (input: {
  34. readonly cassette: string
  35. readonly tags: ReadonlyArray<string>
  36. readonly metadata: HttpRecorder.CassetteMetadata
  37. readonly recording: boolean
  38. readonly options: Options
  39. readonly caseOptions: CaseOptions
  40. }) => Layer.Layer<R, E>
  41. }) => {
  42. const cassettes = new Set<string>()
  43. const run = <A, E2>(
  44. name: string,
  45. caseOptions: CaseOptions,
  46. body: RecordedBody<A, E2, R>,
  47. testOptions?: number | TestOptions,
  48. ) => {
  49. const cassette = cassetteName(input.options.prefix, name, caseOptions)
  50. if (cassettes.has(cassette)) throw new Error(`Duplicate ${input.duplicateLabel} "${cassette}"`)
  51. cassettes.add(cassette)
  52. const tags = unique([
  53. ...classifiedTags(input.options),
  54. ...classifiedTags({
  55. provider: caseOptions.provider,
  56. protocol: caseOptions.protocol,
  57. tags: caseOptions.tags,
  58. }),
  59. ])
  60. if (!matchesSelected({ prefix: input.options.prefix, name, cassette, tags }))
  61. return test.skip(name, () => {}, testOptions)
  62. const recording = process.env.RECORD === "true"
  63. if (recording) {
  64. if (missingEnv([...(input.options.requires ?? []), ...(caseOptions.requires ?? [])]).length > 0) {
  65. return test.skip(name, () => {}, testOptions)
  66. }
  67. } else if (!input.cassetteExists(cassette)) {
  68. return test.skip(name, () => {}, testOptions)
  69. }
  70. return testEffect(
  71. input.layer({
  72. cassette,
  73. tags,
  74. metadata: { ...input.options.metadata, ...caseOptions.metadata, tags },
  75. recording,
  76. options: input.options,
  77. caseOptions,
  78. }),
  79. ).live(name, body, testOptions)
  80. }
  81. const effect = <A, E2>(name: string, body: RecordedBody<A, E2, R>, testOptions?: number | TestOptions) =>
  82. run(name, {} as CaseOptions, body, testOptions)
  83. effect.with = <A, E2>(
  84. name: string,
  85. caseOptions: CaseOptions,
  86. body: RecordedBody<A, E2, R>,
  87. testOptions?: number | TestOptions,
  88. ) => run(name, caseOptions, body, testOptions)
  89. return { effect }
  90. }