build: restore qa lab updater sidecar

This commit is contained in:
Peter Steinberger
2026-04-17 00:42:52 +01:00
parent 0c5bdbde89
commit 26db52ed69
12 changed files with 83 additions and 21 deletions

View File

@@ -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.*",

View File

@@ -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) =>

View File

@@ -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",

View File

@@ -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");

View File

@@ -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>): 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));
}

View File

@@ -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<string>(

View File

@@ -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",
]);
});
});

View File

@@ -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;

View File

@@ -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<typeof captureEnv> | 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}`,
);
});
});

View File

@@ -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", () => {

View File

@@ -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,

View File

@@ -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 () => {