fix: preserve nested runtime test packages

This commit is contained in:
Gustavo Madeira Santana
2026-04-15 12:14:44 -04:00
parent 0618c6933d
commit 4741cb9085
5 changed files with 87 additions and 10 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
- Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring `channels.matrix.password`, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792.
- Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with `NO_REPLY` so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator.
- Plugins/bundled channels: partition bundled channel lazy caches by active bundled root so `OPENCLAW_BUNDLED_PLUGINS_DIR` flips stop reusing stale plugin, setup, secrets, and runtime state. (#67200) Thanks @gumadeiras.
- Packaging/plugins: keep packaged runtime-dependency pruning and npm release validation from deleting or rejecting legitimate nested packages named `test`/`tests` under `node_modules`. (#67275) thanks @gumadeiras.
## 2026.4.15-beta.1

View File

@@ -118,7 +118,6 @@ const skipPackValidationEnv = "OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK";
function normalizePackedPath(packedPath: string): string {
return packedPath.replace(/\\/g, "/");
}
function isNodeModulesPackageRoot(segments: string[], index: number): boolean {
const parent = segments[index - 1];
if (parent === "node_modules") {
@@ -521,11 +520,8 @@ function collectPackedTarballErrors(): string[] {
return [
...collectControlUiPackErrors(packedPaths),
...collectForbiddenPackedPathErrors(packedPaths),
<<<<<<< HEAD
...collectForbiddenPackedContentErrors(packedPaths),
=======
...collectPackedTestCargoErrors(packedPaths),
>>>>>>> caafdea0bb (Build: prune packaged runtime test cargo)
];
}
@@ -591,11 +587,8 @@ export function collectPackedTestCargoErrors(paths: Iterable<string>): string[]
return errors.toSorted((left, right) => left.localeCompare(right));
}
<<<<<<< HEAD
async function main(): Promise<number> {
=======
function main(): number {
>>>>>>> caafdea0bb (Build: prune packaged runtime test cargo)
async function main(): Promise<number> {
const pkg = loadPackageJson();
const now = new Date();
const skipPackValidation = shouldSkipPackedTarballValidation();

View File

@@ -504,6 +504,18 @@ function pruneDependencyFilesBySuffixes(depRoot, suffixes) {
});
}
function relativePathSegments(rootDir, fullPath) {
return path.relative(rootDir, fullPath).split(path.sep).filter(Boolean);
}
function isNodeModulesPackageRoot(segments, index) {
const parent = segments[index - 1];
if (parent === "node_modules") {
return true;
}
return parent?.startsWith("@") === true && segments[index - 2] === "node_modules";
}
function pruneDependencyDirectoriesByBasename(depRoot, basenames) {
if (!basenames || basenames.length === 0 || !fs.existsSync(depRoot)) {
return;
@@ -517,7 +529,8 @@ function pruneDependencyDirectoriesByBasename(depRoot, basenames) {
continue;
}
const fullPath = path.join(currentDir, entry.name);
if (basenameSet.has(entry.name)) {
const segments = relativePathSegments(depRoot, fullPath);
if (basenameSet.has(entry.name) && !isNodeModulesPackageRoot(segments, segments.length - 1)) {
removePathIfExists(fullPath);
continue;
}
@@ -531,7 +544,7 @@ function pruneDependencyFilesByPatterns(depRoot, patterns) {
return;
}
walkFiles(depRoot, (fullPath) => {
const relativePath = path.relative(depRoot, fullPath).replace(/\\/g, "/");
const relativePath = relativePathSegments(depRoot, fullPath).join("/");
if (patterns.some((pattern) => pattern.test(relativePath))) {
removePathIfExists(fullPath);
}

View File

@@ -406,6 +406,15 @@ describe("collectPackedTestCargoErrors", () => {
]),
).toEqual([]);
});
it("allows legitimate package roots named test under node_modules", () => {
expect(
collectPackedTestCargoErrors([
"dist/extensions/fixture-plugin/node_modules/direct/node_modules/test/index.js",
"dist/extensions/fixture-plugin/node_modules/direct/node_modules/@scope/tests/index.js",
]),
).toEqual([]);
});
});
describe("collectReleaseTagErrors", () => {

View File

@@ -415,6 +415,67 @@ describe("stageBundledPluginRuntimeDeps", () => {
).toBe(false);
});
it("preserves nested runtime dependencies named test or tests", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {
name: "@openclaw/fixture-plugin",
version: "1.0.0",
dependencies: { direct: "1.0.0" },
openclaw: { bundle: { stageRuntimeDependencies: true } },
},
});
const directDir = path.join(repoRoot, "node_modules", "direct");
const nestedTestDir = path.join(directDir, "node_modules", "test");
const scopedTestsDir = path.join(directDir, "node_modules", "@scope", "tests");
fs.mkdirSync(nestedTestDir, { recursive: true });
fs.mkdirSync(scopedTestsDir, { recursive: true });
fs.writeFileSync(
path.join(directDir, "package.json"),
'{ "name": "direct", "version": "1.0.0", "dependencies": { "test": "^1.0.0", "@scope/tests": "^1.0.0" } }\n',
"utf8",
);
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'direct';\n", "utf8");
fs.writeFileSync(
path.join(nestedTestDir, "package.json"),
'{ "name": "test", "version": "1.0.0" }\n',
"utf8",
);
fs.writeFileSync(path.join(nestedTestDir, "index.js"), "module.exports = 'test';\n", "utf8");
fs.writeFileSync(
path.join(scopedTestsDir, "package.json"),
'{ "name": "@scope/tests", "version": "1.0.0" }\n',
"utf8",
);
fs.writeFileSync(
path.join(scopedTestsDir, "index.js"),
"module.exports = 'scoped-tests';\n",
"utf8",
);
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
expect(
fs.readFileSync(
path.join(pluginDir, "node_modules", "direct", "node_modules", "test", "index.js"),
"utf8",
),
).toBe("module.exports = 'test';\n");
expect(
fs.readFileSync(
path.join(
pluginDir,
"node_modules",
"direct",
"node_modules",
"@scope",
"tests",
"index.js",
),
"utf8",
),
).toBe("module.exports = 'scoped-tests';\n");
});
it("stages hoisted transitive runtime deps from the root node_modules", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {