Add packed CLI smoke checks for release packaging (#70685)

* Add packed CLI smoke release checks

* Address PR review feedback

* Harden packed CLI smoke checks

* Tighten release verifier parsing

* Scan root dist module files in release verifier
This commit is contained in:
Tak Hoffman
2026-04-23 13:50:15 -05:00
committed by GitHub
parent c151956782
commit 7651a03424
4 changed files with 521 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ import {
realpathSync,
rmSync,
} from "node:fs";
import { builtinModules } from "node:module";
import { tmpdir } from "node:os";
import { isAbsolute, join, relative } from "node:path";
import { pathToFileURL } from "node:url";
@@ -20,9 +21,11 @@ import {
collectBundledPluginRootRuntimeMirrorErrors,
collectRootDistBundledRuntimeMirrors,
collectRuntimeDependencySpecs,
packageNameFromSpecifier,
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs";
import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
import { createRequire } from "node:module";
type InstalledPackageJson = {
version?: string;
@@ -47,6 +50,13 @@ const LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER =
const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = BUNDLED_RUNTIME_SIDECAR_PATHS.filter(
(relativePath) => listBundledPluginPackArtifacts().includes(relativePath),
);
const NODE_BUILTIN_MODULES = new Set(builtinModules.map((name) => name.replace(/^node:/u, "")));
const MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES = 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_BYTES = 2 * 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_FILES = 5000;
const ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE = /\.(?:c|m)?js$/u;
const require = createRequire(import.meta.url);
const acorn = require("acorn") as typeof import("acorn");
export type PublishedInstallScenario = {
name: string;
@@ -101,6 +111,7 @@ export function collectInstalledPackageErrors(params: {
}
errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot));
errors.push(...collectInstalledRootDependencyManifestErrors(params.packageRoot));
errors.push(...collectInstalledMirroredRootDependencyManifestErrors(params.packageRoot));
return errors;
@@ -131,7 +142,7 @@ function listDistJavaScriptFiles(packageRoot: string): string[] {
pending.push(entryPath);
continue;
}
if (entry.isFile() && entry.name.endsWith(".js")) {
if (entry.isFile() && ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE.test(entry.name)) {
files.push(entryPath);
}
}
@@ -154,6 +165,183 @@ export function collectInstalledContextEngineRuntimeErrors(packageRoot: string):
return errors;
}
function listInstalledRootDistJavaScriptFiles(packageRoot: string): string[] {
const distDir = join(packageRoot, "dist");
if (!existsSync(distDir)) {
return [];
}
const pending = [distDir];
const files: string[] = [];
while (pending.length > 0) {
const currentDir = pending.pop();
if (!currentDir) {
continue;
}
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
const entryPath = join(currentDir, entry.name);
const relativePath = relative(distDir, entryPath).replaceAll("\\", "/");
if (relativePath.startsWith("extensions/")) {
continue;
}
if (entry.isDirectory()) {
pending.push(entryPath);
continue;
}
if (entry.isFile() && ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE.test(entry.name)) {
files.push(entryPath);
}
}
}
return files;
}
type ParsedImportSpecifiersResult =
| { ok: true; specifiers: Set<string> }
| { ok: false; error: string };
function extractLiteralSpecifier(node: unknown): string | null {
if (!node || typeof node !== "object") {
return null;
}
const candidate = node as { type?: string; value?: unknown };
if (candidate.type === "Literal" && typeof candidate.value === "string") {
return candidate.value;
}
return null;
}
function extractJavaScriptImportSpecifiers(source: string): ParsedImportSpecifiersResult {
const specifiers = new Set<string>();
let program: unknown;
try {
program = acorn.parse(source, {
allowHashBang: true,
ecmaVersion: "latest",
sourceType: "module",
});
} catch (error) {
return { ok: false, error: formatErrorMessage(error) };
}
const visited = new Set<unknown>();
const pending: unknown[] = [program];
while (pending.length > 0) {
const current = pending.pop();
if (!current || typeof current !== "object" || visited.has(current)) {
continue;
}
visited.add(current);
const node = current as Record<string, unknown>;
const nodeType = typeof node.type === "string" ? node.type : null;
if (nodeType === "ImportDeclaration") {
const specifier = extractLiteralSpecifier(node.source);
if (specifier) {
specifiers.add(specifier);
}
} else if (nodeType === "ExportAllDeclaration" || nodeType === "ExportNamedDeclaration") {
const specifier = extractLiteralSpecifier(node.source);
if (specifier) {
specifiers.add(specifier);
}
} else if (nodeType === "ImportExpression") {
const specifier = extractLiteralSpecifier(node.source);
if (specifier) {
specifiers.add(specifier);
}
} else if (nodeType === "CallExpression") {
const callee = node.callee as { type?: string; name?: string } | undefined;
const args = Array.isArray(node.arguments) ? node.arguments : [];
if (callee?.type === "Identifier" && callee.name === "require" && args.length === 1) {
const specifier = extractLiteralSpecifier(args[0]);
if (specifier) {
specifiers.add(specifier);
}
}
}
for (const value of Object.values(node)) {
if (Array.isArray(value)) {
pending.push(...value);
} else if (value && typeof value === "object") {
pending.push(value);
}
}
}
return { ok: true, specifiers };
}
export function collectInstalledRootDependencyManifestErrors(packageRoot: string): string[] {
const packageJsonPath = join(packageRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return ["installed package is missing package.json."];
}
const packageJsonStat = lstatSync(packageJsonPath);
if (!packageJsonStat.isFile() || packageJsonStat.size > MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES) {
return [
`installed package.json is invalid or exceeds ${MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES} bytes.`,
];
}
let rootPackageJson: InstalledPackageJson;
try {
rootPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as InstalledPackageJson;
} catch (error) {
return [`installed package.json could not be parsed: ${formatErrorMessage(error)}.`];
}
const declaredRuntimeDeps = new Set([
...Object.keys(rootPackageJson.dependencies ?? {}),
...Object.keys(rootPackageJson.optionalDependencies ?? {}),
]);
const distFiles = listInstalledRootDistJavaScriptFiles(packageRoot);
if (distFiles.length > MAX_INSTALLED_ROOT_DIST_JS_FILES) {
return [
`installed package root dist contains ${distFiles.length} JavaScript files, exceeding the ${MAX_INSTALLED_ROOT_DIST_JS_FILES} file scan limit.`,
];
}
const missingImporters = new Map<string, Set<string>>();
for (const filePath of distFiles) {
const fileStat = lstatSync(filePath);
if (!fileStat.isFile() || fileStat.size > MAX_INSTALLED_ROOT_DIST_JS_BYTES) {
const relativePath = relative(join(packageRoot, "dist"), filePath).replaceAll("\\", "/");
return [
`installed package root dist file '${relativePath}' is invalid or exceeds ${MAX_INSTALLED_ROOT_DIST_JS_BYTES} bytes.`,
];
}
const source = readFileSync(filePath, "utf8");
const relativePath = relative(join(packageRoot, "dist"), filePath).replaceAll("\\", "/");
const parsedSpecifiers = extractJavaScriptImportSpecifiers(source);
if (!parsedSpecifiers.ok) {
return [
`installed package root dist file '${relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
];
}
for (const specifier of parsedSpecifiers.specifiers) {
const dependencyName = packageNameFromSpecifier(specifier);
if (
!dependencyName ||
NODE_BUILTIN_MODULES.has(dependencyName) ||
declaredRuntimeDeps.has(dependencyName)
) {
continue;
}
const importers = missingImporters.get(dependencyName) ?? new Set<string>();
importers.add(relativePath);
missingImporters.set(dependencyName, importers);
}
}
return [...missingImporters.entries()]
.map(([dependencyName, importers]) => {
const importerList = [...importers].toSorted((left, right) => left.localeCompare(right));
return `installed package root is missing declared runtime dependency '${dependencyName}' for dist importers: ${importerList.join(", ")}. Add it to package.json dependencies/optionalDependencies.`;
})
.toSorted((left, right) => left.localeCompare(right));
}
export function resolveInstalledBinaryPath(prefixDir: string, platform = process.platform): string {
return platform === "win32"
? join(prefixDir, "openclaw.cmd")

View File

@@ -11,7 +11,7 @@ import {
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
@@ -38,6 +38,7 @@ import {
import { discoverBundledPluginRuntimeDeps } from "./postinstall-bundled-plugins.mjs";
import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs";
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts";
export {
@@ -92,6 +93,13 @@ const forbiddenPrivateQaContentScanPrefixes = ["dist/"] as const;
const appcastPath = resolve("appcast.xml");
const laneBuildMin = 1_000_000_000;
const laneFloorAdoptionDateKey = 20260227;
const SAFE_UNIX_SMOKE_PATH = "/usr/bin:/bin";
export const PACKED_CLI_SMOKE_COMMANDS = [
["--help"],
["status", "--json", "--timeout", "1"],
["config", "schema"],
["models", "list", "--provider", "amazon-bedrock"],
] as const;
function collectBundledExtensions(): BundledExtension[] {
const extensionsDir = resolve("extensions");
@@ -209,6 +217,12 @@ function resolveGlobalRoot(prefixDir: string, cwd: string): string {
}).trim();
}
function resolveInstalledBinaryPath(prefixDir: string): string {
return process.platform === "win32"
? join(prefixDir, "openclaw.cmd")
: join(prefixDir, "bin", "openclaw");
}
export function createPackedBundledPluginPostinstallEnv(
env: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
@@ -218,6 +232,52 @@ export function createPackedBundledPluginPostinstallEnv(
};
}
export function createPackedCliSmokeEnv(
env: NodeJS.ProcessEnv,
overrides: NodeJS.ProcessEnv = {},
): NodeJS.ProcessEnv {
const allowlistedEnvEntries = [
"HOME",
"TMPDIR",
"TMP",
"TEMP",
"SystemRoot",
"ComSpec",
"PATHEXT",
"WINDIR",
] as const;
const windowsRoot = env.SystemRoot ?? env.WINDIR ?? "C:\\Windows";
const nodeBinDir = dirname(process.execPath);
const trustedCmdPath = join(windowsRoot, "System32", "cmd.exe");
const safePath =
process.platform === "win32"
? `${nodeBinDir};${windowsRoot}\\System32;${windowsRoot}`
: `${nodeBinDir}:${SAFE_UNIX_SMOKE_PATH}`;
const homeDir = overrides.HOME ?? env.HOME ?? overrides.USERPROFILE ?? env.USERPROFILE ?? "";
return {
...Object.fromEntries(
allowlistedEnvEntries.flatMap((key) => {
const value = env[key];
return typeof value === "string" && value.length > 0 ? [[key, value]] : [];
}),
),
PATH: safePath,
HOME: homeDir,
USERPROFILE: homeDir,
ComSpec: trustedCmdPath,
APPDATA: homeDir ? join(homeDir, "AppData", "Roaming") : undefined,
LOCALAPPDATA: homeDir ? join(homeDir, "AppData", "Local") : undefined,
AWS_EC2_METADATA_DISABLED: "true",
AWS_SHARED_CREDENTIALS_FILE: homeDir ? join(homeDir, ".aws", "credentials") : undefined,
AWS_CONFIG_FILE: homeDir ? join(homeDir, ".aws", "config") : undefined,
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
OPENCLAW_NO_ONBOARD: "1",
OPENCLAW_SUPPRESS_NOTES: "1",
...overrides,
};
}
function runPackedBundledPluginPostinstall(packageRoot: string): void {
execFileSync(process.execPath, [join(packageRoot, "scripts/postinstall-bundled-plugins.mjs")], {
cwd: packageRoot,
@@ -378,6 +438,41 @@ function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: str
}
}
function runPackedCliSmoke(params: {
prefixDir: string;
cwd: string;
homeDir: string;
stateDir: string;
}): void {
const binaryPath = resolveInstalledBinaryPath(params.prefixDir);
const env = createPackedCliSmokeEnv(process.env, {
HOME: params.homeDir,
OPENCLAW_STATE_DIR: params.stateDir,
OPENAI_API_KEY: "sk-openclaw-release-check",
});
const windowsRoot = env.SystemRoot ?? env.WINDIR ?? "C:\\Windows";
const trustedCmdPath = join(windowsRoot, "System32", "cmd.exe");
for (const args of PACKED_CLI_SMOKE_COMMANDS) {
if (process.platform === "win32") {
execFileSync(trustedCmdPath, ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, [...args])], {
cwd: params.cwd,
stdio: "inherit",
env,
shell: false,
windowsVerbatimArguments: true,
});
continue;
}
execFileSync(binaryPath, [...args], {
cwd: params.cwd,
stdio: "inherit",
env,
shell: false,
});
}
}
function runPackedBundledChannelEntrySmoke(): void {
const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-"));
try {
@@ -390,6 +485,15 @@ function runPackedBundledChannelEntrySmoke(): void {
installPackedTarball(prefixDir, tarballPath, tmpRoot);
const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw");
const homeDir = join(tmpRoot, "home");
const stateDir = join(tmpRoot, "state");
mkdirSync(homeDir, { recursive: true });
runPackedCliSmoke({
prefixDir,
cwd: packageRoot,
homeDir,
stateDir,
});
runPackedBundledPluginPostinstall(packageRoot);
runPackedBundledPluginActivationSmoke(packageRoot, tmpRoot);
execFileSync(
@@ -408,9 +512,6 @@ function runPackedBundledChannelEntrySmoke(): void {
},
);
const homeDir = join(tmpRoot, "home");
const stateDir = join(tmpRoot, "state");
mkdirSync(homeDir, { recursive: true });
execFileSync(
process.execPath,
[join(packageRoot, "openclaw.mjs"), "completion", "--write-state"],

View File

@@ -7,6 +7,7 @@ import {
buildPublishedInstallCommandArgs,
buildPublishedInstallScenarios,
collectInstalledContextEngineRuntimeErrors,
collectInstalledRootDependencyManifestErrors,
collectInstalledMirroredRootDependencyManifestErrors,
collectInstalledPackageErrors,
normalizeInstalledBinaryVersion,
@@ -419,3 +420,179 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
}
});
});
describe("collectInstalledRootDependencyManifestErrors", () => {
function makeInstalledPackageRoot(): string {
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-root-deps-"));
}
function writePackageFile(root: string, relativePath: string, value: unknown): void {
const fullPath = join(root, relativePath);
mkdirSync(dirname(fullPath), { recursive: true });
writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
it("flags root dist imports whose declared runtime package name is missing", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "typebox-CXXonh2u.js"),
'import { Type } from "typebox";\nexport { Type };\n',
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package root is missing declared runtime dependency 'typebox' for dist importers: typebox-CXXonh2u.js. Add it to package.json dependencies/optionalDependencies.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("accepts root dist imports when the runtime package name is declared", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.22",
dependencies: {
typebox: "1.1.28",
},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "typebox-CXXonh2u.js"),
'import { Type } from "typebox";\nexport { Type };\n',
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("flags undeclared imports from mjs and cjs root dist files", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "esm-entry.mjs"),
'export { value } from "mjs-only";\n',
"utf8",
);
writeFileSync(
join(packageRoot, "dist", "cjs-entry.cjs"),
'const cjsOnly = require("cjs-only");\nmodule.exports = cjsOnly;\n',
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package root is missing declared runtime dependency 'cjs-only' for dist importers: cjs-entry.cjs. Add it to package.json dependencies/optionalDependencies.",
"installed package root is missing declared runtime dependency 'mjs-only' for dist importers: esm-entry.mjs. Add it to package.json dependencies/optionalDependencies.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("ignores import-like text inside comments", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "comment-only.js"),
[
'// import "fake-package";',
'/* require("fake-package-two"); */',
"export const ok = true;",
"",
].join("\n"),
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("ignores import-like text inside string literals", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "string-only.js"),
[
'export const help = "run import(\'fake-package\') after setup";',
'export const note = "from \\"fake-package-two\\"";',
"",
].join("\n"),
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("returns a structured error when installed package.json is invalid", () => {
const packageRoot = makeInstalledPackageRoot();
try {
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(join(packageRoot, "package.json"), "{not-json\n", "utf8");
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
expect.stringMatching(/^installed package\.json could not be parsed:/u),
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("refuses oversized root dist files", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "oversized.js"),
"x".repeat(2 * 1024 * 1024 + 1),
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package root dist file 'oversized.js' is invalid or exceeds 2097152 bytes.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
});

View File

@@ -1,6 +1,6 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
import { listBundledPluginPackArtifacts } from "../scripts/lib/bundled-plugin-build-entries.mjs";
import { listPluginSdkDistArtifacts } from "../scripts/lib/plugin-sdk-entries.mjs";
@@ -15,7 +15,9 @@ import {
collectForbiddenPackPaths,
collectMissingPackPaths,
collectPackUnpackedSizeErrors,
createPackedCliSmokeEnv,
createPackedBundledPluginPostinstallEnv,
PACKED_CLI_SMOKE_COMMANDS,
packageNameFromSpecifier,
} from "../scripts/release-check.ts";
import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH } from "../src/infra/package-dist-inventory.ts";
@@ -54,6 +56,53 @@ describe("collectAppcastSparkleVersionErrors", () => {
});
});
describe("packed CLI smoke", () => {
it("keeps the expected packaged CLI smoke command list", () => {
expect(PACKED_CLI_SMOKE_COMMANDS).toEqual([
["--help"],
["status", "--json", "--timeout", "1"],
["config", "schema"],
["models", "list", "--provider", "amazon-bedrock"],
]);
});
it("builds a packed CLI smoke env with packaged-install guardrails", () => {
expect(
createPackedCliSmokeEnv(
{
PATH: "/usr/bin",
HOME: "/tmp/original-home",
USERPROFILE: "/tmp/original-profile",
TMPDIR: "/tmp/original-tmp",
SystemRoot: "C:\\Windows",
GITHUB_TOKEN: "redacted",
OPENAI_API_KEY: "real-secret",
},
{ HOME: "/tmp/smoke-home", OPENCLAW_STATE_DIR: "/tmp/smoke-state" },
),
).toEqual({
PATH:
process.platform === "win32"
? `${dirname(process.execPath)};C:\\Windows\\System32;C:\\Windows`
: `${dirname(process.execPath)}:/usr/bin:/bin`,
HOME: "/tmp/smoke-home",
USERPROFILE: "/tmp/smoke-home",
ComSpec: "C:\\Windows/System32/cmd.exe",
APPDATA: "/tmp/smoke-home/AppData/Roaming",
LOCALAPPDATA: "/tmp/smoke-home/AppData/Local",
AWS_EC2_METADATA_DISABLED: "true",
AWS_SHARED_CREDENTIALS_FILE: "/tmp/smoke-home/.aws/credentials",
AWS_CONFIG_FILE: "/tmp/smoke-home/.aws/config",
TMPDIR: "/tmp/original-tmp",
SystemRoot: "C:\\Windows",
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
OPENCLAW_NO_ONBOARD: "1",
OPENCLAW_SUPPRESS_NOTES: "1",
OPENCLAW_STATE_DIR: "/tmp/smoke-state",
});
});
});
describe("collectBundledExtensionManifestErrors", () => {
it("flags invalid bundled extension install metadata", () => {
expect(