211 lines
6.2 KiB
TypeScript
211 lines
6.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
compareVersions,
|
|
createVersionObserver,
|
|
installVersion,
|
|
waitForVersionUpdate,
|
|
} from "../../lib/version-update";
|
|
|
|
const tick = () => new Promise<void>((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/[email protected]+build.2",
|
|
],
|
|
]);
|
|
});
|
|
|
|
test("kills a stalled installer and treats late success as failure", async () => {
|
|
let finish!: (code: number) => void;
|
|
const exited = new Promise<number>((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<number>(() => {}),
|
|
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<boolean>((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`,
|
|
);
|
|
}
|
|
});
|
|
});
|