httpapi-workspace.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import { mkdir } from "node:fs/promises"
  3. import path from "node:path"
  4. import { Effect } from "effect"
  5. import type { UpgradeWebSocket } from "hono/ws"
  6. import { Flag } from "@opencode-ai/core/flag/flag"
  7. import { registerAdaptor } from "../../src/control-plane/adaptors"
  8. import type { WorkspaceAdaptor } from "../../src/control-plane/types"
  9. import { Workspace } from "../../src/control-plane/workspace"
  10. import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/workspace"
  11. import { InstanceRoutes } from "../../src/server/routes/instance"
  12. import { Session } from "../../src/session"
  13. import { Log } from "../../src/util"
  14. import { resetDatabase } from "../fixture/db"
  15. import { tmpdir } from "../fixture/fixture"
  16. import { Instance } from "../../src/project/instance"
  17. void Log.init({ print: false })
  18. const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
  19. const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
  20. const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
  21. function request(path: string, directory: string, init: RequestInit = {}) {
  22. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
  23. const headers = new Headers(init.headers)
  24. headers.set("x-opencode-directory", directory)
  25. return InstanceRoutes(websocket).request(path, { ...init, headers })
  26. }
  27. function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
  28. return Effect.runPromise(fx.pipe(Effect.provide(Session.defaultLayer)))
  29. }
  30. function localAdaptor(directory: string): WorkspaceAdaptor {
  31. return {
  32. name: "Local Test",
  33. description: "Create a local test workspace",
  34. configure(info) {
  35. return {
  36. ...info,
  37. name: "local-test",
  38. directory,
  39. }
  40. },
  41. async create() {
  42. await mkdir(directory, { recursive: true })
  43. },
  44. async remove() {},
  45. target() {
  46. return {
  47. type: "local" as const,
  48. directory,
  49. }
  50. },
  51. }
  52. }
  53. afterEach(async () => {
  54. Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
  55. Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
  56. await Instance.disposeAll()
  57. await resetDatabase()
  58. })
  59. describe("workspace HttpApi", () => {
  60. test("serves read endpoints", async () => {
  61. await using tmp = await tmpdir({ git: true })
  62. const [adaptors, workspaces, status] = await Promise.all([
  63. request(WorkspacePaths.adaptors, tmp.path),
  64. request(WorkspacePaths.list, tmp.path),
  65. request(WorkspacePaths.status, tmp.path),
  66. ])
  67. expect(adaptors.status).toBe(200)
  68. expect(await adaptors.json()).toEqual([
  69. {
  70. type: "worktree",
  71. name: "Worktree",
  72. description: "Create a git worktree",
  73. },
  74. ])
  75. expect(workspaces.status).toBe(200)
  76. expect(await workspaces.json()).toEqual([])
  77. expect(status.status).toBe(200)
  78. expect(await status.json()).toEqual([])
  79. })
  80. test("serves mutation endpoints", async () => {
  81. Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
  82. await using tmp = await tmpdir({ git: true })
  83. await Instance.provide({
  84. directory: tmp.path,
  85. fn: async () =>
  86. registerAdaptor(Instance.project.id, "local-test", localAdaptor(path.join(tmp.path, ".workspace"))),
  87. })
  88. const created = await request(WorkspacePaths.list, tmp.path, {
  89. method: "POST",
  90. headers: { "content-type": "application/json" },
  91. body: JSON.stringify({ type: "local-test", branch: null, extra: null }),
  92. })
  93. expect(created.status).toBe(200)
  94. const workspace = (await created.json()) as Workspace.Info
  95. expect(workspace).toMatchObject({ type: "local-test", name: "local-test" })
  96. const session = await Instance.provide({
  97. directory: tmp.path,
  98. fn: async () => runSession(Session.Service.use((svc) => svc.create({}))),
  99. })
  100. const restored = await request(WorkspacePaths.sessionRestore.replace(":id", workspace.id), tmp.path, {
  101. method: "POST",
  102. headers: { "content-type": "application/json" },
  103. body: JSON.stringify({ sessionID: session.id }),
  104. })
  105. expect(restored.status).toBe(200)
  106. expect((await restored.json()) as { total: number }).toMatchObject({ total: expect.any(Number) })
  107. const removed = await request(WorkspacePaths.remove.replace(":id", workspace.id), tmp.path, { method: "DELETE" })
  108. expect(removed.status).toBe(200)
  109. expect(await removed.json()).toMatchObject({ id: workspace.id })
  110. const listed = await request(WorkspacePaths.list, tmp.path)
  111. expect(listed.status).toBe(200)
  112. expect(await listed.json()).toEqual([])
  113. })
  114. })