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" const fixture = join(import.meta.dir, "fixture/service.ts") 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("ensures a missing service with native promises", async () => { const directory = await temp() const registration = join(directory, "service.json") const starts: EnsureReason[] = [] const endpoint = await Service.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) } }, 15_000) test("reports a failed registered service", async () => { const registration = await setup("failed-owner") await expect(Service.ensure({ file: registration, version: "test", command: [] })).rejects.toThrow( "Background service failed to start", ) }) 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}`) } async function waitForExit(pid: number) { for (let attempt = 0; attempt < 600; attempt++) { try { process.kill(pid, 0) } catch { return } await Bun.sleep(5) } throw new Error(`Timed out waiting for process ${pid}`) }