From a7260014479e4941269120b2be6a7c39808a0a82 Mon Sep 17 00:00:00 2001 From: openperf <16864032@qq.com> Date: Thu, 30 Apr 2026 19:18:19 +0800 Subject: [PATCH] fix(plugins): canonicalize packageRoot before hashing runtime-deps stage key --- src/plugins/bundled-runtime-deps-roots.ts | 4 ++- src/plugins/bundled-runtime-deps.test.ts | 35 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/plugins/bundled-runtime-deps-roots.ts b/src/plugins/bundled-runtime-deps-roots.ts index cdd0c8264d8..f21dd222ccb 100644 --- a/src/plugins/bundled-runtime-deps-roots.ts +++ b/src/plugins/bundled-runtime-deps-roots.ts @@ -53,7 +53,9 @@ function isPackagedBundledPluginRoot(pluginRoot: string): boolean { } function createPathHash(value: string): string { - return createHash("sha256").update(path.resolve(value)).digest("hex").slice(0, 12); + // Hash the OS-canonical (realpath) form so symlinked / junctioned + // packageRoots converge on a single staging directory across call sites. + return createHash("sha256").update(realpathOrResolve(value)).digest("hex").slice(0, 12); } function sanitizePathSegment(value: string): string { diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 016d42ea3c2..a06fc88a2ba 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -2645,6 +2645,41 @@ describe("ensureBundledPluginRuntimeDeps", () => { expect(path.basename(resolved).startsWith("openclaw-unknown-")).toBe(false); }); + const itSupportsPackageRootSymlinks = process.platform === "win32" ? it.skip : it; + itSupportsPackageRootSymlinks( + "stages bundled runtime deps to the same root for symlinked packageRoot views (issue #74963)", + () => { + const realParent = makeTempDir(); + const stageDir = makeTempDir(); + const realPackageRoot = path.join(realParent, "openclaw-real"); + fs.mkdirSync(realPackageRoot, { recursive: true }); + fs.writeFileSync( + path.join(realPackageRoot, "package.json"), + JSON.stringify({ name: "openclaw", version: "2026.4.27" }), + ); + const realPluginRoot = path.join(realPackageRoot, "dist", "extensions", "discord"); + fs.mkdirSync(realPluginRoot, { recursive: true }); + fs.writeFileSync( + path.join(realPluginRoot, "package.json"), + JSON.stringify({ dependencies: {} }), + ); + const linkedPackageRoot = path.join(realParent, "openclaw-linked"); + fs.symlinkSync(realPackageRoot, linkedPackageRoot, "dir"); + const linkedPluginRoot = path.join(linkedPackageRoot, "dist", "extensions", "discord"); + const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir }; + + const installRootViaReal = resolveBundledRuntimeDependencyInstallRoot(realPluginRoot, { + env, + }); + const installRootViaLink = resolveBundledRuntimeDependencyInstallRoot(linkedPluginRoot, { + env, + }); + + expect(installRootViaLink).toBe(installRootViaReal); + expect(path.basename(installRootViaReal)).toMatch(/^openclaw-2026\.4\.27-[0-9a-f]{12}$/); + }, + ); + it("prunes stale unknown external runtime roots while keeping newest and locked roots", () => { const stageDir = makeTempDir(); const nowMs = Date.parse("2026-04-29T08:00:00.000Z");