| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- import { afterEach, expect, test } from "bun:test"
- import { mkdtemp, rm } from "node:fs/promises"
- import { tmpdir } from "node:os"
- import { join } from "node:path"
- import { Service, type EnsureReason } from "../src/promise/service"
- import { accelerate, waitForExit } from "./fixture/service-timing"
- const fixture = join(import.meta.dir, "fixture/service.ts")
- const ensure = accelerate(Service.ensure)
- const processes: Bun.Subprocess[] = []
- const directories: string[] = []
- afterEach(async () => {
- processes.forEach((process) => process.kill("SIGTERM"))
- await Promise.all(processes.splice(0).map((process) => process.exited))
- await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })))
- })
- test("discovers a registered service", async () => {
- const registration = await setup("graceful")
- expect(await Service.discover({ file: registration, version: "test" })).toEqual(
- expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
- )
- expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined()
- })
- test("discovers a compatible registered service", async () => {
- const registration = await setup("compatible")
- expect(await Service.discover({ file: registration, version: "2.1.0" })).toBeUndefined()
- expect(await Service.discover({ file: registration, version: "2.1.0-next.1" })).toEqual(
- expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
- )
- expect(await Service.discover({ file: registration, version: (version) => version.startsWith("2.") })).toEqual(
- expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
- )
- expect(await Service.discover({ file: registration, version: (version) => version.startsWith("3.") })).toBeUndefined()
- })
- test("ensures a missing service with native promises", async () => {
- const directory = await temp()
- const registration = join(directory, "service.json")
- const starts: EnsureReason[] = []
- const endpoint = await ensure({
- file: registration,
- version: "test",
- command: [process.execPath, fixture, registration, "coordinated"],
- onStart: (reason) => starts.push(reason),
- })
- const info = await Bun.file(registration).json()
- try {
- expect(endpoint.url).toBe(info.url)
- expect(starts).toEqual(["missing"])
- } finally {
- process.kill(info.pid, "SIGTERM")
- await waitForExit(info.pid)
- }
- })
- test("waits for a live contender when another native contender fails", async () => {
- const directory = await temp()
- const registration = join(directory, "service.json")
- const endpoint = await ensure({
- file: registration,
- version: "test",
- command: [process.execPath, fixture, registration, "coordinated-failed-loser", "300"],
- })
- const info = await Bun.file(registration).json()
- try {
- expect(endpoint.url).toBe(info.url)
- } finally {
- process.kill(info.pid, "SIGTERM")
- await waitForExit(info.pid)
- }
- })
- test("reports a failed registered service", async () => {
- const registration = await setup("failed-owner")
- await expect(ensure({ file: registration, version: "test", command: [] })).rejects.toThrow(
- "Background service failed to start",
- )
- })
- test("reports a bounded contender stderr tail with native promises", async () => {
- const directory = await temp()
- const registration = join(directory, "service.json")
- const error = await Service.ensure({
- file: registration,
- version: "test",
- command: [process.execPath, fixture, registration, "stderr-failed"],
- }).catch((error: unknown) => error)
- expect(error).toBeInstanceOf(Error)
- if (!(error instanceof Error)) throw error
- expect(error.message).toContain("actionable startup failure")
- expect(error.message.length).toBeLessThan(9_000)
- }, 10_000)
- test("evicts an unresponsive registered service before starting its replacement", async () => {
- const directory = await temp()
- const registration = join(directory, "service.json")
- const existing = Bun.spawn([process.execPath, fixture, registration, "hanging"], {
- stdout: "ignore",
- stderr: "inherit",
- })
- processes.push(existing)
- await waitForFile(registration)
- const original = await Bun.file(registration).json()
- const endpoint = await ensure({
- file: registration,
- version: "test",
- command: [process.execPath, fixture, registration, "delayed", "10"],
- })
- const replacement = await Bun.file(registration).json()
- expect((await Bun.file(registration + ".requests").text()).trim().split("\n")).toHaveLength(3)
- expect(await existing.exited).toBe(0)
- expect(replacement.pid).not.toBe(original.pid)
- expect(endpoint.url).toBe(replacement.url)
- process.kill(replacement.pid, "SIGTERM")
- await waitForExit(replacement.pid)
- })
- test("requests graceful stop of the exact service instance", async () => {
- const registration = await setup("graceful")
- const info = await Bun.file(registration).json()
- await Service.stop({ file: registration })
- expect(await Bun.file(registration + ".stop").json()).toEqual({ instanceID: info.id })
- })
- async function setup(mode: string) {
- const directory = await temp()
- const registration = join(directory, "service.json")
- processes.push(Bun.spawn([process.execPath, fixture, registration, mode], { stdout: "ignore", stderr: "inherit" }))
- await waitForFile(registration)
- return registration
- }
- async function temp() {
- const directory = await mkdtemp(join(tmpdir(), "opencode-promise-service-"))
- directories.push(directory)
- return directory
- }
- async function waitForFile(file: string) {
- for (let attempt = 0; attempt < 600; attempt++) {
- if (await Bun.file(file).exists()) return
- await Bun.sleep(5)
- }
- throw new Error(`Timed out waiting for ${file}`)
- }
|