fix(update): preserve legacy downgrade verify

This commit is contained in:
Ayaan Zaidi
2026-04-15 11:23:31 +05:30
parent aaa6b05f3b
commit 60e2ccbd5b
3 changed files with 63 additions and 21 deletions

View File

@@ -81,16 +81,24 @@ export async function readPackageDistInventory(packageRoot: string): Promise<str
);
}
export async function collectPackageDistInventoryErrors(packageRoot: string): Promise<string[]> {
let expectedFiles: string[];
export async function readPackageDistInventoryIfPresent(
packageRoot: string,
): Promise<string[] | null> {
try {
expectedFiles = await readPackageDistInventory(packageRoot);
return await readPackageDistInventory(packageRoot);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return [`missing package dist inventory ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`];
return null;
}
throw error;
}
}
export async function collectPackageDistInventoryErrors(packageRoot: string): Promise<string[]> {
const expectedFiles = await readPackageDistInventoryIfPresent(packageRoot);
if (expectedFiles === null) {
return [`missing package dist inventory ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`];
}
const actualFiles = await collectPackageDistInventory(packageRoot);
const expectedSet = new Set(expectedFiles);

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "../../scripts/lib/npm-update-compat-sidecars.mjs";
import { bundledDistPluginFile } from "../../test/helpers/bundled-plugin-paths.js";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
@@ -27,6 +28,7 @@ import {
} from "./update-global.js";
const MATRIX_HELPER_API = bundledDistPluginFile("matrix", "helper-api.js");
const QA_CHANNEL_RUNTIME_API = bundledDistPluginFile("qa-channel", "runtime-api.js");
describe("update global helpers", () => {
let envSnapshot: ReturnType<typeof captureEnv> | undefined;
@@ -405,7 +407,7 @@ describe("update global helpers", () => {
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
for (const relativePath of BUNDLED_RUNTIME_SIDECAR_PATHS) {
for (const relativePath of NPM_UPDATE_COMPAT_SIDECAR_PATHS) {
const absolutePath = path.join(packageRoot, relativePath);
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
@@ -413,9 +415,9 @@ describe("update global helpers", () => {
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
await fs.rm(path.join(packageRoot, MATRIX_HELPER_API));
await fs.rm(path.join(packageRoot, QA_CHANNEL_RUNTIME_API));
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
`missing bundled runtime sidecar ${MATRIX_HELPER_API}`,
`missing bundled runtime sidecar ${QA_CHANNEL_RUNTIME_API}`,
);
});
});

View File

@@ -2,12 +2,12 @@ import fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.js";
import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "../../scripts/lib/npm-update-compat-sidecars.mjs";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { pathExists } from "../utils.js";
import {
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
collectPackageDistInventoryErrors,
collectPackageDistInventory,
readPackageDistInventoryIfPresent,
} from "./package-dist-inventory.js";
import { readPackageVersion } from "./package-json.js";
import { applyPathPrepend } from "./path-prepend.js";
@@ -39,7 +39,6 @@ const NPM_GLOBAL_INSTALL_OMIT_OPTIONAL_FLAGS = [
"--omit=optional",
...NPM_GLOBAL_INSTALL_QUIET_FLAGS,
] as const;
const MISSING_PACKAGE_DIST_INVENTORY_ERROR = `missing package dist inventory ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`;
function normalizePackageTarget(value: string): string {
return value.trim();
@@ -94,20 +93,53 @@ export async function collectInstalledGlobalPackageErrors(params: {
`expected installed version ${params.expectedVersion}, found ${installedVersion ?? "<missing>"}`,
);
}
const distErrors = await collectPackageDistInventoryErrors(params.packageRoot);
if (distErrors.length === 1 && distErrors[0] === MISSING_PACKAGE_DIST_INVENTORY_ERROR) {
errors.push(...(await collectLegacyInstalledGlobalPackageErrors(params.packageRoot)));
return errors;
}
errors.push(...distErrors);
errors.push(...(await collectInstalledPackageDistErrors(params.packageRoot)));
return errors;
}
async function collectLegacyInstalledGlobalPackageErrors(packageRoot: string): Promise<string[]> {
async function collectInstalledPackageDistErrors(packageRoot: string): Promise<string[]> {
const inventoryFiles = await readPackageDistInventoryIfPresent(packageRoot);
if (inventoryFiles !== null) {
return await collectInstalledPathErrors({
packageRoot,
expectedFiles: inventoryFiles,
actualFiles: await collectPackageDistInventory(packageRoot),
missingMessage: (relativePath) => `missing packaged dist file ${relativePath}`,
unexpectedMessage: (relativePath) => `unexpected packaged dist file ${relativePath}`,
});
}
return await collectInstalledPathErrors({
packageRoot,
expectedFiles: [...NPM_UPDATE_COMPAT_SIDECAR_PATHS],
actualFiles: null,
missingMessage: (relativePath) => `missing bundled runtime sidecar ${relativePath}`,
});
}
async function collectInstalledPathErrors(params: {
packageRoot: string;
expectedFiles: string[];
actualFiles: string[] | null;
missingMessage: (relativePath: string) => string;
unexpectedMessage?: ((relativePath: string) => string) | undefined;
}): Promise<string[]> {
const errors: string[] = [];
for (const relativePath of BUNDLED_RUNTIME_SIDECAR_PATHS) {
if (!(await pathExists(path.join(packageRoot, relativePath)))) {
errors.push(`missing bundled runtime sidecar ${relativePath}`);
const actualSet = params.actualFiles ? new Set(params.actualFiles) : null;
for (const relativePath of params.expectedFiles) {
const exists =
actualSet !== null
? actualSet.has(relativePath)
: await pathExists(path.join(params.packageRoot, relativePath));
if (!exists) {
errors.push(params.missingMessage(relativePath));
}
}
if (actualSet !== null && params.unexpectedMessage) {
const expectedSet = new Set(params.expectedFiles);
for (const relativePath of params.actualFiles ?? []) {
if (!expectedSet.has(relativePath)) {
errors.push(params.unexpectedMessage(relativePath));
}
}
}
return errors;