recorded-golden.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import type { HttpRecorder } from "@opencode-ai/http-recorder"
  2. import { describe } from "bun:test"
  3. import { Effect } from "effect"
  4. import type { Model } from "../src"
  5. import { goldenScenarioTags, runGoldenScenario, type GoldenScenarioID } from "./recorded-scenarios"
  6. import { recordedTests } from "./recorded-test"
  7. import { kebab } from "./recorded-utils"
  8. type Transport = "http" | "websocket"
  9. type ScenarioInput =
  10. | GoldenScenarioID
  11. | {
  12. readonly id: GoldenScenarioID
  13. readonly name?: string
  14. readonly cassette?: string
  15. readonly tags?: ReadonlyArray<string>
  16. readonly maxTokens?: number
  17. readonly temperature?: number | false
  18. readonly timeout?: number
  19. }
  20. type TargetInput = {
  21. readonly name: string
  22. readonly model: Model
  23. readonly protocol?: string
  24. readonly requires?: ReadonlyArray<string>
  25. readonly transport?: Transport
  26. readonly prefix?: string
  27. readonly tags?: ReadonlyArray<string>
  28. readonly metadata?: Record<string, unknown>
  29. readonly options?: HttpRecorder.RecordReplayOptions
  30. readonly scenarios: ReadonlyArray<ScenarioInput>
  31. }
  32. const scenarioInput = (input: ScenarioInput) => (typeof input === "string" ? { id: input } : input)
  33. const scenarioTitle = (id: GoldenScenarioID) => {
  34. if (id === "text") return "streams text"
  35. if (id === "tool-call") return "streams tool call"
  36. if (id === "reasoning") return "uses reasoning"
  37. if (id === "image") return "reads image text"
  38. return "drives a tool loop"
  39. }
  40. const defaultPrefix = (target: TargetInput) => {
  41. if (target.prefix) return target.prefix
  42. const transport = target.transport === "websocket" ? "-websocket" : ""
  43. return `${target.model.provider}-${target.protocol ?? target.model.route.id}${transport}`
  44. }
  45. const metadata = (target: TargetInput) => ({
  46. provider: target.model.provider,
  47. protocol: target.protocol,
  48. route: target.model.route.id,
  49. transport: target.transport ?? "http",
  50. model: target.model.id,
  51. ...target.metadata,
  52. })
  53. const tags = (target: TargetInput) => [
  54. ...(target.transport === "websocket" ? ["transport:websocket"] : []),
  55. ...(target.tags ?? []),
  56. ]
  57. const runTarget = (target: TargetInput) => {
  58. const recorded = recordedTests({
  59. prefix: defaultPrefix(target),
  60. provider: target.model.provider,
  61. protocol: target.protocol,
  62. requires: target.requires,
  63. tags: tags(target),
  64. metadata: metadata(target),
  65. options: target.options,
  66. })
  67. describe(`${target.name} recorded`, () => {
  68. target.scenarios.forEach((raw) => {
  69. const input = scenarioInput(raw)
  70. const name = input.name ?? scenarioTitle(input.id)
  71. recorded.effect.with(
  72. name,
  73. {
  74. cassette: input.cassette,
  75. id: `${kebab(target.name)}-${input.id}`,
  76. tags: [...goldenScenarioTags(input.id), ...(input.tags ?? [])],
  77. },
  78. () =>
  79. Effect.gen(function* () {
  80. yield* runGoldenScenario(input.id, {
  81. id: `recorded_${kebab(target.name).replaceAll("-", "_")}_${input.id.replaceAll("-", "_")}`,
  82. model: target.model,
  83. maxTokens: input.maxTokens,
  84. temperature: input.temperature,
  85. })
  86. }),
  87. input.timeout,
  88. )
  89. })
  90. })
  91. }
  92. export const describeRecordedGoldenScenarios = (targets: ReadonlyArray<TargetInput>) => {
  93. targets.forEach(runTarget)
  94. }