From 26db52ed69160941ff301268343cc94b9228cf56 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 00:42:52 +0100 Subject: [PATCH] build: restore qa lab updater sidecar --- package.json | 1 + scripts/openclaw-npm-postpublish-verify.ts | 1 + scripts/openclaw-npm-release-check.ts | 5 +++- scripts/postinstall-bundled-plugins.mjs | 13 +++++++---- scripts/release-check.ts | 10 +++++++- src/infra/npm-update-compat-sidecars.ts | 5 ++++ src/infra/package-dist-inventory.test.ts | 18 ++++++++++++--- src/infra/package-dist-inventory.ts | 6 +++-- src/infra/update-global.test.ts | 7 ++++++ test/openclaw-npm-release-check.test.ts | 12 +++++----- test/release-check.test.ts | 3 ++- .../postinstall-bundled-plugins.test.ts | 23 ++++++++++++++++--- 12 files changed, 83 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 110d2ff2d6f..0ffddf8841e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "!dist/extensions/qa-channel/**", "dist/extensions/qa-channel/runtime-api.js", "!dist/extensions/qa-lab/**", + "dist/extensions/qa-lab/runtime-api.js", "!dist/extensions/qa-matrix/**", "!dist/plugin-sdk/extensions/qa-lab/**", "!dist/plugin-sdk/qa-lab.*", diff --git a/scripts/openclaw-npm-postpublish-verify.ts b/scripts/openclaw-npm-postpublish-verify.ts index fe7a5733912..4da0d7caed9 100644 --- a/scripts/openclaw-npm-postpublish-verify.ts +++ b/scripts/openclaw-npm-postpublish-verify.ts @@ -46,6 +46,7 @@ const LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER = "Failed to load legacy context engine runtime."; const LEGACY_UPDATE_COMPAT_RUNTIME_SIDECAR_PATHS = [ "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", ] as const; const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = [ ...BUNDLED_RUNTIME_SIDECAR_PATHS.filter((relativePath) => diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index a0a5f744e1a..77473e7d09b 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -59,7 +59,10 @@ export type NpmDistTagMirrorAuth = { }; const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw"; const MAX_CALVER_DISTANCE_DAYS = 2; -const LEGACY_UPDATE_COMPAT_PACKED_PATHS = ["dist/extensions/qa-channel/runtime-api.js"] as const; +const LEGACY_UPDATE_COMPAT_PACKED_PATHS = [ + "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", +] as const; const REQUIRED_PACKED_PATHS = [ PACKAGE_DIST_INVENTORY_RELATIVE_PATH, "dist/control-ui/index.html", diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index 61fc18041a6..7d9621424d8 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -41,6 +41,12 @@ const LEGACY_UPDATE_COMPAT_SIDECARS = [ content: "// Compatibility stub for older OpenClaw updaters. The QA channel implementation is not packaged.\nexport {};\n", }, + { + path: "dist/extensions/qa-lab/runtime-api.js", + removedPrefix: "dist/extensions/qa-lab/", + content: + "// Compatibility stub for older OpenClaw updaters. The QA lab implementation is not packaged.\nexport {};\n", + }, ]; const BAILEYS_MEDIA_FILE = join( "node_modules", @@ -313,10 +319,9 @@ export function restoreLegacyUpdaterCompatSidecars(params = {}) { const restored = []; for (const sidecar of LEGACY_UPDATE_COMPAT_SIDECARS) { - // Older npm updater builds verify this exact sidecar after npm has already - // replaced the package. npm may remove stale QA Lab files before this - // postinstall hook runs, so this must be generated independently of prune - // results. The tarball and dist inventory still omit QA Lab. + // Older npm updater builds verify these exact sidecars after npm has + // already replaced the package, so generate them independently of prune + // results. const sidecarPath = join(packageRoot, sidecar.path); makeDirectory(dirname(sidecarPath), { recursive: true }); writeFile(sidecarPath, sidecar.content, "utf8"); diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 6af627ca8f5..a3d9857de6d 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -57,7 +57,13 @@ const requiredPathGroups = [ "dist/build-info.json", "dist/channel-catalog.json", "dist/control-ui/index.html", + "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", ]; +const legacyUpdateCompatPackPaths = new Set([ + "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", +]); const forbiddenPrefixes = [ "dist-runtime/", "dist/OpenClaw.app/", @@ -274,7 +280,9 @@ export function collectForbiddenPackPaths(paths: Iterable): string[] { return [...paths] .filter( (path) => - forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) || /node_modules\//.test(path), + !legacyUpdateCompatPackPaths.has(path) && + (forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) || + /node_modules\//.test(path)), ) .toSorted((left, right) => left.localeCompare(right)); } diff --git a/src/infra/npm-update-compat-sidecars.ts b/src/infra/npm-update-compat-sidecars.ts index eedb8a585d1..789311f74ac 100644 --- a/src/infra/npm-update-compat-sidecars.ts +++ b/src/infra/npm-update-compat-sidecars.ts @@ -4,6 +4,11 @@ export const NPM_UPDATE_COMPAT_SIDECARS = [ content: "// Compatibility stub for older OpenClaw updaters. The QA channel implementation is not packaged.\nexport {};\n", }, + { + path: "dist/extensions/qa-lab/runtime-api.js", + content: + "// Compatibility stub for older OpenClaw updaters. The QA lab implementation is not packaged.\nexport {};\n", + }, ] as const; export const NPM_UPDATE_COMPAT_SIDECAR_PATHS = new Set( diff --git a/src/infra/package-dist-inventory.test.ts b/src/infra/package-dist-inventory.test.ts index 6d67fd819fa..5fddaa9b99e 100644 --- a/src/infra/package-dist-inventory.test.ts +++ b/src/infra/package-dist-inventory.test.ts @@ -37,14 +37,22 @@ describe("package dist inventory", () => { it("keeps npm-omitted dist artifacts out of the inventory", async () => { await withTempDir({ prefix: "openclaw-dist-inventory-pack-" }, async (packageRoot) => { - const packagedQaRuntime = path.join( + const packagedQaChannelRuntime = path.join( packageRoot, "dist", "extensions", "qa-channel", "runtime-api.js", ); + const packagedQaLabRuntime = path.join( + packageRoot, + "dist", + "extensions", + "qa-lab", + "runtime-api.js", + ); const omittedQaChunk = path.join(packageRoot, "dist", "extensions", "qa-channel", "cli.js"); + const omittedQaLabChunk = path.join(packageRoot, "dist", "extensions", "qa-lab", "cli.js"); const omittedQaMatrixChunk = path.join( packageRoot, "dist", @@ -72,13 +80,16 @@ describe("package dist inventory", () => { "color-support", ); const omittedMap = path.join(packageRoot, "dist", "feature.runtime.js.map"); - await fs.mkdir(path.dirname(packagedQaRuntime), { recursive: true }); + await fs.mkdir(path.dirname(packagedQaChannelRuntime), { recursive: true }); + await fs.mkdir(path.dirname(packagedQaLabRuntime), { recursive: true }); await fs.mkdir(path.dirname(omittedQaMatrixChunk), { recursive: true }); await fs.mkdir(path.dirname(omittedQaLabTypes), { recursive: true }); await fs.mkdir(path.dirname(omittedExtensionNodeModuleSymlink), { recursive: true }); await fs.writeFile(path.join(packageRoot, "color-support.js"), "export {};\n", "utf8"); - await fs.writeFile(packagedQaRuntime, "export {};\n", "utf8"); + await fs.writeFile(packagedQaChannelRuntime, "export {};\n", "utf8"); + await fs.writeFile(packagedQaLabRuntime, "export {};\n", "utf8"); await fs.writeFile(omittedQaChunk, "export {};\n", "utf8"); + await fs.writeFile(omittedQaLabChunk, "export {};\n", "utf8"); await fs.writeFile(omittedQaMatrixChunk, "export {};\n", "utf8"); await fs.writeFile(omittedQaLabPluginSdk, "export {};\n", "utf8"); await fs.writeFile(omittedQaLabTypes, "export {};\n", "utf8"); @@ -91,6 +102,7 @@ describe("package dist inventory", () => { await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([ "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", ]); }); }); diff --git a/src/infra/package-dist-inventory.ts b/src/infra/package-dist-inventory.ts index d89bd0fca06..debd7c9c580 100644 --- a/src/infra/package-dist-inventory.ts +++ b/src/infra/package-dist-inventory.ts @@ -2,7 +2,10 @@ import fs from "node:fs/promises"; import path from "node:path"; export const PACKAGE_DIST_INVENTORY_RELATIVE_PATH = "dist/postinstall-inventory.json"; -const PACKAGED_QA_RUNTIME_PATHS = new Set(["dist/extensions/qa-channel/runtime-api.js"]); +const PACKAGED_QA_RUNTIME_PATHS = new Set([ + "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", +]); const OMITTED_QA_EXTENSION_PREFIXES = [ "dist/extensions/qa-channel/", "dist/extensions/qa-lab/", @@ -20,7 +23,6 @@ const OMITTED_PRIVATE_QA_PLUGIN_SDK_FILES = new Set([ const OMITTED_PRIVATE_QA_DIST_PREFIXES = ["dist/qa-runtime-"]; const OMITTED_DIST_SUBTREE_PATTERNS = [ /^dist\/extensions\/[^/]+\/node_modules(?:\/|$)/u, - /^dist\/extensions\/qa-lab(?:\/|$)/u, /^dist\/extensions\/qa-matrix(?:\/|$)/u, /^dist\/plugin-sdk\/extensions\/qa-lab(?:\/|$)/u, ] as const; diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts index 6d53ac1338e..6c266c47e82 100644 --- a/src/infra/update-global.test.ts +++ b/src/infra/update-global.test.ts @@ -32,6 +32,7 @@ import { const MATRIX_HELPER_API = bundledDistPluginFile("matrix", "helper-api.js"); const QA_CHANNEL_RUNTIME_API = bundledDistPluginFile("qa-channel", "runtime-api.js"); +const QA_LAB_RUNTIME_API = bundledDistPluginFile("qa-lab", "runtime-api.js"); describe("update global helpers", () => { let envSnapshot: ReturnType | undefined; @@ -427,6 +428,12 @@ describe("update global helpers", () => { await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain( `missing bundled runtime sidecar ${QA_CHANNEL_RUNTIME_API}`, ); + await fs.writeFile(path.join(packageRoot, QA_CHANNEL_RUNTIME_API), "export {};\n", "utf-8"); + + await fs.rm(path.join(packageRoot, QA_LAB_RUNTIME_API)); + await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain( + `missing bundled runtime sidecar ${QA_LAB_RUNTIME_API}`, + ); }); }); diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index e80099f9458..3d4f05221d4 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -22,7 +22,10 @@ import { } from "../scripts/openclaw-npm-release-check.ts"; import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH } from "../src/infra/package-dist-inventory.ts"; -const LEGACY_UPDATE_COMPAT_PACKED_PATHS = ["dist/extensions/qa-channel/runtime-api.js"] as const; +const LEGACY_UPDATE_COMPAT_PACKED_PATHS = [ + "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", +] as const; const REQUIRED_PACKED_PATHS = [ PACKAGE_DIST_INVENTORY_RELATIVE_PATH, ...LEGACY_UPDATE_COMPAT_PACKED_PATHS, @@ -341,7 +344,6 @@ describe("collectForbiddenPackedPathErrors", () => { ]), ).toEqual([ 'npm package must not include private QA channel artifact "dist/extensions/qa-channel/package.json".', - 'npm package must not include private QA lab artifact "dist/extensions/qa-lab/runtime-api.js".', 'npm package must not include private QA lab artifact "dist/extensions/qa-lab/src/cli.js".', 'npm package must not include private QA lab type artifact "dist/plugin-sdk/extensions/qa-lab/cli.d.ts".', 'npm package must not include private QA runtime chunk "dist/qa-runtime-B9LDtssJ.js".', @@ -349,15 +351,13 @@ describe("collectForbiddenPackedPathErrors", () => { ]); }); - it("allows only the legacy update verifier QA channel runtime sidecar", () => { + it("allows legacy update verifier QA runtime sidecars", () => { expect( collectForbiddenPackedPathErrors([ "dist/extensions/qa-channel/runtime-api.js", "dist/extensions/qa-lab/runtime-api.js", ]), - ).toEqual([ - 'npm package must not include private QA lab artifact "dist/extensions/qa-lab/runtime-api.js".', - ]); + ).toEqual([]); }); it("rejects root dist chunks that still reference the private qa lab", () => { diff --git a/test/release-check.test.ts b/test/release-check.test.ts index 2004659a6ff..7724941e554 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -322,7 +322,6 @@ describe("collectForbiddenPackPaths", () => { "qa/scenarios/index.md", ]), ).toEqual([ - "dist/extensions/qa-lab/runtime-api.js", "dist/plugin-sdk/extensions/qa-lab/cli.d.ts", "dist/plugin-sdk/qa-lab.js", "dist/plugin-sdk/qa-runtime.js", @@ -392,6 +391,8 @@ describe("collectMissingPackPaths", () => { "dist/index.js", "dist/entry.js", "dist/control-ui/index.html", + "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", "dist/extensions/acpx/mcp-proxy.mjs", bundledDistPluginFile("diffs", "assets/viewer-runtime.js"), ...requiredBundledPluginPackPaths, diff --git a/test/scripts/postinstall-bundled-plugins.test.ts b/test/scripts/postinstall-bundled-plugins.test.ts index ff8b44bd658..e89ed149adf 100644 --- a/test/scripts/postinstall-bundled-plugins.test.ts +++ b/test/scripts/postinstall-bundled-plugins.test.ts @@ -215,7 +215,7 @@ describe("bundled plugin postinstall", () => { await expect(fs.stat(staleFile)).rejects.toMatchObject({ code: "ENOENT" }); }); - it("restores only postinstall-generated QA lab compat sidecar after pruning old installs", async () => { + it("restores only postinstall-generated QA compat sidecars after pruning old installs", async () => { const packageRoot = await createTempDirAsync("openclaw-packaged-install-qa-compat-"); const currentFile = path.join(packageRoot, "dist", "entry.js"); const stalePackage = path.join(packageRoot, "dist", "extensions", "qa-lab", "package.json"); @@ -246,9 +246,12 @@ describe("bundled plugin postinstall", () => { "utf8", ), ).resolves.toContain("QA channel implementation is not packaged"); + await expect( + fs.readFile(path.join(packageRoot, "dist", "extensions", "qa-lab", "runtime-api.js"), "utf8"), + ).resolves.toContain("QA lab implementation is not packaged"); }); - it("creates only an empty QA channel compat sidecar for fresh installs", async () => { + it("creates only empty QA compat sidecars for fresh installs", async () => { const packageRoot = await createTempDirAsync("openclaw-packaged-install-no-qa-compat-"); await fs.mkdir(path.join(packageRoot, "dist"), { recursive: true }); await fs.writeFile(path.join(packageRoot, "dist", "entry.js"), "export {};\n"); @@ -260,7 +263,10 @@ describe("bundled plugin postinstall", () => { removedFiles: ["dist/entry-old.js"], log: { log: vi.fn(), warn: vi.fn() }, }), - ).toEqual(["dist/extensions/qa-channel/runtime-api.js"]); + ).toEqual([ + "dist/extensions/qa-channel/runtime-api.js", + "dist/extensions/qa-lab/runtime-api.js", + ]); await expect( fs.readFile( @@ -270,12 +276,23 @@ describe("bundled plugin postinstall", () => { ).resolves.toBe( "// Compatibility stub for older OpenClaw updaters. The QA channel implementation is not packaged.\nexport {};\n", ); + await expect( + fs.readFile(path.join(packageRoot, "dist", "extensions", "qa-lab", "runtime-api.js"), "utf8"), + ).resolves.toBe( + "// Compatibility stub for older OpenClaw updaters. The QA lab implementation is not packaged.\nexport {};\n", + ); await expect( fs.stat(path.join(packageRoot, "dist", "extensions", "qa-channel", "package.json")), ).rejects.toMatchObject({ code: "ENOENT" }); await expect( fs.stat(path.join(packageRoot, "dist", "extensions", "qa-channel", "openclaw.plugin.json")), ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + fs.stat(path.join(packageRoot, "dist", "extensions", "qa-lab", "package.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + fs.stat(path.join(packageRoot, "dist", "extensions", "qa-lab", "openclaw.plugin.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("keeps packaged postinstall non-fatal when the dist inventory is missing", async () => {