import { describe, expect, test } from "bun:test"; import { compareVersions, createVersionObserver, installVersion, waitForVersionUpdate, } from "../../lib/version-update"; const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); describe("server version updates", () => { test("compares strict SemVer precedence including prereleases and ignores build metadata", () => { expect(compareVersions("2.6.1", "2.6.1-rc3")).toBe(1); expect(compareVersions("2.6.1-rc.10", "2.6.1-rc.3")).toBe(1); expect(compareVersions("2.6.1-alpha.1", "2.6.1-alpha.beta")).toBe(-1); expect(compareVersions("2.6.1-rc3.1", "2.6.1-rc3")).toBe(1); expect(compareVersions("2.6.1+new", "2.6.1+old")).toBe(0); expect(compareVersions("3.0.0", "2.6.1")).toBe(1); for (const invalid of [ "", "v3.0.0", "3.0", "03.0.0", "3.0.0-rc.01", "3.0.0-", "3.0.0+", "3.0.0;touch /tmp/pwned", "3.0.0\n", "3.0.0-💥", "9".repeat(129) + ".0.0", ]) { expect(compareVersions(invalid, "2.6.1-rc3")).toBeUndefined(); } }); test("rejects invalid install arguments before spawning", async () => { expect(await installVersion("2.7.0;echo hacked")).toBe(false); }); test("uses a quiet Bun install with explicit inherited environment", async () => { const calls: string[][] = []; const spawn = ( argv: string[], options: { stdin: "ignore"; stdout: "ignore"; stderr: "ignore"; env: NodeJS.ProcessEnv; }, ) => { calls.push(argv); expect(options).toEqual({ stdin: "ignore", stdout: "ignore", stderr: "ignore", env: process.env, }); expect(options.env.PATH).toBe(process.env.PATH); return { exited: Promise.resolve(0), kill: () => { throw new Error("unexpected kill"); }, }; }; expect(await installVersion("2.7.0-rc.1+build.2", spawn, 2)).toBe(true); expect(calls).toEqual([ [ process.execPath, "i", "-g", "--no-cache", "@dmgnr/kuber@2.7.0-rc.1+build.2", ], ]); }); test("kills a stalled installer and treats late success as failure", async () => { let finish!: (code: number) => void; const exited = new Promise((resolve) => { finish = resolve; }); const signals: (NodeJS.Signals | undefined)[] = []; const installing = installVersion( "2.7.0", () => ({ exited, kill: (signal) => { signals.push(signal); finish(0); }, }), 1, ); expect(await installing).toBe(false); expect(signals).toEqual([undefined]); }); test("handles spawn errors and bounds an unresponsive child", async () => { expect( await installVersion("2.7.0", () => { throw new Error("spawn failed"); }), ).toBe(false); const signals: (NodeJS.Signals | undefined)[] = []; expect( await installVersion( "2.7.0", () => ({ exited: new Promise(() => {}), kill: (signal) => { signals.push(signal); }, }), 1, ), ).toBe(false); expect(signals).toEqual([undefined, "SIGKILL"]); }); test("ignores malformed and exactly equal headers without attempting an install", async () => { const runs: string[] = []; const lines: string[] = []; const observe = createVersionObserver({ currentVersion: "2.6.1-rc3", runner: async (version) => { runs.push(version); return true; }, report: (line) => lines.push(line), }); for (const version of [null, "garbage", "2.6.1-rc3"]) observe(version); await tick(); expect(runs).toEqual([]); expect(lines).toEqual([]); }); test("starts once for concurrent newer responses, reports success in gray on stderr", async () => { let finish!: (value: boolean) => void; const pending = new Promise((resolve) => { finish = resolve; }); const runs: string[] = []; const lines: string[] = []; const observe = createVersionObserver({ currentVersion: "2.6.1-rc3", runner: (version) => { runs.push(version); return pending; }, report: (line) => lines.push(line), }); observe("2.6.1"); observe("9.0.0"); expect(runs).toEqual([]); await tick(); expect(runs).toEqual(["2.6.1"]); expect(lines).toEqual([]); finish(true); await waitForVersionUpdate(); expect(lines).toEqual(["\x1b[90m+ Updated to 2.6.1\x1b[0m\n"]); observe("10.0.0"); await tick(); expect(runs).toHaveLength(1); }); test("reports failure or rejection once without throwing into requests", async () => { for (const runner of [ async () => false, async () => { throw new Error("offline"); }, ]) { const lines: string[] = []; const observe = createVersionObserver({ currentVersion: "2.6.1-rc3", runner, report: (line) => lines.push(line), }); observe("2.7.0"); observe("2.8.0"); await tick(); expect(lines).toEqual([ "\x1b[90m+ New version available: 2.7.0\x1b[0m\n", ]); } }); test("a short subprocess emits one outcome to stderr when its runner settles", async () => { const url = new URL("../../lib/version-update.ts", import.meta.url).href; for (const result of ["true", "false", "reject"]) { const script = ` const { createVersionObserver } = await import(${JSON.stringify(url)}); createVersionObserver({ currentVersion: '2.6.1', runner: async () => ${result === "reject" ? "Promise.reject(Error('offline'))" : result} })('2.7.0'); await new Promise(resolve => setTimeout(resolve, 20)); `; const child = Bun.spawn([process.execPath, "-e", script], { stdin: "ignore", stdout: "pipe", stderr: "pipe", }); expect(await child.exited).toBe(0); expect(await new Response(child.stdout).text()).toBe(""); expect(await new Response(child.stderr).text()).toBe( `\x1b[90m+ ${result === "true" ? "Updated to " : "New version available: "}2.7.0\x1b[0m\n`, ); } }); });