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}`) }