From 60e2ccbd5ba9b4984fecd91631b41c83ac5b9bfb Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 15 Apr 2026 11:23:31 +0530 Subject: [PATCH] fix(update): preserve legacy downgrade verify --- src/infra/package-dist-inventory.ts | 16 ++++++-- src/infra/update-global.test.ts | 8 ++-- src/infra/update-global.ts | 60 ++++++++++++++++++++++------- 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/src/infra/package-dist-inventory.ts b/src/infra/package-dist-inventory.ts index e83640b1688..1375ff28c53 100644 --- a/src/infra/package-dist-inventory.ts +++ b/src/infra/package-dist-inventory.ts @@ -81,16 +81,24 @@ export async function readPackageDistInventory(packageRoot: string): Promise { - let expectedFiles: string[]; +export async function readPackageDistInventoryIfPresent( + packageRoot: string, +): Promise { 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 { + 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); diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts index c6f768f183d..8a9e2bd8b5d 100644 --- a/src/infra/update-global.test.ts +++ b/src/infra/update-global.test.ts @@ -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 | 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}`, ); }); }); diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index 45e568b1dcf..e87f22fdeee 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -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 ?? ""}`, ); } - 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 { +async function collectInstalledPackageDistErrors(packageRoot: string): Promise { + 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 { 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;