httpapi-instance.legacy.test.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import { Flag } from "@opencode-ai/core/flag/flag"
  3. import { GlobalBus } from "@/bus/global"
  4. import { Instance } from "../../src/project/instance"
  5. import { Server } from "../../src/server/server"
  6. import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
  7. import * as Log from "@opencode-ai/core/util/log"
  8. import { resetDatabase } from "../fixture/db"
  9. import { disposeAllInstances, tmpdir } from "../fixture/fixture"
  10. void Log.init({ print: false })
  11. const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
  12. function app() {
  13. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
  14. return Server.Default().app
  15. }
  16. async function waitDisposed(directory: string) {
  17. return await new Promise<void>((resolve, reject) => {
  18. const timer = setTimeout(() => {
  19. GlobalBus.off("event", onEvent)
  20. reject(new Error("timed out waiting for instance disposal"))
  21. }, 10_000)
  22. function onEvent(event: { directory?: string; payload: { type?: string } }) {
  23. if (event.payload.type !== "server.instance.disposed" || event.directory !== directory) return
  24. clearTimeout(timer)
  25. GlobalBus.off("event", onEvent)
  26. resolve()
  27. }
  28. GlobalBus.on("event", onEvent)
  29. })
  30. }
  31. afterEach(async () => {
  32. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
  33. await disposeAllInstances()
  34. await resetDatabase()
  35. })
  36. describe("instance HttpApi", () => {
  37. test("serves catalog read endpoints through Hono bridge", async () => {
  38. await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
  39. const [commands, agents, skills, lsp, formatter] = await Promise.all([
  40. app().request(InstancePaths.command, { headers: { "x-opencode-directory": tmp.path } }),
  41. app().request(InstancePaths.agent, { headers: { "x-opencode-directory": tmp.path } }),
  42. app().request(InstancePaths.skill, { headers: { "x-opencode-directory": tmp.path } }),
  43. app().request(InstancePaths.lsp, { headers: { "x-opencode-directory": tmp.path } }),
  44. app().request(InstancePaths.formatter, { headers: { "x-opencode-directory": tmp.path } }),
  45. ])
  46. expect(commands.status).toBe(200)
  47. expect(await commands.json()).toContainEqual(expect.objectContaining({ name: "init", source: "command" }))
  48. expect(agents.status).toBe(200)
  49. expect(await agents.json()).toContainEqual(expect.objectContaining({ name: "build", mode: "primary" }))
  50. expect(skills.status).toBe(200)
  51. expect(await skills.json()).toBeArray()
  52. expect(lsp.status).toBe(200)
  53. expect(await lsp.json()).toEqual([])
  54. expect(formatter.status).toBe(200)
  55. expect(await formatter.json()).toEqual([])
  56. })
  57. test("serves project git init through Hono bridge", async () => {
  58. await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
  59. const disposed = waitDisposed(tmp.path)
  60. const response = await app().request("/project/git/init", {
  61. method: "POST",
  62. headers: { "x-opencode-directory": tmp.path },
  63. })
  64. expect(response.status).toBe(200)
  65. expect(await response.json()).toMatchObject({ vcs: "git", worktree: tmp.path })
  66. await disposed
  67. const current = await app().request("/project/current", { headers: { "x-opencode-directory": tmp.path } })
  68. expect(current.status).toBe(200)
  69. expect(await current.json()).toMatchObject({ vcs: "git", worktree: tmp.path })
  70. })
  71. test("serves project update through Hono bridge", async () => {
  72. await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
  73. const current = await app().request("/project/current", { headers: { "x-opencode-directory": tmp.path } })
  74. expect(current.status).toBe(200)
  75. const project = (await current.json()) as { id: string }
  76. const response = await app().request(`/project/${project.id}`, {
  77. method: "PATCH",
  78. headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
  79. body: JSON.stringify({ name: "patched-project", commands: { start: "bun dev" } }),
  80. })
  81. expect(response.status).toBe(200)
  82. expect(await response.json()).toMatchObject({
  83. id: project.id,
  84. name: "patched-project",
  85. commands: { start: "bun dev" },
  86. })
  87. const list = await app().request("/project", { headers: { "x-opencode-directory": tmp.path } })
  88. expect(list.status).toBe(200)
  89. expect(await list.json()).toContainEqual(
  90. expect.objectContaining({ id: project.id, name: "patched-project", commands: { start: "bun dev" } }),
  91. )
  92. })
  93. test("serves instance dispose through Hono bridge", async () => {
  94. await using tmp = await tmpdir()
  95. const disposed = new Promise<string | undefined>((resolve) => {
  96. const onEvent = (event: { directory?: string; payload: { type?: string } }) => {
  97. if (event.payload.type !== "server.instance.disposed") return
  98. GlobalBus.off("event", onEvent)
  99. resolve(event.directory)
  100. }
  101. GlobalBus.on("event", onEvent)
  102. })
  103. const response = await app().request(InstancePaths.dispose, {
  104. method: "POST",
  105. headers: { "x-opencode-directory": tmp.path },
  106. })
  107. expect(response.status).toBe(200)
  108. expect(await response.json()).toBe(true)
  109. expect(await disposed).toBe(tmp.path)
  110. })
  111. })