process-lock.test.ts 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. import { expect } from "bun:test"
  2. import { ProcessLock } from "@opencode-ai/core/util/process-lock"
  3. import { Effect } from "effect"
  4. import fs from "node:fs/promises"
  5. import os from "node:os"
  6. import path from "node:path"
  7. import { it } from "../lib/effect"
  8. const worker = path.join(import.meta.dir, "../fixture/process-lock-worker.ts")
  9. it.live(
  10. "releases ownership when the scope closes",
  11. Effect.gen(function* () {
  12. const root = yield* temp("opencode-process-lock-")
  13. const file = path.join(root, "service.lock")
  14. yield* Effect.scoped(ProcessLock.acquire(file))
  15. yield* Effect.scoped(ProcessLock.acquire(file))
  16. }),
  17. )
  18. it.live(
  19. "releases ownership when the process dies",
  20. Effect.gen(function* () {
  21. const root = yield* temp("opencode-process-lock-death-")
  22. const file = path.join(root, "service.lock")
  23. const ready = path.join(root, "ready")
  24. const child = yield* Effect.acquireRelease(
  25. Effect.sync(() =>
  26. Bun.spawn([process.execPath, worker, JSON.stringify({ file, ready })], {
  27. stdout: "ignore",
  28. stderr: "pipe",
  29. }),
  30. ),
  31. (child) =>
  32. Effect.promise(async () => {
  33. kill(child)
  34. await child.exited
  35. }),
  36. )
  37. yield* Effect.promise(async () => {
  38. for (let attempt = 0; attempt < 100 && !(await Bun.file(ready).exists()); attempt++) await Bun.sleep(20)
  39. })
  40. expect(yield* Effect.promise(() => Bun.file(ready).exists())).toBe(true)
  41. const error = yield* Effect.scoped(ProcessLock.acquire(file)).pipe(Effect.flip)
  42. expect(error._tag).toBe("ProcessLockHeldError")
  43. if (process.platform !== "win32") {
  44. process.kill(child.pid, "SIGSTOP")
  45. const paused = yield* Effect.scoped(ProcessLock.acquire(file)).pipe(Effect.flip)
  46. expect(paused._tag).toBe("ProcessLockHeldError")
  47. process.kill(child.pid, "SIGCONT")
  48. }
  49. kill(child)
  50. yield* Effect.promise(() => child.exited)
  51. yield* Effect.scoped(ProcessLock.acquire(file))
  52. }),
  53. )
  54. function temp(prefix: string) {
  55. return Effect.acquireRelease(
  56. Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), prefix))),
  57. (root) => Effect.promise(() => fs.rm(root, { recursive: true, force: true })),
  58. )
  59. }
  60. function kill(child: Bun.Subprocess) {
  61. if (process.platform === "win32") return child.kill()
  62. return child.kill("SIGKILL")
  63. }