promise-service.test.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { afterEach, expect, test } from "bun:test"
  2. import { mkdtemp, rm } from "node:fs/promises"
  3. import { tmpdir } from "node:os"
  4. import { join } from "node:path"
  5. import { Service, type EnsureReason } from "../src/promise/service"
  6. const fixture = join(import.meta.dir, "fixture/service.ts")
  7. const processes: Bun.Subprocess[] = []
  8. const directories: string[] = []
  9. afterEach(async () => {
  10. processes.forEach((process) => process.kill("SIGTERM"))
  11. await Promise.all(processes.splice(0).map((process) => process.exited))
  12. await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })))
  13. })
  14. test("discovers a registered service", async () => {
  15. const registration = await setup("graceful")
  16. expect(await Service.discover({ file: registration, version: "test" })).toEqual(
  17. expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
  18. )
  19. expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined()
  20. })
  21. test("ensures a missing service with native promises", async () => {
  22. const directory = await temp()
  23. const registration = join(directory, "service.json")
  24. const starts: EnsureReason[] = []
  25. const endpoint = await Service.ensure({
  26. file: registration,
  27. version: "test",
  28. command: [process.execPath, fixture, registration, "coordinated"],
  29. onStart: (reason) => starts.push(reason),
  30. })
  31. const info = await Bun.file(registration).json()
  32. try {
  33. expect(endpoint.url).toBe(info.url)
  34. expect(starts).toEqual(["missing"])
  35. } finally {
  36. process.kill(info.pid, "SIGTERM")
  37. await waitForExit(info.pid)
  38. }
  39. }, 15_000)
  40. test("reports a failed registered service", async () => {
  41. const registration = await setup("failed-owner")
  42. await expect(Service.ensure({ file: registration, version: "test", command: [] })).rejects.toThrow(
  43. "Background service failed to start",
  44. )
  45. })
  46. test("requests graceful stop of the exact service instance", async () => {
  47. const registration = await setup("graceful")
  48. const info = await Bun.file(registration).json()
  49. await Service.stop({ file: registration })
  50. expect(await Bun.file(registration + ".stop").json()).toEqual({ instanceID: info.id })
  51. })
  52. async function setup(mode: string) {
  53. const directory = await temp()
  54. const registration = join(directory, "service.json")
  55. processes.push(Bun.spawn([process.execPath, fixture, registration, mode], { stdout: "ignore", stderr: "inherit" }))
  56. await waitForFile(registration)
  57. return registration
  58. }
  59. async function temp() {
  60. const directory = await mkdtemp(join(tmpdir(), "opencode-promise-service-"))
  61. directories.push(directory)
  62. return directory
  63. }
  64. async function waitForFile(file: string) {
  65. for (let attempt = 0; attempt < 600; attempt++) {
  66. if (await Bun.file(file).exists()) return
  67. await Bun.sleep(5)
  68. }
  69. throw new Error(`Timed out waiting for ${file}`)
  70. }
  71. async function waitForExit(pid: number) {
  72. for (let attempt = 0; attempt < 600; attempt++) {
  73. try {
  74. process.kill(pid, 0)
  75. } catch {
  76. return
  77. }
  78. await Bun.sleep(5)
  79. }
  80. throw new Error(`Timed out waiting for process ${pid}`)
  81. }