httpapi-experimental.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import type { UpgradeWebSocket } from "hono/ws"
  3. import { Flag } from "@opencode-ai/core/flag/flag"
  4. import { GlobalBus } from "@/bus/global"
  5. import { Instance } from "../../src/project/instance"
  6. import { InstanceRoutes } from "../../src/server/routes/instance"
  7. import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental"
  8. import { Log } from "../../src/util"
  9. import { Worktree } from "../../src/worktree"
  10. import { resetDatabase } from "../fixture/db"
  11. import { tmpdir } from "../fixture/fixture"
  12. void Log.init({ print: false })
  13. const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
  14. const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
  15. function app() {
  16. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
  17. return InstanceRoutes(websocket)
  18. }
  19. async function waitReady(directory: string) {
  20. return await new Promise<void>((resolve, reject) => {
  21. const timer = setTimeout(() => {
  22. GlobalBus.off("event", onEvent)
  23. reject(new Error("timed out waiting for worktree.ready"))
  24. }, 10_000)
  25. function onEvent(event: { directory?: string; payload: { type?: string } }) {
  26. if (event.payload.type !== Worktree.Event.Ready.type || event.directory !== directory) return
  27. clearTimeout(timer)
  28. GlobalBus.off("event", onEvent)
  29. resolve()
  30. }
  31. GlobalBus.on("event", onEvent)
  32. })
  33. }
  34. afterEach(async () => {
  35. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
  36. await Instance.disposeAll()
  37. await resetDatabase()
  38. })
  39. describe("experimental HttpApi", () => {
  40. test("serves read-only experimental endpoints through Hono bridge", async () => {
  41. await using tmp = await tmpdir({
  42. config: {
  43. formatter: false,
  44. lsp: false,
  45. mcp: {
  46. demo: {
  47. type: "local",
  48. command: ["echo", "demo"],
  49. enabled: false,
  50. },
  51. },
  52. },
  53. })
  54. const headers = { "x-opencode-directory": tmp.path }
  55. const [consoleState, consoleOrgs, toolIDs, worktrees, resources] = await Promise.all([
  56. app().request(ExperimentalPaths.console, { headers }),
  57. app().request(ExperimentalPaths.consoleOrgs, { headers }),
  58. app().request(ExperimentalPaths.toolIDs, { headers }),
  59. app().request(ExperimentalPaths.worktree, { headers }),
  60. app().request(ExperimentalPaths.resource, { headers }),
  61. ])
  62. expect(consoleState.status).toBe(200)
  63. expect(await consoleState.json()).toEqual({
  64. consoleManagedProviders: [],
  65. switchableOrgCount: 0,
  66. })
  67. expect(consoleOrgs.status).toBe(200)
  68. expect(await consoleOrgs.json()).toEqual({ orgs: [] })
  69. expect(toolIDs.status).toBe(200)
  70. expect(await toolIDs.json()).toContain("bash")
  71. expect(worktrees.status).toBe(200)
  72. expect(await worktrees.json()).toEqual([])
  73. expect(resources.status).toBe(200)
  74. expect(await resources.json()).toEqual({})
  75. })
  76. test("serves worktree mutations through Hono bridge", async () => {
  77. await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
  78. const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
  79. const created = await app().request(ExperimentalPaths.worktree, {
  80. method: "POST",
  81. headers,
  82. body: JSON.stringify({ name: "api-test" }),
  83. })
  84. expect(created.status).toBe(200)
  85. const info = (await created.json()) as Worktree.Info
  86. expect(info).toMatchObject({ name: "api-test", branch: "opencode/api-test" })
  87. await waitReady(info.directory)
  88. const listed = await app().request(ExperimentalPaths.worktree, { headers })
  89. expect(listed.status).toBe(200)
  90. expect(await listed.json()).toContain(info.directory)
  91. const reset = await app().request(ExperimentalPaths.worktreeReset, {
  92. method: "POST",
  93. headers,
  94. body: JSON.stringify({ directory: info.directory }),
  95. })
  96. expect(reset.status).toBe(200)
  97. expect(await reset.json()).toBe(true)
  98. const removed = await app().request(ExperimentalPaths.worktree, {
  99. method: "DELETE",
  100. headers,
  101. body: JSON.stringify({ directory: info.directory }),
  102. })
  103. expect(removed.status).toBe(200)
  104. expect(await removed.json()).toBe(true)
  105. const afterRemove = await app().request(ExperimentalPaths.worktree, { headers })
  106. expect(afterRemove.status).toBe(200)
  107. expect(await afterRemove.json()).toEqual([])
  108. })
  109. })