Files
oh-my-pi/packages/coding-agent/test/update-cli.test.ts
T

190 lines
7.7 KiB
TypeScript

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,
pruneBunInstallCache,
replaceBinaryForUpdate,
resolveBunGlobalNodeModulesDirFromLocations,
resolveUpdateMethodForTest,
} from "../src/cli/update-cli";
const tempDirs: string[] = [];
async function makeTempDir(): Promise<string> {
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/<pkg>/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.
expect(buildBunInstallArgs("15.7.6")).toEqual([
"install",
"-g",
"--no-cache",
"--registry=https://registry.npmjs.org/",
"@oh-my-pi/pi-coding-agent@15.7.6",
]);
});
it("derives global node_modules from supported bun global locations", () => {
expect(resolveBunGlobalNodeModulesDirFromLocations(path.join("home", ".bun", "bin"), undefined)).toBe(
path.join("home", ".bun", "install", "global", "node_modules"),
);
expect(
resolveBunGlobalNodeModulesDirFromLocations(undefined, path.join("home", ".bun", "install", "cache")),
).toBe(path.join("home", ".bun", "install", "global", "node_modules"));
});
});
describe("update-cli bun cache pruning", () => {
it("keeps only the newest cached version for filtered global install packages", async () => {
const dir = await makeTempDir();
await Bun.write(path.join(dir, "react", "18.3.1@@@1"), "");
await Bun.write(path.join(dir, "react", "19.2.6@@@1"), "");
await Bun.write(
path.join(dir, "react@18.3.1@@@1", "package.json"),
JSON.stringify({ name: "react", version: "18.3.1" }),
);
await Bun.write(
path.join(dir, "react@19.2.6@@@1", "package.json"),
JSON.stringify({ name: "react", version: "19.2.6" }),
);
await Bun.write(path.join(dir, "@oh-my-pi", "pi-utils", "15.7.6@@@1"), "");
await Bun.write(path.join(dir, "@oh-my-pi", "pi-utils", "15.8.0@@@1"), "");
await Bun.write(
path.join(dir, "@oh-my-pi", "pi-utils@15.7.6@@@1", "package.json"),
JSON.stringify({ name: "@oh-my-pi/pi-utils", version: "15.7.6" }),
);
await Bun.write(
path.join(dir, "@oh-my-pi", "pi-utils@15.8.0@@@1", "package.json"),
JSON.stringify({ name: "@oh-my-pi/pi-utils", version: "15.8.0" }),
);
await Bun.write(path.join(dir, "chalk", "4.1.2@@@1"), "");
await Bun.write(path.join(dir, "chalk", "5.6.2@@@1"), "");
await Bun.write(
path.join(dir, "chalk@4.1.2@@@1", "package.json"),
JSON.stringify({ name: "chalk", version: "4.1.2" }),
);
await Bun.write(
path.join(dir, "chalk@5.6.2@@@1", "package.json"),
JSON.stringify({ name: "chalk", version: "5.6.2" }),
);
const result = await pruneBunInstallCache(dir, new Set(["react", "@oh-my-pi/pi-utils"]));
expect(result).toEqual({ scannedPackages: 2, removedEntries: 4 });
expect(await Bun.file(path.join(dir, "react", "18.3.1@@@1")).exists()).toBe(false);
expect(await Bun.file(path.join(dir, "react@18.3.1@@@1", "package.json")).exists()).toBe(false);
expect(await Bun.file(path.join(dir, "react", "19.2.6@@@1")).exists()).toBe(true);
expect(await Bun.file(path.join(dir, "react@19.2.6@@@1", "package.json")).exists()).toBe(true);
expect(await Bun.file(path.join(dir, "@oh-my-pi", "pi-utils", "15.7.6@@@1")).exists()).toBe(false);
expect(await Bun.file(path.join(dir, "@oh-my-pi", "pi-utils@15.7.6@@@1", "package.json")).exists()).toBe(false);
expect(await Bun.file(path.join(dir, "@oh-my-pi", "pi-utils", "15.8.0@@@1")).exists()).toBe(true);
expect(await Bun.file(path.join(dir, "@oh-my-pi", "pi-utils@15.8.0@@@1", "package.json")).exists()).toBe(true);
expect(await Bun.file(path.join(dir, "chalk", "4.1.2@@@1")).exists()).toBe(true);
expect(await Bun.file(path.join(dir, "chalk@4.1.2@@@1", "package.json")).exists()).toBe(true);
});
it("treats a stable release as newer than a matching prerelease", async () => {
const dir = await makeTempDir();
await Bun.write(path.join(dir, "pkg", "1.0.0-beta.1@@@1"), "");
await Bun.write(path.join(dir, "pkg", "1.0.0@@@1"), "");
await Bun.write(
path.join(dir, "pkg@1.0.0-beta.1@@@1", "package.json"),
JSON.stringify({ name: "pkg", version: "1.0.0-beta.1" }),
);
await Bun.write(
path.join(dir, "pkg@1.0.0@@@1", "package.json"),
JSON.stringify({ name: "pkg", version: "1.0.0" }),
);
const result = await pruneBunInstallCache(dir);
expect(result).toEqual({ scannedPackages: 1, removedEntries: 2 });
expect(await Bun.file(path.join(dir, "pkg", "1.0.0-beta.1@@@1")).exists()).toBe(false);
expect(await Bun.file(path.join(dir, "pkg@1.0.0-beta.1@@@1", "package.json")).exists()).toBe(false);
expect(await Bun.file(path.join(dir, "pkg", "1.0.0@@@1")).exists()).toBe(true);
expect(await Bun.file(path.join(dir, "pkg@1.0.0@@@1", "package.json")).exists()).toBe(true);
});
});
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);
});
});