mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
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:
@@ -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")
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user