httpapi-json-parity.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import type { UpgradeWebSocket } from "hono/ws"
  3. import { Effect } from "effect"
  4. import { Flag } from "@opencode-ai/core/flag/flag"
  5. import { ModelID, ProviderID } from "../../src/provider/schema"
  6. import { Instance } from "../../src/project/instance"
  7. import { InstanceRoutes } from "../../src/server/routes/instance"
  8. import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental"
  9. import { SessionPaths } from "../../src/server/routes/instance/httpapi/session"
  10. import { MessageID, PartID } from "../../src/session/schema"
  11. import { Session } from "@/session/session"
  12. import * as Log from "@opencode-ai/core/util/log"
  13. import { resetDatabase } from "../fixture/db"
  14. import { tmpdir } from "../fixture/fixture"
  15. void Log.init({ print: false })
  16. const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
  17. const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
  18. function app(experimental: boolean) {
  19. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
  20. return InstanceRoutes(websocket)
  21. }
  22. function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
  23. return Effect.runPromise(fx.pipe(Effect.provide(Session.defaultLayer)))
  24. }
  25. function pathFor(path: string, params: Record<string, string>) {
  26. return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), path)
  27. }
  28. async function seedSessions(directory: string) {
  29. return await Instance.provide({
  30. directory,
  31. fn: () =>
  32. runSession(
  33. Effect.gen(function* () {
  34. const svc = yield* Session.Service
  35. const parent = yield* svc.create({ title: "parent" })
  36. yield* svc.create({ title: "child", parentID: parent.id })
  37. const message = yield* svc.updateMessage({
  38. id: MessageID.ascending(),
  39. role: "user",
  40. sessionID: parent.id,
  41. agent: "build",
  42. model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
  43. time: { created: Date.now() },
  44. })
  45. yield* svc.updatePart({
  46. id: PartID.ascending(),
  47. sessionID: parent.id,
  48. messageID: message.id,
  49. type: "text",
  50. text: "hello",
  51. })
  52. return { parent, message }
  53. }),
  54. ),
  55. })
  56. }
  57. async function readJson(
  58. label: string,
  59. app: ReturnType<typeof InstanceRoutes>,
  60. directory: string,
  61. path: string,
  62. headers: HeadersInit,
  63. ) {
  64. const response = await Instance.provide({
  65. directory,
  66. fn: () => app.request(path, { headers }),
  67. })
  68. if (response.status !== 200) throw new Error(`${label} returned ${response.status}: ${await response.text()}`)
  69. return await response.json()
  70. }
  71. async function expectJsonParity(input: {
  72. label: string
  73. legacy: ReturnType<typeof InstanceRoutes>
  74. httpapi: ReturnType<typeof InstanceRoutes>
  75. directory: string
  76. path: string
  77. headers: HeadersInit
  78. }) {
  79. const legacy = await readJson(input.label, input.legacy, input.directory, input.path, input.headers)
  80. const httpapi = await readJson(input.label, input.httpapi, input.directory, input.path, input.headers)
  81. expect({ label: input.label, body: httpapi }).toEqual({ label: input.label, body: legacy })
  82. }
  83. afterEach(async () => {
  84. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
  85. await Instance.disposeAll()
  86. await resetDatabase()
  87. })
  88. describe("HttpApi JSON parity", () => {
  89. test("matches legacy JSON shape for session read endpoints", async () => {
  90. await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
  91. const headers = { "x-opencode-directory": tmp.path }
  92. const seeded = await seedSessions(tmp.path)
  93. const legacy = app(false)
  94. const httpapi = app(true)
  95. await [
  96. { label: "session.list roots", path: `${SessionPaths.list}?roots=true`, headers },
  97. { label: "session.list all", path: SessionPaths.list, headers },
  98. { label: "session.get", path: pathFor(SessionPaths.get, { sessionID: seeded.parent.id }), headers },
  99. { label: "session.children", path: pathFor(SessionPaths.children, { sessionID: seeded.parent.id }), headers },
  100. { label: "session.messages", path: pathFor(SessionPaths.messages, { sessionID: seeded.parent.id }), headers },
  101. {
  102. label: "session.message",
  103. path: pathFor(SessionPaths.message, { sessionID: seeded.parent.id, messageID: seeded.message.id }),
  104. headers,
  105. },
  106. {
  107. label: "experimental.session",
  108. path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10" })}`,
  109. headers,
  110. },
  111. ].reduce(
  112. (promise, input) => promise.then(() => expectJsonParity({ ...input, legacy, httpapi, directory: tmp.path })),
  113. Promise.resolve(),
  114. )
  115. })
  116. })