httpapi-sync.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import { Effect } from "effect"
  3. import { Flag } from "@opencode-ai/core/flag/flag"
  4. import { Instance } from "../../src/project/instance"
  5. import { Server } from "../../src/server/server"
  6. import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
  7. import { Session } from "@/session/session"
  8. import * as Log from "@opencode-ai/core/util/log"
  9. import { resetDatabase } from "../fixture/db"
  10. import { tmpdir } from "../fixture/fixture"
  11. void Log.init({ print: false })
  12. const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
  13. const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
  14. function app(httpapi = true) {
  15. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = httpapi
  16. return httpapi ? Server.Default().app : Server.Legacy().app
  17. }
  18. function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
  19. return Effect.runPromise(fx.pipe(Effect.provide(Session.defaultLayer)))
  20. }
  21. afterEach(async () => {
  22. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
  23. Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
  24. await Instance.disposeAll()
  25. await resetDatabase()
  26. })
  27. describe("sync HttpApi", () => {
  28. test("serves sync routes through Hono bridge", async () => {
  29. Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
  30. await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
  31. const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
  32. const session = await Instance.provide({
  33. directory: tmp.path,
  34. fn: async () => runSession(Session.Service.use((svc) => svc.create({ title: "sync" }))),
  35. })
  36. const started = await app().request(SyncPaths.start, { method: "POST", headers })
  37. expect(started.status).toBe(200)
  38. expect(await started.json()).toBe(true)
  39. const history = await app().request(SyncPaths.history, {
  40. method: "POST",
  41. headers,
  42. body: JSON.stringify({}),
  43. })
  44. expect(history.status).toBe(200)
  45. const rows = (await history.json()) as Array<{
  46. id: string
  47. aggregate_id: string
  48. seq: number
  49. type: string
  50. data: Record<string, unknown>
  51. }>
  52. expect(rows.map((row) => row.aggregate_id)).toContain(session.id)
  53. const replayed = await app().request(SyncPaths.replay, {
  54. method: "POST",
  55. headers,
  56. body: JSON.stringify({
  57. directory: tmp.path,
  58. events: rows
  59. .filter((row) => row.aggregate_id === session.id)
  60. .map((row) => ({
  61. id: row.id,
  62. aggregateID: row.aggregate_id,
  63. seq: row.seq,
  64. type: row.type,
  65. data: row.data,
  66. })),
  67. }),
  68. })
  69. expect(replayed.status).toBe(200)
  70. expect(await replayed.json()).toEqual({ sessionID: session.id })
  71. })
  72. test("matches legacy seq validation", async () => {
  73. await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
  74. const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
  75. const cases = [
  76. {
  77. path: SyncPaths.history,
  78. body: { aggregate: -1 },
  79. },
  80. {
  81. path: SyncPaths.history,
  82. body: { aggregate: 1.5 },
  83. },
  84. {
  85. path: SyncPaths.replay,
  86. body: {
  87. directory: tmp.path,
  88. events: [{ id: "event", aggregateID: "session", seq: -1, type: "session.created", data: {} }],
  89. },
  90. },
  91. {
  92. path: SyncPaths.replay,
  93. body: {
  94. directory: tmp.path,
  95. events: [{ id: "event", aggregateID: "session", seq: 1.5, type: "session.created", data: {} }],
  96. },
  97. },
  98. ]
  99. for (const item of cases) {
  100. const legacy = await app(false).request(item.path, {
  101. method: "POST",
  102. headers,
  103. body: JSON.stringify(item.body),
  104. })
  105. const httpapi = await app(true).request(item.path, {
  106. method: "POST",
  107. headers,
  108. body: JSON.stringify(item.body),
  109. })
  110. expect(httpapi.status).toBe(legacy.status)
  111. expect(httpapi.status).toBe(400)
  112. }
  113. })
  114. })