mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(infra): recognize global Bun installs without breaking pnpm (#118143)
This commit is contained in:
committed by
GitHub
parent
bc826d6866
commit
d9c9ad8d8a
@@ -20,14 +20,19 @@ async function withPackageManagerRoot<T>(
|
||||
});
|
||||
}
|
||||
|
||||
async function writePublishedOpenClawRoot(root: string): Promise<void> {
|
||||
async function writePublishedOpenClawRoot(
|
||||
root: string,
|
||||
options: { shrinkwrap: boolean },
|
||||
): Promise<void> {
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(root, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", packageManager: "pnpm@11.2.2" }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(path.join(root, "npm-shrinkwrap.json"), "{}", "utf8");
|
||||
if (options.shrinkwrap) {
|
||||
await fs.writeFile(path.join(root, "npm-shrinkwrap.json"), "{}", "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
describe("detectPackageManager", () => {
|
||||
@@ -93,43 +98,78 @@ describe("detectPackageManager", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps pnpm-owned direct package roots that ship npm-shrinkwrap", async () => {
|
||||
await withTempDir({ prefix: "openclaw-detect-pm-pnpm-direct-" }, async (base) => {
|
||||
const nodeModulesRoot = path.join(base, "pnpm-global", "node_modules");
|
||||
const packageRoot = path.join(nodeModulesRoot, "openclaw");
|
||||
await writePublishedOpenClawRoot(packageRoot);
|
||||
await fs.writeFile(path.join(nodeModulesRoot, ".modules.yaml"), "layoutVersion: 5", "utf8");
|
||||
it.each(
|
||||
[
|
||||
{ manager: "pnpm", layout: "direct" },
|
||||
{ manager: "pnpm", layout: "virtual" },
|
||||
{ manager: "bun", layout: "global" },
|
||||
].flatMap((scenario) => [
|
||||
{ ...scenario, packageFormat: "lockless", shrinkwrap: false },
|
||||
{ ...scenario, packageFormat: "legacy shrinkwrapped", shrinkwrap: true },
|
||||
]),
|
||||
)(
|
||||
"detects $manager ownership for $packageFormat packages in $layout layouts",
|
||||
async ({ manager, layout, shrinkwrap }) => {
|
||||
await withTempDir({ prefix: `openclaw-detect-pm-${manager}-` }, async (base) => {
|
||||
const bunInstall = path.join(base, "custom-bun-home");
|
||||
const nodeModulesRoot =
|
||||
manager === "bun"
|
||||
? path.join(bunInstall, "install", "global", "node_modules")
|
||||
: path.join(base, `${manager}-global`, "node_modules");
|
||||
const packageRoot =
|
||||
layout === "virtual"
|
||||
? path.join(nodeModulesRoot, ".pnpm", "openclaw@2026.5.27", "node_modules", "openclaw")
|
||||
: path.join(nodeModulesRoot, "openclaw");
|
||||
await writePublishedOpenClawRoot(packageRoot, { shrinkwrap });
|
||||
if (manager === "pnpm") {
|
||||
await fs.writeFile(
|
||||
path.join(nodeModulesRoot, ".modules.yaml"),
|
||||
"layoutVersion: 5",
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
await expect(detectPackageManager(packageRoot)).resolves.toBe("pnpm");
|
||||
});
|
||||
});
|
||||
await withEnvAsync({ BUN_INSTALL: bunInstall }, async () => {
|
||||
await expect(detectPackageManager(packageRoot)).resolves.toBe(manager);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps pnpm-owned virtual-store package roots that ship npm-shrinkwrap", async () => {
|
||||
await withTempDir({ prefix: "openclaw-detect-pm-pnpm-virtual-" }, async (base) => {
|
||||
const nodeModulesRoot = path.join(base, "project", "node_modules");
|
||||
const packageRoot = path.join(
|
||||
nodeModulesRoot,
|
||||
".pnpm",
|
||||
"openclaw@2026.5.27",
|
||||
it("keeps pnpm 11 ownership through markerless global virtual-store symlinks", async () => {
|
||||
await withTempDir({ prefix: "openclaw-detect-pm-pnpm-global-store-" }, async (base) => {
|
||||
const globalRoot = path.join(base, "pnpm-home", "global", "v11");
|
||||
const installRoot = path.join(globalRoot, "install-openclaw");
|
||||
const linkedPackageRoot = path.join(installRoot, "node_modules", "openclaw");
|
||||
const storePackageRoot = path.join(
|
||||
base,
|
||||
"pnpm-home",
|
||||
"store",
|
||||
"v11",
|
||||
"links",
|
||||
"@",
|
||||
"openclaw",
|
||||
"2026.7.2",
|
||||
"graph-hash",
|
||||
"node_modules",
|
||||
"openclaw",
|
||||
);
|
||||
await writePublishedOpenClawRoot(packageRoot);
|
||||
await fs.writeFile(path.join(nodeModulesRoot, ".modules.yaml"), "layoutVersion: 5", "utf8");
|
||||
await writePublishedOpenClawRoot(storePackageRoot, { shrinkwrap: false });
|
||||
await fs.mkdir(path.dirname(linkedPackageRoot), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(installRoot, "package.json"),
|
||||
JSON.stringify({ private: true, dependencies: { openclaw: "2026.7.2" } }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(path.join(installRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
|
||||
const symlinkType = process.platform === "win32" ? "junction" : "dir";
|
||||
await fs.symlink(storePackageRoot, linkedPackageRoot, symlinkType);
|
||||
await fs.symlink(installRoot, path.join(globalRoot, "hash-openclaw"), symlinkType);
|
||||
|
||||
await expect(detectPackageManager(packageRoot)).resolves.toBe("pnpm");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bun-owned global package roots that ship npm-shrinkwrap", async () => {
|
||||
await withTempDir({ prefix: "openclaw-detect-pm-bun-" }, async (base) => {
|
||||
const bunInstall = path.join(base, "bun-home");
|
||||
await withEnvAsync({ BUN_INSTALL: bunInstall }, async () => {
|
||||
const packageRoot = path.join(bunInstall, "install", "global", "node_modules", "openclaw");
|
||||
await writePublishedOpenClawRoot(packageRoot);
|
||||
|
||||
await expect(detectPackageManager(packageRoot)).resolves.toBe("bun");
|
||||
});
|
||||
await expect(detectPackageManager(linkedPackageRoot)).resolves.toBe("pnpm");
|
||||
await expect(detectPackageManager(await fs.realpath(linkedPackageRoot))).resolves.toBe(
|
||||
"pnpm",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -59,12 +59,12 @@ export async function detectPackageManager(root: string): Promise<DetectedPackag
|
||||
const hasPnpmLock = files.includes("pnpm-lock.yaml");
|
||||
const hasBunLock = files.includes("bun.lock") || files.includes("bun.lockb");
|
||||
|
||||
// Published packages retain source pnpm metadata, and modern releases omit
|
||||
// shrinkwrap; detect Bun by its install root before checking older npm locks.
|
||||
if (await isBunOwnedPackageRoot(root)) {
|
||||
return "bun";
|
||||
}
|
||||
if (hasNpmShrinkwrap) {
|
||||
// Published npm packages carry npm-shrinkwrap even when their source uses pnpm;
|
||||
// installed pnpm/bun-owned roots need layout proof before overriding npm.
|
||||
if (await isBunOwnedPackageRoot(root)) {
|
||||
return "bun";
|
||||
}
|
||||
if (pm === "pnpm" && (hasPnpmLock || (await isPnpmOwnedPackageRoot(root)))) {
|
||||
return "pnpm";
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { useMockHttp } from "../test-utils/mock-http.js";
|
||||
import { fetchNpmPackageTargetStatus } from "./update-check-package-target.js";
|
||||
import {
|
||||
@@ -816,33 +817,45 @@ describe("checkUpdateStatus", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("detects lockless OpenClaw npm installs despite packed pnpm metadata", async () => {
|
||||
await withTempDir({ prefix: "openclaw-update-check-lockless-npm-" }, async (base) => {
|
||||
const root = path.join(base, "prefix", "node_modules", "openclaw");
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(root, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", packageManager: "pnpm@11.2.2" }),
|
||||
"utf8",
|
||||
);
|
||||
it.each([
|
||||
{ manager: "npm", expectedLockfile: "package-lock.json" },
|
||||
{ manager: "bun", expectedLockfile: "bun.lockb" },
|
||||
])(
|
||||
"detects lockless OpenClaw $manager installs despite packed pnpm metadata",
|
||||
async ({ manager, expectedLockfile }) => {
|
||||
await withTempDir({ prefix: `openclaw-update-check-lockless-${manager}-` }, async (base) => {
|
||||
const bunInstall = path.join(base, "custom-bun-home");
|
||||
const root =
|
||||
manager === "bun"
|
||||
? path.join(bunInstall, "install", "global", "node_modules", "openclaw")
|
||||
: path.join(base, "prefix", "node_modules", "openclaw");
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(root, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", packageManager: "pnpm@11.2.2" }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const status = await checkUpdateStatus({
|
||||
root,
|
||||
includeRegistry: false,
|
||||
fetchGit: false,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
await withEnvAsync({ BUN_INSTALL: bunInstall }, async () => {
|
||||
const status = await checkUpdateStatus({
|
||||
root,
|
||||
includeRegistry: false,
|
||||
fetchGit: false,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
|
||||
expect(status.installKind).toBe("package");
|
||||
expect(status.packageManager).toBe("npm");
|
||||
expect(status.deps).toMatchObject({
|
||||
manager: "npm",
|
||||
lockfilePath: path.join(root, "package-lock.json"),
|
||||
status: "unknown",
|
||||
reason: "lockfile missing",
|
||||
expect(status.installKind).toBe("package");
|
||||
expect(status.packageManager).toBe(manager);
|
||||
expect(status.deps).toMatchObject({
|
||||
manager,
|
||||
lockfilePath: path.join(root, expectedLockfile),
|
||||
status: "unknown",
|
||||
reason: "lockfile missing",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("reports missing and stale dependency markers for package installs", async () => {
|
||||
await withTempDir({ prefix: "openclaw-update-check-deps-" }, async (root) => {
|
||||
|
||||
Reference in New Issue
Block a user