diff --git a/CHANGELOG.md b/CHANGELOG.md index f59d770c33f..0cfb32414f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 9371d766da4..f16563c3398 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -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[] return errors.toSorted((left, right) => left.localeCompare(right)); } -<<<<<<< HEAD async function main(): Promise { -======= -function main(): number { ->>>>>>> caafdea0bb (Build: prune packaged runtime test cargo) +async function main(): Promise { const pkg = loadPackageJson(); const now = new Date(); const skipPackValidation = shouldSkipPackedTarballValidation(); diff --git a/scripts/stage-bundled-plugin-runtime-deps.mjs b/scripts/stage-bundled-plugin-runtime-deps.mjs index 5c04686b1a2..8d44ad4be2b 100644 --- a/scripts/stage-bundled-plugin-runtime-deps.mjs +++ b/scripts/stage-bundled-plugin-runtime-deps.mjs @@ -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); } diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index 523dd0bc777..a89d33a410f 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -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", () => { diff --git a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts index e5634f8a945..ed6190d517b 100644 --- a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts +++ b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts @@ -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: {