mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
180 lines
7.5 KiB
TypeScript
180 lines
7.5 KiB
TypeScript
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test";
|
|
|
|
const binaryUpdateMocks = vi.hoisted(() => ({
|
|
performBinaryUpdate: vi.fn(),
|
|
}));
|
|
|
|
const childProcessMocks = vi.hoisted(() => ({
|
|
execSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../src/utils/binary-update.ts", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../src/utils/binary-update.ts")>();
|
|
return { ...actual, performBinaryUpdate: binaryUpdateMocks.performBinaryUpdate };
|
|
});
|
|
|
|
vi.mock("child_process", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("child_process")>();
|
|
return { ...actual, execSync: childProcessMocks.execSync };
|
|
});
|
|
|
|
import {
|
|
compareVersion,
|
|
isMajorUpgrade,
|
|
isNewerVersion,
|
|
isPrerelease,
|
|
parseVersion,
|
|
performAutoUpdate,
|
|
shouldAutoUpdate,
|
|
} from "../src/utils/update-checker.ts";
|
|
|
|
let configDir: string;
|
|
let previousConfigDir: string | undefined;
|
|
let previousInstallMethod: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
configDir = mkdtempSync(join(tmpdir(), "bl-auto-update-binary-"));
|
|
previousConfigDir = process.env.BAILIAN_CONFIG_DIR;
|
|
previousInstallMethod = process.env.BAILIAN_INSTALL_METHOD;
|
|
process.env.BAILIAN_CONFIG_DIR = configDir;
|
|
process.env.BAILIAN_INSTALL_METHOD = "binary";
|
|
binaryUpdateMocks.performBinaryUpdate.mockResolvedValue("2.0.0");
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (previousConfigDir === undefined) delete process.env.BAILIAN_CONFIG_DIR;
|
|
else process.env.BAILIAN_CONFIG_DIR = previousConfigDir;
|
|
if (previousInstallMethod === undefined) delete process.env.BAILIAN_INSTALL_METHOD;
|
|
else process.env.BAILIAN_INSTALL_METHOD = previousInstallMethod;
|
|
rmSync(configDir, { recursive: true, force: true });
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
test("parseVersion strips pre-release and build metadata", () => {
|
|
expect(parseVersion("1.4.2")).toEqual([1, 4, 2]);
|
|
expect(parseVersion("2.0.0-beta.1")).toEqual([2, 0, 0]);
|
|
expect(parseVersion("2.0.0+build.42")).toEqual([2, 0, 0]);
|
|
expect(parseVersion("2.0.0-beta.1+build.42")).toEqual([2, 0, 0]);
|
|
});
|
|
|
|
test("parseVersion coerces non-numeric segments to 0 instead of NaN", () => {
|
|
const [a, b, c] = parseVersion("2.0.0-beta.1");
|
|
expect(Number.isNaN(a)).toBe(false);
|
|
expect(Number.isNaN(b)).toBe(false);
|
|
expect(Number.isNaN(c)).toBe(false);
|
|
expect([a, b, c]).toEqual([2, 0, 0]);
|
|
});
|
|
|
|
test("isNewerVersion never misjudges pre-release versions as equal", () => {
|
|
// Pre-release of a higher version must still be detected as newer.
|
|
expect(isNewerVersion("2.0.0-beta.1", "1.4.2")).toBe(true);
|
|
// Pre-release target must not be considered newer than an equal release.
|
|
expect(isNewerVersion("1.4.2", "2.0.0-beta.1")).toBe(false);
|
|
// Patch bump still detected.
|
|
expect(isNewerVersion("1.4.3", "1.4.2")).toBe(true);
|
|
// Equal versions are not newer.
|
|
expect(isNewerVersion("1.4.2", "1.4.2")).toBe(false);
|
|
// A release outranks its own pre-release: the release IS newer.
|
|
expect(isNewerVersion("1.4.2", "1.4.2-beta.1")).toBe(true);
|
|
// ...and the pre-release is NOT newer than the release.
|
|
expect(isNewerVersion("1.4.2-beta.1", "1.4.2")).toBe(false);
|
|
});
|
|
|
|
test("isMajorUpgrade handles pre-release versions without false negatives", () => {
|
|
// Major bump through a pre-release channel must trigger.
|
|
expect(isMajorUpgrade("2.0.0-beta.1", "1.4.2")).toBe(true);
|
|
// Small minor gap does not trigger.
|
|
expect(isMajorUpgrade("1.5.0", "1.4.2")).toBe(false);
|
|
// Minor gap > 3 triggers within the same major.
|
|
expect(isMajorUpgrade("1.8.0", "1.4.2")).toBe(true);
|
|
// No upgrade.
|
|
expect(isMajorUpgrade("1.4.2", "1.4.2")).toBe(false);
|
|
// Pre-release of the same major/minor does not trigger.
|
|
expect(isMajorUpgrade("1.4.2-beta.1", "1.4.2")).toBe(false);
|
|
});
|
|
|
|
// All published beta builds use the `0.0.0-<meta>` convention today. Under that
|
|
// scheme a beta collapses to [0,0,0], so a stable release is always a major
|
|
// upgrade over a beta. The robust invariants — regardless of hash ordering —
|
|
// are: beta -> stable auto-updates, canary -> canary never auto-updates, and a
|
|
// stable user is never upgraded down to a canary.
|
|
test("beta builds (0.0.0-*) auto-update to stable, never to another beta", () => {
|
|
const beta = "0.0.0-beta-e0a7c86-20260624";
|
|
const otherBeta = "0.0.0-beta-aaaaaaaa-20260625";
|
|
const stable = "1.4.2";
|
|
|
|
// beta -> stable: newer, significant, and stable -> auto-update
|
|
expect(isNewerVersion(stable, beta)).toBe(true);
|
|
expect(shouldAutoUpdate(stable, beta)).toBe(true);
|
|
|
|
// canary -> canary: never auto-updates (same core, no major gap)
|
|
expect(shouldAutoUpdate(otherBeta, beta)).toBe(false);
|
|
expect(shouldAutoUpdate(beta, otherBeta)).toBe(false);
|
|
|
|
// stable -> canary: never an upgrade
|
|
expect(isNewerVersion(beta, stable)).toBe(false);
|
|
expect(shouldAutoUpdate(beta, stable)).toBe(false);
|
|
});
|
|
|
|
test("isPrerelease detects pre-release suffixes and ignores build metadata", () => {
|
|
expect(isPrerelease("1.4.2")).toBe(false);
|
|
expect(isPrerelease("1.4.2-beta.1")).toBe(true);
|
|
expect(isPrerelease("0.0.0-beta-e0a7c86-20260624")).toBe(true);
|
|
expect(isPrerelease("2.0.0-alpha")).toBe(true);
|
|
// Build metadata alone does not make a version a pre-release.
|
|
expect(isPrerelease("1.4.2+build.42")).toBe(false);
|
|
expect(isPrerelease("1.4.2-beta.1+build.42")).toBe(true);
|
|
});
|
|
|
|
test("compareVersion respects full semver pre-release precedence", () => {
|
|
// Release outranks its own pre-release (the case the old stripper missed).
|
|
expect(compareVersion("1.4.2", "1.4.2-beta.1")).toBeGreaterThan(0);
|
|
expect(compareVersion("1.4.2-beta.1", "1.4.2")).toBeLessThan(0);
|
|
// Numeric pre-release identifiers compared numerically.
|
|
expect(compareVersion("1.4.2-beta.11", "1.4.2-beta.2")).toBeGreaterThan(0);
|
|
// Alphanumeric identifiers compared lexically (rc > beta > alpha).
|
|
expect(compareVersion("1.4.2-rc.1", "1.4.2-beta.5")).toBeGreaterThan(0);
|
|
expect(compareVersion("1.4.2-beta.1", "1.4.2-alpha.1")).toBeGreaterThan(0);
|
|
// Numeric identifier has lower precedence than a non-numeric one.
|
|
expect(compareVersion("1.4.2-alpha.beta", "1.4.2-alpha.1")).toBeGreaterThan(0);
|
|
// Canonical semver ordering end-to-end.
|
|
const ordered = [
|
|
"1.0.0-alpha",
|
|
"1.0.0-alpha.1",
|
|
"1.0.0-alpha.beta",
|
|
"1.0.0-beta",
|
|
"1.0.0-beta.2",
|
|
"1.0.0-beta.11",
|
|
"1.0.0-rc.1",
|
|
"1.0.0",
|
|
];
|
|
for (let i = 0; i < ordered.length - 1; i++) {
|
|
expect(compareVersion(ordered[i + 1]!, ordered[i]!)).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("shouldAutoUpdate only targets stable releases with a significant gap", () => {
|
|
// Stable major bump: auto-update.
|
|
expect(shouldAutoUpdate("2.0.0", "1.4.2")).toBe(true);
|
|
// Stable minor gap > 3: auto-update.
|
|
expect(shouldAutoUpdate("1.8.0", "1.4.2")).toBe(true);
|
|
// Small stable gap: notify only.
|
|
expect(shouldAutoUpdate("1.5.0", "1.4.2")).toBe(false);
|
|
// Pre-release target is NEVER auto-installed, even on a major bump.
|
|
expect(shouldAutoUpdate("2.0.0-beta.1", "1.4.2")).toBe(false);
|
|
expect(shouldAutoUpdate("2.0.0-rc.1", "1.4.2")).toBe(false);
|
|
// Same core, release over its pre-release: notify only (no major gap).
|
|
expect(shouldAutoUpdate("1.4.2", "1.4.2-beta.1")).toBe(false);
|
|
});
|
|
|
|
test("binary auto-update syncs bailian skills after the CLI update succeeds", async () => {
|
|
const updated = await performAutoUpdate("1.14.3", "2.0.0");
|
|
|
|
expect(updated).toBe(true);
|
|
expect(binaryUpdateMocks.performBinaryUpdate).toHaveBeenCalledWith("2.0.0");
|
|
expect(childProcessMocks.execSync).toHaveBeenCalledWith("bl skill init", { stdio: "inherit" });
|
|
});
|