diff --git a/CHANGELOG.md b/CHANGELOG.md index 82475948d1d..4f4d4916fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai - Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs. - Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that `pnpm build` includes `ui:build`. - Tests: give QA config mutation RPCs enough native Windows budget to finish gateway config writes and restart settle after hot scenario runs. +- Crabbox: sync clean sparse worktrees through a temporary full checkout even when reusing an existing lease so tracked build-time files are not omitted. - Build: route `scripts/ui.js` through the shared pnpm runner and keep Control UI chunking helpers in sparse-included source so native Windows Corepack builds can produce `dist/control-ui`. - Tests: give the memory fallback QA scenario enough turn budget to exercise native Windows gateway runs instead of failing on the client timeout while the mock agent is still dispatching. - Tests: collect QA gateway CPU/RSS metrics on native Windows and give the channel baseline enough turn budget to report slow gateway runs instead of timing out before proof. diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index bfd6158b99c..24f5fd8a652 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -679,7 +679,7 @@ function shouldUseFullCheckoutForCleanSparseRemoteSync(commandArgs, providerName if (commandArgs[0] !== "run" || isLocalContainerProvider(providerName)) { return false; } - if (hasOption(commandArgs, "--no-sync") || hasOption(commandArgs, "--id")) { + if (hasOption(commandArgs, "--no-sync")) { return false; } diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index 3cb3e4eb438..e3aac449705 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -63,7 +63,11 @@ function makeFakeGit( "process.exit(response.status ?? 0);", ].join("\n"); writeFileSync(gitPath, `${script}\n`, "utf8"); - writeFileSync(`${gitPath}.cmd`, `@echo off\r\n"${process.execPath}" "%~dp0git" %*\r\n`, "utf8"); + writeFileSync( + `${gitPath}.cmd`, + `@echo off\r\n"${process.execPath}" "%~dp0git" %*\r\n`, + "utf8", + ); chmodSync(gitPath, 0o755); return binDir; } @@ -283,35 +287,40 @@ describe("scripts/crabbox-wrapper", () => { ]); }); - it("finds a Crabbox checkout next to the Git common dir in linked worktrees", () => { - const fakeWorkspaceParent = mkdtempSync(path.join(tmpdir(), "openclaw-linked-worktree-")); - tempDirs.push(fakeWorkspaceParent); - const gitCommonDir = path.join(fakeWorkspaceParent, "openclaw", ".git"); - const crabboxBinDir = path.join(fakeWorkspaceParent, "crabbox", "bin"); - mkdirSync(gitCommonDir, { recursive: true }); - writeFakeCrabbox(crabboxBinDir, "provider: aws\n"); - const gitResponses = { - ["rev-parse\u0000--git-common-dir"]: { stdout: `${gitCommonDir}\n` }, - }; - const gitBinDir = makeFakeGit(gitResponses); + const itWithPosixLinkedWorktreeFixture = process.platform === "win32" ? it.skip : it; - const result = spawnSync( - process.execPath, - ["scripts/crabbox-wrapper.mjs", "run", "--provider", "aws", "--", "echo ok"], - { - cwd: repoRoot, - encoding: "utf8", - env: { - ...process.env, - OPENCLAW_FAKE_GIT_RESPONSES: JSON.stringify(gitResponses), - PATH: [gitBinDir, path.dirname(process.execPath)].join(path.delimiter), + itWithPosixLinkedWorktreeFixture( + "finds a Crabbox checkout next to the Git common dir in linked worktrees", + () => { + const fakeWorkspaceParent = mkdtempSync(path.join(tmpdir(), "openclaw-linked-worktree-")); + tempDirs.push(fakeWorkspaceParent); + const gitCommonDir = path.join(fakeWorkspaceParent, "openclaw", ".git"); + const crabboxBinDir = path.join(fakeWorkspaceParent, "crabbox", "bin"); + mkdirSync(gitCommonDir, { recursive: true }); + writeFakeCrabbox(crabboxBinDir, "provider: aws\n"); + const gitResponses = { + ["rev-parse\u0000--git-common-dir"]: { stdout: `${gitCommonDir}\n` }, + }; + const gitBinDir = makeFakeGit(gitResponses); + + const result = spawnSync( + process.execPath, + ["scripts/crabbox-wrapper.mjs", "run", "--provider", "aws", "--", "echo ok"], + { + cwd: repoRoot, + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_FAKE_GIT_RESPONSES: JSON.stringify(gitResponses), + PATH: [gitBinDir, path.dirname(process.execPath)].join(path.delimiter), + }, }, - }, - ); + ); - expect(result.status).toBe(0); - expect(parseFakeCrabboxOutput(result).args).toContain("aws"); - }); + expect(result.status).toBe(0); + expect(parseFakeCrabboxOutput(result).args).toContain("aws"); + }, + ); it("accepts advertised providers from wrapped Crabbox help", () => { const result = runWrapper( @@ -355,18 +364,14 @@ describe("scripts/crabbox-wrapper", () => { } it("falls back to normal sync decisions when git is missing from PATH", () => { - const binDir = makeFakeCrabbox( + const result = runWrapper( "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", - ); - const result = spawnSync( - process.execPath, - ["scripts/crabbox-wrapper.mjs", "run", "--provider", "aws", "--", "echo ok"], + ["run", "--provider", "aws", "--", "echo ok"], { - cwd: repoRoot, - encoding: "utf8", - env: { - ...process.env, - PATH: [binDir, path.dirname(process.execPath)].join(path.delimiter), + gitResponses: { + ["rev-parse\u0000--git-common-dir"]: { status: 1 }, + ["config\u0000--bool\u0000core.sparseCheckout"]: { status: 1 }, + ["sparse-checkout\u0000list"]: { status: 1 }, }, }, ); @@ -493,6 +498,35 @@ describe("scripts/crabbox-wrapper", () => { expect(parseFakeCrabboxOutput(result).cwd).toContain("openclaw-crabbox-sync-"); }); + it("uses a temporary full checkout when clean sparse AWS syncs reuse a lease", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + [ + "run", + "--provider", + "aws", + "--target", + "windows", + "--id", + "cbx_existing", + "--", + "corepack", + "pnpm", + "build", + ], + { + gitResponses: { + ["config\u0000--bool\u0000core.sparseCheckout"]: { stdout: "true\n" }, + ["status\u0000--porcelain=v1"]: { stdout: "" }, + }, + }, + ); + + expect(result.status).toBe(0); + expect(result.stderr).toContain("syncing from temporary full checkout"); + expect(parseFakeCrabboxOutput(result).cwd).toContain("openclaw-crabbox-sync-"); + }); + it("bootstraps Git metadata for sparse changed gates on remote raw syncs", () => { const result = runWrapper( "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", @@ -626,7 +660,7 @@ describe("scripts/crabbox-wrapper", () => { expect(parseFakeCrabboxOutput(result).cwd).toBe(repoRoot); }); - it("keeps existing AWS leases on the original sparse checkout", () => { + it("uses a temporary full checkout when existing AWS leases sync clean sparse worktrees", () => { const result = runWrapper( "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", ["run", "--provider", "aws", "--id", "cbx_existing", "--", "echo ok"], @@ -639,8 +673,8 @@ describe("scripts/crabbox-wrapper", () => { ); expect(result.status).toBe(0); - expect(result.stderr).not.toContain("syncing from temporary full checkout"); - expect(parseFakeCrabboxOutput(result).cwd).toBe(repoRoot); + expect(result.stderr).toContain("syncing from temporary full checkout"); + expect(parseFakeCrabboxOutput(result).cwd).toContain("openclaw-crabbox-sync-"); }); it("uses a temporary full checkout when clean sparse branches differ from the Blacksmith ref", () => {