Files
modelstudioai__cli/packages/runtime/tests/binary-update-layout.test.ts
rendianmeng 658763af2c fix: ci test
2026-08-03 15:29:55 +08:00

188 lines
7.2 KiB
TypeScript

import { mkdtempSync, rmSync, writeFileSync, mkdirSync, readlinkSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vite-plus/test";
import {
ensureBinaryPathEntries,
ensureWindowsBinJunction,
getBinaryBinRoot,
getBinaryCurrentPath,
getBinaryShareRoot,
getBinaryVersionsDir,
isValidUpdateTargetVersion,
normalizeBinaryVersion,
pruneBinaryVersions,
readCurrentVersionDir,
resolveBinaryDownloadSpec,
switchCurrentToVersion,
} from "../src/utils/binary-update.ts";
import { binaryAssetFileName, releaseAssetUrl } from "bailian-cli-core";
function withTempBinaryRoots(run: () => Promise<void>): Promise<void> {
const root = mkdtempSync(join(tmpdir(), "bl-binary-layout-"));
const previousShare = process.env.BAILIAN_SHARE_DIR;
const previousBin = process.env.BAILIAN_BIN_DIR;
process.env.BAILIAN_SHARE_DIR = root;
process.env.BAILIAN_BIN_DIR = join(root, "path-bin");
return run().finally(() => {
if (previousShare === undefined) delete process.env.BAILIAN_SHARE_DIR;
else process.env.BAILIAN_SHARE_DIR = previousShare;
if (previousBin === undefined) delete process.env.BAILIAN_BIN_DIR;
else process.env.BAILIAN_BIN_DIR = previousBin;
rmSync(root, { recursive: true, force: true });
});
}
function seedVersion(version: string): string {
const versionDir = join(getBinaryVersionsDir(), version);
mkdirSync(versionDir, { recursive: true });
const binaryName = process.platform === "win32" ? "bl.exe" : "bl";
writeFileSync(join(versionDir, binaryName), "fake-binary");
if (process.platform === "win32") {
writeFileSync(join(versionDir, "bailian.exe"), "fake-binary");
}
return versionDir;
}
test("share/bin roots respect BAILIAN_* overrides", async () => {
await withTempBinaryRoots(async () => {
expect(getBinaryShareRoot()).toContain("bl-binary-layout-");
expect(getBinaryBinRoot()).toBe(join(getBinaryShareRoot(), "path-bin"));
expect(getBinaryCurrentPath()).toBe(join(getBinaryShareRoot(), "current"));
});
});
test("switchCurrentToVersion retargets current pointer", async () => {
await withTempBinaryRoots(async () => {
const firstDir = seedVersion("1.0.0");
await switchCurrentToVersion("1.0.0");
expect(await readCurrentVersionDir()).toBe(firstDir);
const secondDir = seedVersion("1.1.0");
await switchCurrentToVersion("1.1.0");
expect(await readCurrentVersionDir()).toBe(secondDir);
});
});
test("pruneBinaryVersions keeps current and requested previous only", async () => {
await withTempBinaryRoots(async () => {
seedVersion("1.0.0");
seedVersion("1.1.0");
seedVersion("1.2.0");
await switchCurrentToVersion("1.2.0");
await pruneBinaryVersions(["1.2.0", "1.1.0"]);
expect(existsSync(join(getBinaryVersionsDir(), "1.2.0"))).toBe(true);
expect(existsSync(join(getBinaryVersionsDir(), "1.1.0"))).toBe(true);
expect(existsSync(join(getBinaryVersionsDir(), "1.0.0"))).toBe(false);
});
});
test("normalizeBinaryVersion strips release-style v prefix", () => {
expect(normalizeBinaryVersion("v1.2.3")).toBe("1.2.3");
expect(normalizeBinaryVersion("1.2.3")).toBe("1.2.3");
expect(normalizeBinaryVersion(" 0.1.14-channel.1 ")).toBe("0.1.14-channel.1");
});
test("isValidUpdateTargetVersion accepts semver and channel betas", () => {
expect(isValidUpdateTargetVersion("1.13.0")).toBe(true);
expect(isValidUpdateTargetVersion("v1.13.0")).toBe(true);
expect(isValidUpdateTargetVersion("1.4.2-beta.1")).toBe(true);
expect(isValidUpdateTargetVersion("0.0.0-beta-be3033b-202607311142")).toBe(true);
expect(isValidUpdateTargetVersion("v0.0.0-beta-be3033b-202607311142")).toBe(true);
expect(isValidUpdateTargetVersion("latest")).toBe(false);
expect(isValidUpdateTargetVersion("1.2")).toBe(false);
expect(isValidUpdateTargetVersion("foo")).toBe(false);
expect(isValidUpdateTargetVersion("")).toBe(false);
// Path-traversal / path-separator inputs must never reach versions/<ver>/
expect(isValidUpdateTargetVersion("../../../..")).toBe(false);
expect(isValidUpdateTargetVersion("..\\..\\..")).toBe(false);
expect(isValidUpdateTargetVersion("1.2.3/../x")).toBe(false);
expect(isValidUpdateTargetVersion("1.2.3\\..\\x")).toBe(false);
expect(isValidUpdateTargetVersion("/etc/passwd")).toBe(false);
expect(isValidUpdateTargetVersion("versions/../../tmp")).toBe(false);
expect(isValidUpdateTargetVersion("1.2.3/")).toBe(false);
expect(isValidUpdateTargetVersion("..")).toBe(false);
});
test("resolveBinaryDownloadSpec targets version assets, not latest manifest url", async () => {
const version = "0.1.14-channel.1";
const { detectBinaryPlatform } = await import("bailian-cli-core");
const { os, arch } = detectBinaryPlatform();
const zipName = binaryAssetFileName(version, os, arch);
const sumsSha = "deadbeef".repeat(8);
const previousFetch = globalThis.fetch;
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/SHA256SUMS")) {
return new Response(`${sumsSha} ${zipName}\n`, { status: 200 });
}
if (url.endsWith("/manifest.json")) {
return new Response(
JSON.stringify({
version: "9.9.9",
assets: {
[`${os}-${arch}`]: {
file: `bl-9.9.9-${os}-${arch}.zip`,
sha256: "aa".repeat(32),
url: "https://example.invalid/wrong.zip",
},
},
}),
{ status: 200 },
);
}
return new Response("not found", { status: 404 });
}) as typeof fetch;
try {
const spec = await resolveBinaryDownloadSpec(`v${version}`);
expect(spec.zipName).toBe(zipName);
expect(spec.url).toBe(releaseAssetUrl(version, zipName));
expect(spec.url).not.toContain("wrong.zip");
expect(spec.expectedSha).toBe(sumsSha);
} finally {
globalThis.fetch = previousFetch;
}
});
test("ensureBinaryPathEntries wires PATH entries through current", async () => {
await withTempBinaryRoots(async () => {
seedVersion("2.0.0");
await switchCurrentToVersion("2.0.0");
await ensureBinaryPathEntries("2.0.0");
if (process.platform === "win32") {
const binRoot = getBinaryBinRoot();
const target = readlinkSync(binRoot);
expect(target.replaceAll("/", "\\").toLowerCase()).toBe(
getBinaryCurrentPath().replaceAll("/", "\\").toLowerCase(),
);
} else {
const blLink = readlinkSync(join(getBinaryBinRoot(), "bl"));
const bailianLink = readlinkSync(join(getBinaryBinRoot(), "bailian"));
expect(blLink).toBe(join(getBinaryCurrentPath(), "bl"));
expect(bailianLink).toBe(join(getBinaryCurrentPath(), "bl"));
}
});
});
test("ensureWindowsBinJunction migrates a real bin directory via rename", async () => {
await withTempBinaryRoots(async () => {
seedVersion("3.0.0");
await switchCurrentToVersion("3.0.0");
const binRoot = getBinaryBinRoot();
mkdirSync(binRoot, { recursive: true });
writeFileSync(join(binRoot, "bl.exe"), "old-copy");
await ensureWindowsBinJunction(binRoot);
const target = readlinkSync(binRoot);
expect(target.replaceAll("\\", "/").toLowerCase()).toBe(
getBinaryCurrentPath().replaceAll("\\", "/").toLowerCase(),
);
expect(existsSync(`${binRoot}.migrating.${process.pid}`)).toBe(false);
});
});