fix(update): make pnpm preflight resolution deterministic

This commit is contained in:
Peter Steinberger
2026-05-13 03:01:05 +01:00
parent f1ddaf46c7
commit 1f02abe381
2 changed files with 144 additions and 6 deletions

View File

@@ -385,6 +385,129 @@ describe("runGatewayUpdate", () => {
expect(calls).not.toContain("pnpm ui:build");
});
it("uses pnpm highest resolution mode for update installs", async () => {
await setupGitCheckout({ packageManager: "pnpm@8.0.0" });
await setupUiIndex();
const stableTag = "v1.0.1-1";
const installEnvs: NodeJS.ProcessEnv[] = [];
const doctorNodePath = await resolveStableNodePath(process.execPath);
const { runCommand } = createGitInstallRunner({
stableTag,
installCommand: "pnpm install",
buildCommand: "pnpm build",
uiBuildCommand: "pnpm ui:build",
doctorCommand: `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`,
onCommand: (key, options) => {
if (key === "pnpm install") {
installEnvs.push(options?.env ?? {});
}
return undefined;
},
});
const result = await runWithCommand(runCommand, { channel: "stable" });
expect(result.status).toBe("ok");
expect(installEnvs).toHaveLength(1);
expect(installEnvs[0]).toMatchObject({
PNPM_CONFIG_RESOLUTION_MODE: "highest",
npm_config_resolution_mode: "highest",
pnpm_config_resolution_mode: "highest",
});
});
it("uses pnpm highest resolution mode for dev preflight installs", async () => {
await setupGitPackageManagerFixture();
const upstreamSha = "upstream123";
const installEnvs: NodeJS.ProcessEnv[] = [];
const doctorNodePath = await resolveStableNodePath(process.execPath);
const doctorCommand = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
const runCommand = async (
argv: string[],
options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number },
) => {
const key = argv.join(" ");
if (key === `git -C ${tempDir} rev-parse --show-toplevel`) {
return { stdout: tempDir, stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse HEAD`) {
return { stdout: "abc123", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse --abbrev-ref HEAD`) {
return { stdout: "main", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} status --porcelain -- :!dist/control-ui/`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse --abbrev-ref --symbolic-full-name @{upstream}`) {
return { stdout: "origin/main", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} fetch --all --prune --tags`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
return { stdout: upstreamSha, stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-list --max-count=10 ${upstreamSha}`) {
return { stdout: `${upstreamSha}\n`, stderr: "", code: 0 };
}
if (key === "pnpm --version") {
return { stdout: "10.0.0", stderr: "", code: 0 };
}
if (
key.startsWith(`git -C ${tempDir} worktree add --detach /tmp/`) &&
key.endsWith(` ${upstreamSha}`) &&
preflightPrefixPattern.test(key)
) {
return { stdout: `HEAD is now at ${upstreamSha}`, stderr: "", code: 0 };
}
if (
key.startsWith("git -C /tmp/") &&
preflightPrefixPattern.test(key) &&
key.includes(" checkout --detach ") &&
key.endsWith(upstreamSha)
) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === "pnpm install") {
installEnvs.push(options?.env ?? {});
return { stdout: "", stderr: "", code: 0 };
}
if (key === "pnpm build" || key === "pnpm ui:build") {
return { stdout: "", stderr: "", code: 0 };
}
if (
key.startsWith(`git -C ${tempDir} worktree remove --force /tmp/`) &&
preflightPrefixPattern.test(key)
) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} worktree prune`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rebase ${upstreamSha}`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === doctorCommand) {
return { stdout: "", stderr: "", code: 0 };
}
return { stdout: "", stderr: "", code: 0 };
};
const result = await runWithCommand(runCommand, { channel: "dev" });
expect(result.status).toBe("ok");
expect(installEnvs).toHaveLength(2);
for (const env of installEnvs) {
expect(env).toMatchObject({
PNPM_CONFIG_RESOLUTION_MODE: "highest",
npm_config_resolution_mode: "highest",
pnpm_config_resolution_mode: "highest",
});
}
});
it("returns error and stops early when build fails", async () => {
await setupGitCheckout({ packageManager: "pnpm@8.0.0" });
const stableTag = "v1.0.1-1";

View File

@@ -529,6 +529,21 @@ function resolveBuildEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined
};
}
function resolveInstallEnv(
manager: "pnpm" | "bun" | "npm",
env?: NodeJS.ProcessEnv,
): NodeJS.ProcessEnv | undefined {
if (manager !== "pnpm") {
return env;
}
return {
...env,
PNPM_CONFIG_RESOLUTION_MODE: env?.PNPM_CONFIG_RESOLUTION_MODE ?? "highest",
npm_config_resolution_mode: env?.npm_config_resolution_mode ?? "highest",
pnpm_config_resolution_mode: env?.pnpm_config_resolution_mode ?? "highest",
};
}
function isSupersededInstallFailure(
step: UpdateStepResult,
steps: readonly UpdateStepResult[],
@@ -1006,9 +1021,8 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
const depsStepName = preflightIgnoreScripts
? `preflight deps install (ignore scripts) (${shortSha})`
: `preflight deps install (${shortSha})`;
const depsStep = await runStep(
step(depsStepName, depsStepArgv, worktreeDir, manager.env),
);
const installEnv = resolveInstallEnv(manager.manager, manager.env);
const depsStep = await runStep(step(depsStepName, depsStepArgv, worktreeDir, installEnv));
steps.push(depsStep);
let finalDepsStep = depsStep;
if (
@@ -1023,7 +1037,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
`preflight deps install (ignore scripts) (${shortSha})`,
retryArgv,
worktreeDir,
manager.env,
installEnv,
),
);
steps.push(retryStep);
@@ -1200,6 +1214,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
};
}
try {
const installEnv = resolveInstallEnv(manager.manager, manager.env);
const depsStep = await runStep(
step(
"deps install",
@@ -1207,7 +1222,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
compatFallback: manager.fallback && manager.manager === "npm",
}),
gitRoot,
manager.env,
installEnv,
),
);
steps.push(depsStep);
@@ -1216,7 +1231,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
const retryArgv = managerInstallIgnoreScriptsArgs(manager.manager);
if (retryArgv) {
const retryStep = await runStep(
step("deps install (ignore scripts)", retryArgv, gitRoot, manager.env),
step("deps install (ignore scripts)", retryArgv, gitRoot, installEnv),
);
steps.push(retryStep);
finalDepsStep = retryStep;