


















@@ -29,6 +29,7 @@ import {
2929resolveGlobalRoot,
3030resolveNpmGlobalPrefixLayoutFromGlobalRoot,
3131resolveNpmGlobalPrefixLayoutFromPrefix,
32+resolvePnpmGlobalDirFromGlobalRoot,
3233type CommandRunner,
3334} from "./update-global.js";
3435@@ -416,6 +417,151 @@ describe("update global helpers", () => {
416417}
417418});
418419420+it("detects custom pnpm global layouts from the running package root", async () => {
421+await withTempDir({ prefix: "openclaw-update-pnpm-custom-root-" }, async (base) => {
422+const customGlobalDir = path.join(base, "custom-pnpm");
423+const customGlobalRoot = path.join(customGlobalDir, "5", "node_modules");
424+const pkgRoot = path.join(customGlobalRoot, "openclaw");
425+const defaultPnpmRoot = path.join(base, "default-pnpm", "5", "node_modules");
426+await fs.mkdir(pkgRoot, { recursive: true });
427+await fs.writeFile(
428+path.join(customGlobalDir, "5", "pnpm-lock.yaml"),
429+"lockfileVersion: '9.0'\n",
430+"utf8",
431+);
432+await fs.writeFile(
433+path.join(customGlobalRoot, ".modules.yaml"),
434+"layoutVersion: 5\n",
435+"utf8",
436+);
437+438+const runCommand: CommandRunner = async (argv) => {
439+if (argv[0] === "npm") {
440+return { stdout: "", stderr: "", code: 1 };
441+}
442+if (argv[0] === "pnpm") {
443+return { stdout: `${defaultPnpmRoot}\n`, stderr: "", code: 0 };
444+}
445+throw new Error(`unexpected command: ${argv.join(" ")}`);
446+};
447+448+await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe(
449+"pnpm",
450+);
451+await expect(
452+resolveGlobalInstallTarget({
453+manager: "pnpm",
454+ runCommand,
455+timeoutMs: 1000,
456+ pkgRoot,
457+}),
458+).resolves.toEqual({
459+manager: "pnpm",
460+command: "pnpm",
461+globalRoot: customGlobalRoot,
462+packageRoot: pkgRoot,
463+});
464+expect(resolvePnpmGlobalDirFromGlobalRoot(customGlobalRoot)).toBe(customGlobalDir);
465+});
466+});
467+468+it("detects custom pnpm global layouts from virtual-store package roots", async () => {
469+await withTempDir({ prefix: "openclaw-update-pnpm-virtual-root-" }, async (base) => {
470+const customGlobalDir = path.join(base, "custom-pnpm");
471+const customGlobalRoot = path.join(customGlobalDir, "5", "node_modules");
472+const pkgRoot = path.join(
473+customGlobalDir,
474+"5",
475+".pnpm",
476+"openclaw@file+..+pack+openclaw-2026.5.6.tgz",
477+"node_modules",
478+"openclaw",
479+);
480+const defaultPnpmRoot = path.join(base, "default-pnpm", "5", "node_modules");
481+await fs.mkdir(customGlobalRoot, { recursive: true });
482+await fs.mkdir(pkgRoot, { recursive: true });
483+await fs.writeFile(
484+path.join(customGlobalDir, "5", "pnpm-lock.yaml"),
485+"lockfileVersion: '9.0'\n",
486+"utf8",
487+);
488+await fs.writeFile(
489+path.join(customGlobalRoot, ".modules.yaml"),
490+"layoutVersion: 5\n",
491+"utf8",
492+);
493+494+const runCommand: CommandRunner = async (argv) => {
495+if (argv[0] === "npm") {
496+return { stdout: "", stderr: "", code: 1 };
497+}
498+if (argv[0] === "pnpm") {
499+return { stdout: `${defaultPnpmRoot}\n`, stderr: "", code: 0 };
500+}
501+throw new Error(`unexpected command: ${argv.join(" ")}`);
502+};
503+504+await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe(
505+"pnpm",
506+);
507+await expect(
508+resolveGlobalInstallTarget({
509+manager: "pnpm",
510+ runCommand,
511+timeoutMs: 1000,
512+ pkgRoot,
513+}),
514+).resolves.toEqual({
515+manager: "pnpm",
516+command: "pnpm",
517+globalRoot: customGlobalRoot,
518+packageRoot: path.join(customGlobalRoot, "openclaw"),
519+});
520+});
521+});
522+523+it("does not infer pnpm ownership without pnpm node_modules metadata", async () => {
524+await withTempDir({ prefix: "openclaw-update-pnpm-shape-only-" }, async (base) => {
525+const customGlobalDir = path.join(base, "custom-pnpm");
526+const customGlobalRoot = path.join(customGlobalDir, "5", "node_modules");
527+const pkgRoot = path.join(customGlobalRoot, "openclaw");
528+const defaultPnpmRoot = path.join(base, "default-pnpm", "5", "node_modules");
529+await fs.mkdir(pkgRoot, { recursive: true });
530+await fs.writeFile(
531+path.join(customGlobalDir, "5", "pnpm-lock.yaml"),
532+"lockfileVersion: '9.0'\n",
533+"utf8",
534+);
535+536+const runCommand: CommandRunner = async (argv) => {
537+if (argv[0] === "npm") {
538+return { stdout: "", stderr: "", code: 1 };
539+}
540+if (argv[0] === "pnpm") {
541+return { stdout: `${defaultPnpmRoot}\n`, stderr: "", code: 0 };
542+}
543+throw new Error(`unexpected command: ${argv.join(" ")}`);
544+};
545+546+await expect(
547+detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000),
548+).resolves.toBeNull();
549+await expect(
550+resolveGlobalInstallTarget({
551+manager: "pnpm",
552+ runCommand,
553+timeoutMs: 1000,
554+ pkgRoot,
555+}),
556+).resolves.toEqual({
557+manager: "pnpm",
558+command: "pnpm",
559+globalRoot: defaultPnpmRoot,
560+packageRoot: path.join(defaultPnpmRoot, "openclaw"),
561+});
562+});
563+});
564+419565it("builds install argv and npm fallback argv", () => {
420566expect(resolveGlobalInstallCommand("npm")).toEqual({
421567manager: "npm",
@@ -457,6 +603,14 @@ describe("update global helpers", () => {
457603expect(
458604globalInstallArgs({ manager: "pnpm", command: "/opt/homebrew/bin/pnpm" }, "openclaw@latest"),
459605).toEqual(["/opt/homebrew/bin/pnpm", "add", "-g", "openclaw@latest"]);
606+expect(globalInstallArgs("pnpm", "openclaw@latest", null, "/opt/pnpm-global")).toEqual([
607+"pnpm",
608+"add",
609+"-g",
610+"--global-dir",
611+"/opt/pnpm-global",
612+"openclaw@latest",
613+]);
460614});
461615462616it("builds npm staged install argv with an explicit prefix", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。