import { afterEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { buildBunInstallArgs, replaceBinaryForUpdate, resolveUpdateMethodForTest } from "../src/cli/update-cli"; const tempDirs: string[] = []; async function makeTempDir(): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-update-test-")); tempDirs.push(dir); return dir; } afterEach(async () => { await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true }))); }); describe("update-cli install target detection", () => { it("uses bun update when prioritized omp is inside bun global bin", () => { const method = resolveUpdateMethodForTest("/Users/test/.bun/bin/omp", "/Users/test/.bun/bin"); expect(method).toBe("bun"); }); it("uses binary update when prioritized omp is outside bun global bin", () => { const method = resolveUpdateMethodForTest("/Users/test/.local/bin/omp", "/Users/test/.bun/bin"); expect(method).toBe("binary"); }); it("uses binary update when bun global bin cannot be resolved", () => { const method = resolveUpdateMethodForTest("/Users/test/.local/bin/omp", undefined); expect(method).toBe("binary"); }); }); describe("update-cli bun install command", () => { it("pins the official npm registry and bypasses the manifest cache so a stale mirror or snapshot cannot mask a freshly published version", () => { // Regression: omp queries https://registry.npmjs.org//latest directly. // The install MUST hit the same registry, otherwise: // - a lagging mirror (corp proxy, Taobao, …) rejects the version with // `No version matching "X" (but package exists)`, // - or bun's local manifest snapshot does the same when the user's bun // is already pointed at the official registry but its cache predates // the release. // See https://github.com/can1357/oh-my-pi/issues/1686. const args = buildBunInstallArgs("15.7.6", "linux-x64"); expect(args.slice(0, 5)).toEqual([ "install", "-g", "--no-cache", "--registry=https://registry.npmjs.org/", "@oh-my-pi/pi-coding-agent@15.7.6", ]); }); it("pins the native addon core and the platform-specific leaf to the same version so the loader sentinel cannot drift on supported tags", () => { // Regression: bun install -g @ would update only the top-level // package, leaving @oh-my-pi/pi-natives and @oh-my-pi/pi-natives- // at their previous version. The next launch then loaded a stale .node // file and aborted at validateLoadedBindings with `The .node file on // disk is from a different release than this loader`. See // https://github.com/can1357/oh-my-pi/issues/1824. for (const tag of ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"]) { const args = buildBunInstallArgs("15.9.0", tag); expect(args).toContain("@oh-my-pi/pi-natives@15.9.0"); expect(args).toContain(`@oh-my-pi/pi-natives-${tag}@15.9.0`); } }); it("omits the leaf on unsupported platform tags so an EBADPLATFORM swap does not mask the underlying `no matching version` error", () => { // Defensive: an unsupported tag (e.g. linux-arm32) still installs the // core natives package — which will fail at module load if the platform // truly is unsupported — but we never request a leaf the release // pipeline doesn't publish, otherwise bun aborts with EBADPLATFORM // and hides the real diagnostic from `loadNative`'s aggregated error. const args = buildBunInstallArgs("15.9.0", "linux-arm"); expect(args).toContain("@oh-my-pi/pi-natives@15.9.0"); expect(args.some(arg => arg.startsWith("@oh-my-pi/pi-natives-"))).toBe(false); }); }); describe("update-cli binary replacement", () => { it("restores the previous binary when the replacement fails verification", async () => { const dir = await makeTempDir(); const targetPath = path.join(dir, "omp"); const tempPath = `${targetPath}.new`; const backupPath = `${targetPath}.bak`; await Bun.write(targetPath, "old binary"); await Bun.write(tempPath, "broken binary"); await expect( replaceBinaryForUpdate({ targetPath, tempPath, backupPath, expectedVersion: "15.1.8", verifyInstalledVersion: async () => ({ ok: false, path: targetPath }), }), ).rejects.toThrow("restored previous omp binary"); expect(await Bun.file(targetPath).text()).toBe("old binary"); expect(await Bun.file(tempPath).exists()).toBe(false); expect(await Bun.file(backupPath).exists()).toBe(false); }); it("keeps the replacement only after it reports the expected version", async () => { const dir = await makeTempDir(); const targetPath = path.join(dir, "omp"); const tempPath = `${targetPath}.new`; const backupPath = `${targetPath}.bak`; await Bun.write(targetPath, "old binary"); await Bun.write(tempPath, "new binary"); await replaceBinaryForUpdate({ targetPath, tempPath, backupPath, expectedVersion: "15.1.8", verifyInstalledVersion: async () => ({ ok: true, actual: "15.1.8", path: targetPath }), }); expect(await Bun.file(targetPath).text()).toBe("new binary"); expect(await Bun.file(tempPath).exists()).toBe(false); expect(await Bun.file(backupPath).exists()).toBe(false); }); });