From 73affb491a70f9baa235d07386247a8bddc8abdc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 12:39:38 +0100 Subject: [PATCH] fix: bound dev update cleanup --- scripts/e2e/parallels-macos-smoke.sh | 1 + scripts/e2e/parallels-windows-smoke.sh | 3 +- src/infra/update-runner.test.ts | 100 ++++++++++++++++++++++++- src/infra/update-runner.ts | 23 +++--- 4 files changed, 114 insertions(+), 13 deletions(-) diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index 9659ed3eac9..9068463873a 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -1149,6 +1149,7 @@ run_dev_channel_update() { rm -rf $(shell_quote "$update_root") export PATH=$(shell_quote "$bootstrap_bin:$GUEST_EXEC_PATH") /usr/bin/env NODE_OPTIONS=--max-old-space-size=4096 \ + OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \ $GUEST_NODE_BIN $GUEST_OPENCLAW_ENTRY update --channel dev --yes --json EOF )" "$update_log" "$update_done" "$TIMEOUT_UPDATE_DEV_S" "$update_runner" diff --git a/scripts/e2e/parallels-windows-smoke.sh b/scripts/e2e/parallels-windows-smoke.sh index 63088e20709..8af4617696f 100644 --- a/scripts/e2e/parallels-windows-smoke.sh +++ b/scripts/e2e/parallels-windows-smoke.sh @@ -2398,8 +2398,9 @@ New-Item -ItemType Directory -Path $stateDir -Force | Out-Null Remove-Item (Join-Path $workspace 'BOOTSTRAP.md') -Force -ErrorAction SilentlyContinue EOF )" + stop_gateway guest_run_openclaw "$API_KEY_ENV" "$API_KEY_VALUE" \ - agent --agent main --session-id parallels-windows-smoke --message "Reply with exact ASCII text OK only." --json + agent --local --agent main --session-id parallels-windows-smoke --message "Reply with exact ASCII text OK only." --json } capture_latest_ref_failure() { diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index 769051dec31..4a3d5cb6ec8 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -710,6 +710,7 @@ describe("runGatewayUpdate", () => { it("does not fail a good windows dev preflight only because worktree cleanup hit long paths", async () => { await setupGitPackageManagerFixture(); const calls: string[] = []; + const cleanupTimeouts: Array = []; const upstreamSha = "upstream123"; const doctorNodePath = await resolveStableNodePath(process.execPath); const doctorCommand = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`; @@ -718,7 +719,7 @@ describe("runGatewayUpdate", () => { try { const runCommand = async ( argv: string[], - _options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number }, + options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number }, ) => { const key = argv.join(" "); calls.push(key); @@ -772,6 +773,7 @@ describe("runGatewayUpdate", () => { key.startsWith(`git -C ${tempDir} worktree remove --force `) && preflightPrefixPattern.test(key) ) { + cleanupTimeouts.push(options?.timeoutMs); return { stdout: "", stderr: "error: failed to delete worktree: Filename too long", @@ -798,6 +800,7 @@ describe("runGatewayUpdate", () => { expect(result.status).toBe("ok"); const cleanupStep = result.steps.find((step) => step.name === "preflight cleanup"); expect(cleanupStep?.exitCode).toBe(0); + expect(cleanupTimeouts[0]).toBeLessThanOrEqual(60_000); expect(cleanupStep?.stderrTail ?? "").toContain( "windows fallback cleanup removed preflight tree", ); @@ -806,6 +809,101 @@ describe("runGatewayUpdate", () => { } }); + it("falls back when dev preflight worktree cleanup times out", async () => { + await setupGitPackageManagerFixture(); + const calls: string[] = []; + const cleanupTimeouts: Array = []; + const upstreamSha = "upstream123"; + 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(" "); + calls.push(key); + + 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" || key === "pnpm build" || key === "pnpm lint") { + return { stdout: "", stderr: "", code: 0 }; + } + if ( + key.startsWith(`git -C ${tempDir} worktree remove --force `) && + preflightPrefixPattern.test(key) + ) { + cleanupTimeouts.push(options?.timeoutMs); + return { + stdout: "", + stderr: "Command timed out after 60000ms", + code: null, + }; + } + 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 }; + } + if (key === "pnpm ui:build") { + return { stdout: "", stderr: "", code: 0 }; + } + return { stdout: "", stderr: "", code: 0 }; + }; + + const result = await runWithCommand(runCommand, { channel: "dev" }); + + expect(result.status).toBe("ok"); + const cleanupStep = result.steps.find((step) => step.name === "preflight cleanup"); + expect(cleanupStep?.exitCode).toBe(0); + expect(cleanupTimeouts[0]).toBeLessThanOrEqual(60_000); + expect(cleanupStep?.stderrTail ?? "").toContain("fallback cleanup removed preflight tree"); + }); + it("adds heap headroom to windows pnpm build steps during dev updates", async () => { await setupGitPackageManagerFixture(); const upstreamSha = "upstream123"; diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index ac8da655674..2623382b8b9 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -138,6 +138,7 @@ const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]); const PREFLIGHT_TEMP_PREFIX = process.platform === "win32" ? "ocu-pf-" : "openclaw-update-preflight-"; const PREFLIGHT_WORKTREE_DIRNAME = process.platform === "win32" ? "wt" : "worktree"; +const PREFLIGHT_CLEANUP_TIMEOUT_MS = 60_000; const WINDOWS_PREFLIGHT_BASE_DIR = "ocu"; const WINDOWS_BUILD_MAX_OLD_SPACE_MB = 4096; @@ -215,10 +216,7 @@ async function removePathRecursive(target: string) { .catch(() => {}); } -async function repairWindowsPreflightCleanup(worktreeDir: string, preflightRoot: string) { - if (process.platform !== "win32") { - return false; - } +async function repairPreflightCleanup(worktreeDir: string, preflightRoot: string) { try { await fs.rm(worktreeDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }); await fs.rm(preflightRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }); @@ -938,22 +936,25 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< break; } } finally { - const removeStep = await runStep( - step( + const removeStep = await runStep({ + ...step( "preflight cleanup", ["git", "-C", gitRoot, "worktree", "remove", "--force", worktreeDir], gitRoot, ), - ); + timeoutMs: Math.min(timeoutMs, PREFLIGHT_CLEANUP_TIMEOUT_MS), + }); if ( removeStep.exitCode !== 0 && - (await repairWindowsPreflightCleanup(worktreeDir, preflightRoot)) + (await repairPreflightCleanup(worktreeDir, preflightRoot)) ) { removeStep.exitCode = 0; + const fallbackMessage = + process.platform === "win32" + ? "windows fallback cleanup removed preflight tree" + : "fallback cleanup removed preflight tree"; removeStep.stderrTail = trimLogTail( - [removeStep.stderrTail, "windows fallback cleanup removed preflight tree"] - .filter(Boolean) - .join("\n"), + [removeStep.stderrTail, fallbackMessage].filter(Boolean).join("\n"), MAX_LOG_CHARS, ); }