fix(scripts): materialize PR worktrees before gates (#115120)

* fix(scripts): materialize PR worktrees before gates

* test(agents): stabilize failed local service fixture

* test(ui): isolate config route location
This commit is contained in:
Vincent Koc
2026-07-28 23:38:17 +08:00
committed by GitHub
parent 9ae79eed2d
commit cbe19ed3be
4 changed files with 80 additions and 1 deletions

View File

@@ -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

View File

@@ -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 {}`,

View File

@@ -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");

View File

@@ -29,6 +29,7 @@ function deferred<T>() {
let localStorageMock: Storage;
beforeEach(() => {
window.history.replaceState({}, "", "/");
vi.spyOn(realtimeTalk, "switchActiveRealtimeTalkCameras").mockImplementation(
switchActiveRealtimeTalkCameras,
);