project-init-git.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { afterEach, describe, expect } from "bun:test"
  2. import { AppFileSystem } from "@opencode-ai/core/filesystem"
  3. import { Effect, Layer } from "effect"
  4. import path from "path"
  5. import { InstanceRef } from "../../src/effect/instance-ref"
  6. import { InstanceBootstrap } from "../../src/project/bootstrap-service"
  7. import { InstanceStore } from "../../src/project/instance-store"
  8. import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
  9. import { Snapshot } from "../../src/snapshot"
  10. import { Server } from "../../src/server/server"
  11. import * as Log from "@opencode-ai/core/util/log"
  12. import { resetDatabase } from "../fixture/db"
  13. import { disposeAllInstances, TestInstance } from "../fixture/fixture"
  14. import { testEffect } from "../lib/effect"
  15. void Log.init({ print: false })
  16. afterEach(async () => {
  17. await disposeAllInstances()
  18. await resetDatabase()
  19. })
  20. const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
  21. const testInstanceStore = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
  22. const it = testEffect(Layer.mergeAll(AppFileSystem.defaultLayer, Snapshot.defaultLayer, testInstanceStore))
  23. function request(directory: string, url: string, init: RequestInit = {}) {
  24. return Effect.promise(() => {
  25. const headers = new Headers(init.headers)
  26. headers.set("x-opencode-directory", directory)
  27. return Promise.resolve(Server.Default().app.request(url, { ...init, headers }))
  28. })
  29. }
  30. function json<T>(response: Response) {
  31. return Effect.promise(() => response.json() as Promise<T>)
  32. }
  33. function collectGlobalEvents() {
  34. return Effect.acquireRelease(
  35. Effect.sync(() => {
  36. const seen: GlobalEvent[] = []
  37. const on = (event: GlobalEvent) => {
  38. seen.push(event)
  39. }
  40. GlobalBus.on("event", on)
  41. return { seen, on }
  42. }),
  43. ({ on }) => Effect.sync(() => GlobalBus.off("event", on)),
  44. )
  45. }
  46. const disposedEvents = (seen: GlobalEvent[], dir: string) =>
  47. seen.filter((evt) => evt.directory === dir && evt.payload.type === "server.instance.disposed").length
  48. describe("project.initGit endpoint", () => {
  49. it.instance("initializes git and reloads immediately", () =>
  50. Effect.gen(function* () {
  51. const tmp = yield* TestInstance
  52. const fs = yield* AppFileSystem.Service
  53. const events = yield* collectGlobalEvents()
  54. const init = yield* request(tmp.directory, "/project/git/init", {
  55. method: "POST",
  56. })
  57. const body = yield* json(init)
  58. expect(init.status).toBe(200)
  59. expect(body).toMatchObject({
  60. id: "global",
  61. vcs: "git",
  62. worktree: tmp.directory,
  63. })
  64. // Reload behavior: bus emits exactly one server.instance.disposed for the directory.
  65. expect(disposedEvents(events.seen, tmp.directory)).toBe(1)
  66. expect(yield* fs.exists(path.join(tmp.directory, ".git", "opencode"))).toBe(false)
  67. const current = yield* request(tmp.directory, "/project/current")
  68. expect(current.status).toBe(200)
  69. expect(yield* json(current)).toMatchObject({
  70. id: "global",
  71. vcs: "git",
  72. worktree: tmp.directory,
  73. })
  74. const ctx = yield* InstanceStore.Service.use((store) => store.reload({ directory: tmp.directory }))
  75. const tracked = yield* Snapshot.Service.use((snapshot) => snapshot.track()).pipe(
  76. Effect.provideService(InstanceRef, ctx),
  77. )
  78. expect(tracked).toBeTruthy()
  79. }),
  80. )
  81. it.instance(
  82. "does not reload when the project is already git",
  83. () =>
  84. Effect.gen(function* () {
  85. const tmp = yield* TestInstance
  86. const events = yield* collectGlobalEvents()
  87. const init = yield* request(tmp.directory, "/project/git/init", {
  88. method: "POST",
  89. })
  90. expect(init.status).toBe(200)
  91. expect(yield* json(init)).toMatchObject({
  92. vcs: "git",
  93. worktree: tmp.directory,
  94. })
  95. expect(disposedEvents(events.seen, tmp.directory)).toBe(0)
  96. const current = yield* request(tmp.directory, "/project/current")
  97. expect(current.status).toBe(200)
  98. expect(yield* json(current)).toMatchObject({
  99. vcs: "git",
  100. worktree: tmp.directory,
  101. })
  102. }),
  103. { git: true },
  104. )
  105. })