fix(package): prevent installs from deleting required runtime modules (#113821)

* fix(package): preserve imported runtime chunks after install

* fix(package): isolate public build from private QA

* test(package): prove installed postinstall dependency graph
This commit is contained in:
Peter Steinberger
2026-07-25 14:05:41 -07:00
committed by GitHub
parent 7490560c67
commit d5a3740707
16 changed files with 299 additions and 108 deletions
+2 -3
View File
@@ -1683,10 +1683,7 @@ jobs:
OPENCLAW_BUILD_PRIVATE_QA: "1"
NODE_OPTIONS: --max-old-space-size=8192
run: |
node scripts/build-all.mjs qaRuntime
pnpm ui:build
package_args=(
--skip-build
--output-dir .artifacts/qa-e2e/smoke-ci-package
--output-name openclaw-current.tgz
)
@@ -1694,6 +1691,8 @@ jobs:
package_args=(--allow-unreleased-changelog "${package_args[@]}")
fi
node scripts/package-openclaw-for-docker.mjs "${package_args[@]}"
node scripts/build-all.mjs qaRuntime
pnpm ui:build
- name: Run smoke profile part
env:
+1
View File
@@ -76,6 +76,7 @@ COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs scripts/prepare-git-hooks.mjs ./scripts/
COPY scripts/lib/guard-inventory-utils.mjs ./scripts/lib/guard-inventory-utils.mjs
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
COPY --from=workspace-deps /out/packages/ ./packages/
+1
View File
@@ -347,6 +347,7 @@
"scripts/lib/official-external-channel-catalog.json",
"scripts/lib/official-external-plugin-catalog.json",
"scripts/lib/official-external-provider-catalog.json",
"scripts/lib/guard-inventory-utils.mjs",
"scripts/lib/package-dist-imports.mjs",
"scripts/lib/recommended-tool-installs.json",
"scripts/postinstall-bundled-plugins.mjs",
+1
View File
@@ -20,6 +20,7 @@ COPY packages ./packages
COPY extensions ./extensions
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/prepare-git-hooks.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts/lib/guard-inventory-utils.mjs ./scripts/lib/guard-inventory-utils.mjs
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
corepack enable \
+10 -2
View File
@@ -6,8 +6,16 @@ export function resolveRepoSpecifier(
specifier: unknown,
importerFile: unknown,
): string | null;
/** Visit static and dynamic module specifiers in a parsed TypeScript source file. */
export function visitModuleSpecifiers(ts: unknown, sourceFile: unknown, visit: unknown): void;
/** Visit module specifiers, optionally including packaged-runtime dependencies. */
export function visitModuleSpecifiers(
ts: unknown,
sourceFile: unknown,
visit: unknown,
options?: {
includeCommonJs?: boolean;
includeImportMetaUrl?: boolean;
},
): void;
/** Diff expected and actual inventory entries using JSON identity. */
export function diffInventoryEntries(
expected: unknown,
+34 -1
View File
@@ -22,7 +22,7 @@ export function resolveRepoSpecifier(repoRoot, specifier, importerFile) {
}
/** Visit static and dynamic module specifiers in a parsed TypeScript source file. */
export function visitModuleSpecifiers(ts, sourceFile, visit) {
export function visitModuleSpecifiers(ts, sourceFile, visit, options = {}) {
function walk(node) {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
visit({
@@ -54,6 +54,39 @@ export function visitModuleSpecifiers(ts, sourceFile, visit) {
specifier: node.arguments[0].text,
specifierNode: node.arguments[0],
});
} else if (
options.includeCommonJs &&
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === "require" &&
node.arguments.length === 1 &&
ts.isStringLiteralLike(node.arguments[0])
) {
visit({
kind: "commonjs-require",
node,
specifier: node.arguments[0].text,
specifierNode: node.arguments[0],
});
} else if (
options.includeImportMetaUrl &&
ts.isNewExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === "URL" &&
node.arguments?.length >= 2 &&
ts.isStringLiteralLike(node.arguments[0]) &&
ts.isPropertyAccessExpression(node.arguments[1]) &&
node.arguments[1].name.text === "url" &&
ts.isMetaProperty(node.arguments[1].expression) &&
node.arguments[1].expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
node.arguments[1].expression.name.text === "meta"
) {
visit({
kind: "import-meta-url",
node,
specifier: node.arguments[0].text,
specifierNode: node.arguments[0],
});
}
ts.forEachChild(node, walk);
+25 -101
View File
@@ -1,5 +1,7 @@
// Scans packaged dist JavaScript for relative imports and missing closure entries.
import path from "node:path";
import ts from "typescript";
import { visitModuleSpecifiers } from "./guard-inventory-utils.mjs";
const JS_DIST_FILE_RE = /^dist\/.*\.(?:cjs|js|mjs)$/u;
@@ -26,108 +28,30 @@ function resolveDistImportPath(importerPath, specifier) {
return path.posix.normalize(path.posix.join(path.posix.dirname(importerPath), stripped));
}
function findStatementStart(source, index) {
return (
Math.max(
source.lastIndexOf(";", index),
source.lastIndexOf("{", index),
source.lastIndexOf("}", index),
source.lastIndexOf("\n", index),
source.lastIndexOf("\r", index),
) + 1
);
}
function isImportSpecifierContext(source, index) {
const dynamicPrefix = source.slice(Math.max(0, index - 32), index);
if (/\bimport\s*\(\s*$/u.test(dynamicPrefix)) {
return true;
}
const statementPrefix = source.slice(findStatementStart(source, index), index).trimStart();
return (
/^(?:import|export)\b[\s\S]*\bfrom\s*$/u.test(statementPrefix) ||
/^import\s*$/u.test(statementPrefix)
);
}
function isRequireSpecifierContext(source, index) {
const prefix = source.slice(Math.max(0, index - 32), index);
return /\brequire\s*\(\s*$/u.test(prefix);
}
function isImportMetaUrlContext(source, quoteStart, quoteEnd) {
const prefix = source.slice(Math.max(0, quoteStart - 32), quoteStart);
if (!/\bnew\s+URL\s*\(\s*$/u.test(prefix)) {
return false;
}
const suffix = source.slice(quoteEnd + 1, quoteEnd + 96);
return /^\s*,\s*import\.meta\.url\s*,?\s*\)/u.test(suffix);
}
function collectImportSpecifiers(source) {
function collectImportSpecifiers(source, importerPath) {
const specifiers = [];
let inBlockComment = false;
let inLineComment = false;
for (let index = 0; index < source.length; index += 1) {
if (inBlockComment) {
if (source[index] === "*" && source[index + 1] === "/") {
inBlockComment = false;
index += 1;
const sourceFile = ts.createSourceFile(
importerPath,
source,
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.JS,
);
visitModuleSpecifiers(
ts,
sourceFile,
({ kind, specifier }) => {
if (
specifier.startsWith(".") &&
(kind !== "import-meta-url" ||
(hasJavaScriptFileExtension(specifier) &&
resolveDistImportPath(importerPath, specifier)?.startsWith("dist/")))
) {
specifiers.push(specifier);
}
continue;
}
if (inLineComment) {
if (source[index] === "\n" || source[index] === "\r") {
inLineComment = false;
}
continue;
}
if (source[index] === "/" && source[index + 1] === "*") {
inBlockComment = true;
index += 1;
continue;
}
if (source[index] === "/" && source[index + 1] === "/") {
inLineComment = true;
index += 1;
continue;
}
const quote = source[index];
if (quote !== '"' && quote !== "'") {
continue;
}
let cursor = index + 1;
let value = "";
while (cursor < source.length) {
const char = source[cursor];
if (char === "\\") {
value += source.slice(cursor, cursor + 2);
cursor += 2;
continue;
}
if (char === quote) {
break;
}
value += char;
cursor += 1;
}
if (cursor >= source.length) {
break;
}
if (value.startsWith(".")) {
const isDistDependency =
isImportSpecifierContext(source, index) ||
isRequireSpecifierContext(source, index) ||
(isImportMetaUrlContext(source, index, cursor) && hasJavaScriptFileExtension(value));
if (isDistDependency) {
specifiers.push(value);
}
}
index = cursor;
}
},
{ includeCommonJs: true, includeImportMetaUrl: true },
);
return specifiers;
}
@@ -157,7 +81,7 @@ export function collectPackageDistImports(params) {
continue;
}
const source = params.readText(importerPath);
for (const specifier of collectImportSpecifiers(source)) {
for (const specifier of collectImportSpecifiers(source, importerPath)) {
const importedPath = resolveDistImportPath(importerPath, specifier);
if (!importedPath) {
continue;
+2
View File
@@ -30,6 +30,8 @@ const PACKAGE_BUILD_PLUGIN_SELECTION_ENV_NAMES = [
"OPENCLAW_EXTENSIONS",
"OPENCLAW_DOCKER_BUILD_EXTENSIONS",
DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV,
// Public package builds must not inherit a smoke lane's private QA entrypoints.
"OPENCLAW_BUILD_PRIVATE_QA",
];
const SIGNAL_EXIT_CODES = {
SIGHUP: 129,
+1
View File
@@ -108,6 +108,7 @@ const requiredPathGroups = [
"scripts/lib/official-external-plugin-catalog.json",
"scripts/lib/official-external-provider-catalog.json",
"scripts/lib/recommended-tool-installs.json",
"scripts/lib/guard-inventory-utils.mjs",
"scripts/lib/package-dist-imports.mjs",
"scripts/postinstall-bundled-plugins.mjs",
"dist/agents/compaction-planning.worker.js",
+5
View File
@@ -164,6 +164,9 @@ describe("Dockerfile", () => {
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile");
const postinstallIndex = dockerfile.indexOf("COPY scripts/postinstall-bundled-plugins.mjs");
const prepareIndex = dockerfile.indexOf("scripts/prepare-git-hooks.mjs");
const importGrammarIndex = dockerfile.indexOf(
"COPY scripts/lib/guard-inventory-utils.mjs ./scripts/lib/guard-inventory-utils.mjs",
);
const distImportHelperIndex = dockerfile.indexOf(
"COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs",
);
@@ -176,6 +179,7 @@ describe("Dockerfile", () => {
expect(postinstallIndex).toBeGreaterThan(-1);
expect(prepareIndex).toBeGreaterThan(-1);
expect(importGrammarIndex).toBeGreaterThan(-1);
expect(distImportHelperIndex).toBeGreaterThan(-1);
expect(packageManifestIndex).toBeGreaterThan(-1);
expect(extensionManifestIndex).toBeGreaterThan(-1);
@@ -190,6 +194,7 @@ describe("Dockerfile", () => {
);
expect(postinstallIndex).toBeLessThan(installIndex);
expect(prepareIndex).toBeLessThan(installIndex);
expect(importGrammarIndex).toBeLessThan(installIndex);
expect(distImportHelperIndex).toBeLessThan(installIndex);
expect(packageManifestIndex).toBeLessThan(installIndex);
expect(extensionManifestIndex).toBeLessThan(installIndex);
@@ -347,6 +347,7 @@ describe("package-openclaw-for-docker", () => {
packageExtensions: string | undefined;
dockerBuildExtensions: string | undefined;
internalDockerBuildPluginIds: string | undefined;
privateQa: string | undefined;
skipDts: string | undefined;
timeoutMs: number | undefined;
}> = [];
@@ -355,11 +356,13 @@ describe("package-openclaw-for-docker", () => {
const previousPackageExtensions = process.env.OPENCLAW_EXTENSIONS;
const previousDockerBuildExtensions = process.env.OPENCLAW_DOCKER_BUILD_EXTENSIONS;
const previousInternalPluginIds = process.env[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV];
const previousPrivateQa = process.env.OPENCLAW_BUILD_PRIVATE_QA;
process.env.OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS = "1234";
process.env.OPENCLAW_RUN_NODE_SKIP_DTS_BUILD = "1";
process.env.OPENCLAW_EXTENSIONS = "clickclack";
process.env.OPENCLAW_DOCKER_BUILD_EXTENSIONS = "slack";
process.env[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV] = "msteams";
process.env.OPENCLAW_BUILD_PRIVATE_QA = "1";
try {
await buildPackageArtifacts("/repo", {
@@ -377,6 +380,7 @@ describe("package-openclaw-for-docker", () => {
packageExtensions: options.env?.OPENCLAW_EXTENSIONS,
dockerBuildExtensions: options.env?.OPENCLAW_DOCKER_BUILD_EXTENSIONS,
internalDockerBuildPluginIds: options.env?.[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV],
privateQa: options.env?.OPENCLAW_BUILD_PRIVATE_QA,
skipDts: options.env?.OPENCLAW_RUN_NODE_SKIP_DTS_BUILD,
timeoutMs: options.timeoutMs,
});
@@ -397,6 +401,7 @@ describe("package-openclaw-for-docker", () => {
["OPENCLAW_EXTENSIONS", previousPackageExtensions],
["OPENCLAW_DOCKER_BUILD_EXTENSIONS", previousDockerBuildExtensions],
[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV, previousInternalPluginIds],
["OPENCLAW_BUILD_PRIVATE_QA", previousPrivateQa],
] as const) {
if (previousValue === undefined) {
delete process.env[envName];
@@ -415,6 +420,7 @@ describe("package-openclaw-for-docker", () => {
internalDockerBuildPluginIds: undefined,
noPnpm: "1",
packageExtensions: undefined,
privateQa: undefined,
skipDts: "0",
timeoutMs: 1234,
},
+2
View File
@@ -716,6 +716,7 @@ describe("collectMissingPackPaths", () => {
"scripts/lib/official-external-plugin-catalog.json",
"scripts/lib/official-external-provider-catalog.json",
"scripts/lib/recommended-tool-installs.json",
"scripts/lib/guard-inventory-utils.mjs",
"scripts/lib/package-dist-imports.mjs",
"scripts/postinstall-bundled-plugins.mjs",
"dist/agents/compaction-planning.worker.js",
@@ -756,6 +757,7 @@ describe("collectMissingPackPaths", () => {
"scripts/lib/official-external-plugin-catalog.json",
"scripts/lib/official-external-provider-catalog.json",
"scripts/lib/recommended-tool-installs.json",
"scripts/lib/guard-inventory-utils.mjs",
"scripts/lib/package-dist-imports.mjs",
"scripts/postinstall-bundled-plugins.mjs",
"dist/agents/compaction-planning.worker.js",
@@ -357,6 +357,66 @@ describe("check-openclaw-package-tarball", () => {
);
});
it("rejects leaked private QA Docker chunks that import an omitted QA runtime", () => {
withTarball(
["dist/docker-runtime-BVdgRgxA.js"],
{
"dist/docker-runtime-BVdgRgxA.js":
'import { createQaDockerRuntime } from "./qa-runtime-Bi1S3plf.js";\n' +
"export { createQaDockerRuntime };\n",
},
(tarball) => {
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(
"dist/docker-runtime-BVdgRgxA.js imports missing dist/qa-runtime-Bi1S3plf.js",
);
},
);
});
it.each([
{
name: "named imports",
source: 'import { value } from "./missing.js";\n',
},
{
name: "multiline named imports",
source: 'import {\n value,\n} from "./missing.js";\n',
},
{
name: "named re-exports",
source: 'export { value } from "./missing.js";\n',
},
])("rejects missing packaged chunks in $name", ({ source }) => {
withTarball(
["dist/index.js"],
{ "dist/index.js": source },
(tarball) => {
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("dist/index.js imports missing dist/missing.js");
},
"2026.4.27",
);
});
it("does not reject import-like text inside packaged template literals", () => {
withTarball(
["dist/index.js"],
{ "dist/index.js": 'const example = `\nimport "./phantom.js"\n`;\n' },
(tarball) => {
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("OpenClaw package tarball integrity passed.");
},
"2026.4.27",
);
});
it("accepts dist files whose relative chunks are present", () => {
withTarball(
["dist/cli/run-main.js", "dist/memory-state-current.js"],
@@ -393,6 +453,23 @@ describe("check-openclaw-package-tarball", () => {
);
});
it("rejects named imported chunks omitted from the postinstall inventory", () => {
withTarball(
["dist/index.js"],
{
"dist/index.js": 'import { value } from "./chunk.js";\nexport { value };\n',
"dist/chunk.js": "export const value = 42;\n",
},
(tarball) => {
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("inventory omits imported dist file dist/chunk.js");
},
"2026.4.27",
);
});
it("rejects CommonJS require chunks omitted from the postinstall inventory", () => {
withTarball(
["dist/index.cjs"],
@@ -477,6 +554,27 @@ describe("check-openclaw-package-tarball", () => {
);
});
it.each([
"../../openclaw.mjs",
"../../scripts/run-node.mjs",
"../../dist/entry.js",
"../../dist/entry.mjs",
])("allows import.meta.url JavaScript probes outside packaged dist (%s)", (specifier) => {
withTarball(
["dist/index.js"],
{
"dist/index.js": `const candidate = new URL(${JSON.stringify(specifier)}, import.meta.url);\n`,
},
(tarball) => {
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("OpenClaw package tarball integrity passed.");
},
"2026.4.27",
);
});
it("allows import.meta.url source helper probes", () => {
withTarball(
["dist/index.js"],
@@ -47,6 +47,69 @@ describe("check-package-dist-imports", () => {
expect(result.stdout).toContain("OpenClaw package dist import closure passed.");
});
it.each([
{
name: "named imports",
source: 'import { value } from "./missing.js";\n',
},
{
name: "multiline named imports",
source: 'import {\n value,\n} from "./missing.js";\n',
},
{
name: "named re-exports",
source: 'export { value } from "./missing.js";\n',
},
{
name: "multiline named re-exports",
source: 'export {\n value,\n} from "./missing.js";\n',
},
])("rejects missing chunks in $name", ({ source }) => {
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
mkdirSync(join(root, "dist"), { recursive: true });
writeFileSync(join(root, "dist", "index.js"), source, "utf8");
const result = spawnSync("node", [CHECK_SCRIPT, root], { encoding: "utf8" });
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("dist/index.js imports missing dist/missing.js");
});
it("ignores import-like text inside multiline template literals", () => {
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
mkdirSync(join(root, "dist"), { recursive: true });
writeFileSync(
join(root, "dist", "index.js"),
'const example = `\nimport "./phantom.js"\n`;\n',
"utf8",
);
const result = spawnSync("node", [CHECK_SCRIPT, root], { encoding: "utf8" });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("OpenClaw package dist import closure passed.");
});
it.each([
"../../openclaw.mjs",
"../../scripts/run-node.mjs",
"../../dist/entry.js",
"../../dist/entry.mjs",
])("ignores import.meta.url probes outside packaged dist (%s)", (specifier) => {
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
mkdirSync(join(root, "dist"), { recursive: true });
writeFileSync(
join(root, "dist", "index.js"),
`const candidate = new URL(${JSON.stringify(specifier)}, import.meta.url);\n`,
"utf8",
);
const result = spawnSync("node", [CHECK_SCRIPT, root], { encoding: "utf8" });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("OpenClaw package dist import closure passed.");
});
it("rejects missing CommonJS require chunks", () => {
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
mkdirSync(join(root, "dist"), { recursive: true });
+4 -1
View File
@@ -5066,10 +5066,13 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
expect(smokeBuildStep.run).toContain("node scripts/build-all.mjs qaRuntime");
expect(smokeBuildStep.run).toContain("pnpm ui:build");
expect(smokeBuildStep.env.OPENCLAW_BUILD_PRIVATE_QA).toBe("1");
expect(smokeBuildStep.run).toContain("--skip-build");
expect(smokeBuildStep.run).not.toContain("--skip-build");
expect(smokeBuildStep.run).toContain("--allow-unreleased-changelog");
expect(smokeBuildStep.run).toContain("grep -Fq");
expect(smokeBuildStep.run).toContain('"${package_args[@]}"');
expect(smokeBuildStep.run.indexOf("node scripts/package-openclaw-for-docker.mjs")).toBeLessThan(
smokeBuildStep.run.indexOf("node scripts/build-all.mjs qaRuntime"),
);
expect(workflow.jobs["qa-smoke-ci-artifacts"]).toBeUndefined();
expect(workflow.jobs["qa-smoke-ci"]).toBeUndefined();
expect(smokeProfileJob.needs).toEqual(["preflight"]);
@@ -114,6 +114,16 @@ describe("bundled plugin postinstall", () => {
fileURLToPath(new URL("../../scripts/lib/package-dist-imports.mjs", import.meta.url)),
path.join(scriptRoot, "lib", "package-dist-imports.mjs"),
);
await fs.copyFile(
fileURLToPath(new URL("../../scripts/lib/guard-inventory-utils.mjs", import.meta.url)),
path.join(scriptRoot, "lib", "guard-inventory-utils.mjs"),
);
await fs.mkdir(path.join(packageRoot, "node_modules"), { recursive: true });
await fs.symlink(
fileURLToPath(new URL("../../node_modules/typescript", import.meta.url)),
path.join(packageRoot, "node_modules", "typescript"),
process.platform === "win32" ? "junction" : "dir",
);
for (const sentinel of sentinels) {
await fs.mkdir(path.dirname(sentinel), { recursive: true });
await fs.writeFile(sentinel, "owned by another Node application\n");
@@ -691,6 +701,40 @@ describe("bundled plugin postinstall", () => {
await expectPathMissing(staleFile);
});
it("keeps named imported chunks without preserving template-literal pseudoimports", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-named-import-");
const entryFile = path.join(packageRoot, "dist", "cli", "run-main.js");
const importedChunk = path.join(packageRoot, "dist", "memory-state-current.js");
const phantomChunk = path.join(packageRoot, "dist", "memory-state-phantom.js");
await fs.mkdir(path.dirname(entryFile), { recursive: true });
await fs.writeFile(
entryFile,
[
"import {",
" value,",
'} from "../memory-state-current.js";',
"const example = `",
'import "../memory-state-phantom.js"',
"`;",
"export { value, example };",
"",
].join("\n"),
);
await writePackageDistInventory(packageRoot);
await fs.writeFile(importedChunk, "export const value = 42;\n");
await fs.writeFile(phantomChunk, "export const stale = true;\n");
expect(
pruneInstalledPackageDist({
packageRoot,
log: { log: vi.fn(), warn: vi.fn() },
}),
).toEqual(["dist/memory-state-phantom.js"]);
await expectPathExists(importedChunk);
await expectPathMissing(phantomChunk);
});
it("does not abort dist pruning when a listed chunk disappears before import expansion", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-missing-chunk-");
const entryFile = path.join(packageRoot, "dist", "control-ui", "assets", "instances.js");