From 7651a03424fad151fdda1dfe21f8b408626e780e Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:50:15 -0500 Subject: [PATCH] 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 --- scripts/openclaw-npm-postpublish-verify.ts | 190 ++++++++++++++++++- scripts/release-check.ts | 109 ++++++++++- test/openclaw-npm-postpublish-verify.test.ts | 177 +++++++++++++++++ test/release-check.test.ts | 51 ++++- 4 files changed, 521 insertions(+), 6 deletions(-) diff --git a/scripts/openclaw-npm-postpublish-verify.ts b/scripts/openclaw-npm-postpublish-verify.ts index a74dc7390ae..fab26bc2416 100644 --- a/scripts/openclaw-npm-postpublish-verify.ts +++ b/scripts/openclaw-npm-postpublish-verify.ts @@ -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 } + | { 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(); + 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(); + 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; + 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>(); + + 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(); + 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") diff --git a/scripts/release-check.ts b/scripts/release-check.ts index c505229bd74..6c2efcdbfb4 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -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"], diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 3844be14e3a..b394d7ca673 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -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 }); + } + }); +}); diff --git a/test/release-check.test.ts b/test/release-check.test.ts index c0331a38251..61c1022f386 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -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(