diff --git a/scripts/pr-lib/worktree.sh b/scripts/pr-lib/worktree.sh index 00de55aa71f3..323fa63e65ad 100644 --- a/scripts/pr-lib/worktree.sh +++ b/scripts/pr-lib/worktree.sh @@ -29,6 +29,16 @@ EOF return 1 } +ensure_full_pr_worktree_checkout() { + local sparse_checkout + sparse_checkout=$(git config --bool core.sparseCheckout 2>/dev/null || true) + if [ "$sparse_checkout" = "true" ]; then + # Prepare gates build the whole repository. Inherited sparse settings can + # omit tracked transitive inputs and turn healthy PRs into false failures. + git sparse-checkout disable + fi +} + enter_worktree() { local pr="$1" local reset_to_main="${2:-false}" @@ -81,6 +91,7 @@ enter_worktree() { return 1 fi + ensure_full_pr_worktree_checkout git fetch origin main if [ "$reset_to_main" = "true" ]; then git checkout -B "temp/pr-$pr" origin/main diff --git a/src/agents/provider-local-service.test.ts b/src/agents/provider-local-service.test.ts index eb2c7c174ab9..490022666895 100644 --- a/src/agents/provider-local-service.test.ts +++ b/src/agents/provider-local-service.test.ts @@ -820,6 +820,9 @@ describe("provider local service", () => { const tempDir = tempDirs.make("openclaw-local-service-failed-unref-"); const servicePidPath = path.join(tempDir, "service.pid"); const moduleUrl = new URL("./provider-local-service.ts", import.meta.url).href; + // The assertion targets diagnostic-pipe cleanup after failed readiness. + // Give the nested Node child enough startup time under loaded CI. + const failedServiceReadyTimeoutMs = 5_000; const serviceScript = [ `const fs=require("node:fs");`, `fs.writeFileSync(${JSON.stringify(servicePidPath)},String(process.pid));`, @@ -837,7 +840,7 @@ describe("provider local service", () => { ` service: {`, ` command: process.execPath,`, ` args: ["-e", ${JSON.stringify(serviceScript)}],`, - ` readyTimeoutMs: 100,`, + ` readyTimeoutMs: ${failedServiceReadyTimeoutMs},`, ` },`, ` });`, `} catch {}`, diff --git a/test/scripts/pr-operation-lock.test.ts b/test/scripts/pr-operation-lock.test.ts index 606d057ff419..3b4951f2ad92 100644 --- a/test/scripts/pr-operation-lock.test.ts +++ b/test/scripts/pr-operation-lock.test.ts @@ -96,6 +96,22 @@ function createRepo(nestedName?: string) { return dir; } +function addTrackedUiConfig(repoDir: string) { + const configDir = join(repoDir, "ui", "config"); + mkdirSync(configDir, { recursive: true }); + writeFileSync(join(configDir, "control-ui-chunking.ts"), "export const chunking = true;\n"); + execFileSync("git", ["add", "ui/config/control-ui-chunking.ts"], { cwd: repoDir }); + execFileSync("git", ["commit", "-qm", "add ui config"], { cwd: repoDir }); +} + +function setSparseCheckout(repoDir: string) { + execFileSync("git", ["sparse-checkout", "init", "--no-cone"], { cwd: repoDir }); + execFileSync("git", ["sparse-checkout", "set", "--no-cone", "--stdin"], { + cwd: repoDir, + input: "/*\n!/*/\n/base.txt\n", + }); +} + function bashSource(repoDir: string, supervised = false) { return [ "set -euo pipefail", @@ -2193,6 +2209,54 @@ describePosix("scripts/pr per-PR operation lock", () => { ).toBe("temp/pr-43"); }); + it("materializes a new PR worktree inherited from a sparse checkout", () => { + const repoDir = createRepo(); + addTrackedUiConfig(repoDir); + execFileSync("git", ["remote", "add", "origin", repoDir], { cwd: repoDir }); + setSparseCheckout(repoDir); + const worktreeDir = join(repoDir, ".worktrees", "pr-44"); + + const result = runLockShell(repoDir, [ + "ensure_gh_api_auth() { return 0; }", + "enter_worktree 44", + ]); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(existsSync(join(worktreeDir, "ui", "config", "control-ui-chunking.ts"))).toBe(true); + expect( + execFileSync("git", ["config", "--bool", "core.sparseCheckout"], { + cwd: worktreeDir, + encoding: "utf8", + }).trim(), + ).toBe("false"); + }); + + it("materializes an existing sparse PR worktree before reuse", () => { + const repoDir = createRepo(); + addTrackedUiConfig(repoDir); + execFileSync("git", ["remote", "add", "origin", repoDir], { cwd: repoDir }); + const worktreeDir = join(repoDir, ".worktrees", "pr-45"); + execFileSync("git", ["worktree", "add", "-q", "-b", "temp/pr-45", worktreeDir], { + cwd: repoDir, + }); + setSparseCheckout(worktreeDir); + expect(existsSync(join(worktreeDir, "ui", "config", "control-ui-chunking.ts"))).toBe(false); + + const result = runLockShell(repoDir, [ + "ensure_gh_api_auth() { return 0; }", + "enter_worktree 45", + ]); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(existsSync(join(worktreeDir, "ui", "config", "control-ui-chunking.ts"))).toBe(true); + expect( + execFileSync("git", ["config", "--bool", "core.sparseCheckout"], { + cwd: worktreeDir, + encoding: "utf8", + }).trim(), + ).toBe("false"); + }); + it("refuses a symlink alias to another registered worktree", () => { const repoDir = createRepo(); const worktreesDir = join(repoDir, ".worktrees"); diff --git a/ui/src/pages/config/config-page.test.ts b/ui/src/pages/config/config-page.test.ts index df7e39347696..5a40935c4256 100644 --- a/ui/src/pages/config/config-page.test.ts +++ b/ui/src/pages/config/config-page.test.ts @@ -29,6 +29,7 @@ function deferred() { let localStorageMock: Storage; beforeEach(() => { + window.history.replaceState({}, "", "/"); vi.spyOn(realtimeTalk, "switchActiveRealtimeTalkCameras").mockImplementation( switchActiveRealtimeTalkCameras, );