From 0f82c810fcbf5157cc3bff0dbc82db1454858daf Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 24 May 2026 13:46:46 +0200 Subject: [PATCH] fix(test): sync sparse AWS Crabbox runs from full checkout --- CHANGELOG.md | 1 + scripts/crabbox-wrapper.mjs | 69 +++++++++++++++++++++++----- test/scripts/crabbox-wrapper.test.ts | 53 +++++++++++++++++++++ 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63106e1a747d..4ace164872a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes ### Fixes +- Crabbox: sync clean sparse-checkout remote changed gates from a temporary full checkout with local-only commits overlaid as worktree changes so git-backed script checks can seed the runner repository. - Tests: make startup memory and startup bench smoke scripts build CLI startup artifacts when run from a fresh source checkout. - iMessage: mark authorized slash-command turns as text-sourced commands so `/status`, `/new`, and `/restart` acknowledgements return to the source conversation. (#82642) thanks @homer-byte. - Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`. diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index 7177cca75502..0c723cb45143 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -426,8 +426,8 @@ function runCommandArgs(commandArgs) { return start >= 0 ? commandArgs.slice(start) : []; } -function commandRuntimeEntrypoint(commandArgs) { - const words = commandArgs.length === 1 ? commandArgs[0].split(/\s+/u) : commandArgs; +function normalizedCommandWords(commandArgs) { + const words = commandArgs.length === 1 ? commandArgs[0].split(/\s+/u) : [...commandArgs]; while (words[0] === "env") { words.shift(); while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0] ?? "")) { @@ -437,13 +437,43 @@ function commandRuntimeEntrypoint(commandArgs) { while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0] ?? "")) { words.shift(); } - const first = (words[0] ?? "") - .replace(/^['"]|['";|&()]+$/g, "") - .split("/") - .pop(); + return words.map((word) => word.replace(/^['"]|['";|&()]+$/g, "")); +} + +function commandRuntimeEntrypoint(commandArgs) { + const words = normalizedCommandWords(commandArgs); + const first = (words[0] ?? "").split("/").pop(); return ["pnpm", "npm", "npx", "corepack", "node", "yarn", "bun"].includes(first) ? first : ""; } +function isChangedGateCommand(commandArgs) { + const words = normalizedCommandWords(commandArgs); + if (words[0] === "corepack") { + words.shift(); + } + return ( + (words[0] === "pnpm" && words[1] === "check:changed") || + (words[0] === "pnpm" && words[1] === "run" && words[2] === "check:changed") || + (words[0] === "node" && (words[1] ?? "").endsWith("scripts/check-changed.mjs")) + ); +} + +function headInRemoteRefs() { + const refs = gitOutput([ + "for-each-ref", + "--contains", + "HEAD", + "--format=%(refname)", + "refs/remotes", + ]); + return refs.status === 0 && refs.stdout !== ""; +} + +function mergeBaseForChangedGate() { + const base = gitOutput(["merge-base", "origin/main", "HEAD"]); + return base.status === 0 && base.stdout ? base.stdout : "origin/main"; +} + function isSparseCheckout() { const config = gitOutput(["config", "--bool", "core.sparseCheckout"]); if (config.status === 0 && config.stdout === "true") { @@ -457,8 +487,8 @@ function isWorktreeClean() { return gitOutput(["status", "--porcelain=v1"]).stdout === ""; } -function shouldUseFullCheckoutForCleanSparseBlacksmithSync(commandArgs, providerName) { - if (commandArgs[0] !== "run" || providerName !== "blacksmith-testbox") { +function shouldUseFullCheckoutForCleanSparseRemoteSync(commandArgs, providerName) { + if (commandArgs[0] !== "run" || isLocalContainerProvider(providerName)) { return false; } if ( @@ -471,7 +501,7 @@ function shouldUseFullCheckoutForCleanSparseBlacksmithSync(commandArgs, provider return isSparseCheckout() && isWorktreeClean(); } -function prepareFullCheckoutForSync() { +function prepareFullCheckoutForSync(options = {}) { const dir = mkdtempSync(resolve(tmpdir(), "openclaw-crabbox-sync-")); let active = false; const add = gitOutput(["worktree", "add", "--detach", dir, "HEAD"]); @@ -487,8 +517,17 @@ function prepareFullCheckoutForSync() { throw new Error(`git sparse-checkout disable failed: ${disableSparse.text}`); } + if (options.changedGateBase) { + const reset = gitOutput(["-C", dir, "reset", "--mixed", "--quiet", options.changedGateBase]); + if (reset.status !== 0) { + cleanupFullCheckout(dir, active); + throw new Error(`git reset for changed-gate sync failed: ${reset.text}`); + } + } + return { dir, + changedGateBase: options.changedGateBase ?? "", cleanup() { cleanupFullCheckout(dir, active); active = false; @@ -645,13 +684,21 @@ if (provider === "blacksmith-testbox") { let childCwd = repoRoot; let cleanupChildCwd = () => {}; let cleanupDone = false; -if (shouldUseFullCheckoutForCleanSparseBlacksmithSync(args, provider)) { - const checkout = prepareFullCheckoutForSync(); +if (shouldUseFullCheckoutForCleanSparseRemoteSync(args, provider)) { + const runWords = runCommandArgs(args); + const changedGateBase = + isChangedGateCommand(runWords) && !headInRemoteRefs() ? mergeBaseForChangedGate() : ""; + const checkout = prepareFullCheckoutForSync({ changedGateBase }); childCwd = checkout.dir; cleanupChildCwd = () => checkout.cleanup(); console.error( `[crabbox] sparse clean checkout detected; syncing from temporary full checkout ${checkout.dir}`, ); + if (checkout.changedGateBase) { + console.error( + `[crabbox] remote changed gate detected; overlaying local HEAD as worktree changes from ${checkout.changedGateBase}`, + ); + } } function cleanupOnce() { diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index 118f44a1d728..6758786a137c 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -45,6 +45,7 @@ function makeFakeGit(responses: Record { expect(parseFakeCrabboxOutput(result).cwd).toContain("openclaw-crabbox-sync-"); }); + it("uses a temporary full checkout for clean sparse AWS syncs", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "aws", "--", "corepack", "pnpm", "check:changed"], + { + 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(result.stderr).toContain("overlaying local HEAD as worktree changes from origin/main"); + expect(parseFakeCrabboxOutput(result).cwd).toContain("openclaw-crabbox-sync-"); + }); + + it("keeps clean sparse local-container syncs on the original checkout", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "local-container", "--", "echo ok"], + { + gitResponses: { + ["config\u0000--bool\u0000core.sparseCheckout"]: { stdout: "true\n" }, + ["status\u0000--porcelain=v1"]: { stdout: "" }, + }, + }, + ); + + expect(result.status).toBe(0); + expect(result.stderr).not.toContain("syncing from temporary full checkout"); + expect(parseFakeCrabboxOutput(result).cwd).toBe(repoRoot); + }); + + it("keeps existing AWS leases on the original sparse checkout", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "aws", "--id", "cbx_existing", "--", "echo ok"], + { + gitResponses: { + ["config\u0000--bool\u0000core.sparseCheckout"]: { stdout: "true\n" }, + ["status\u0000--porcelain=v1"]: { stdout: "" }, + }, + }, + ); + + expect(result.status).toBe(0); + expect(result.stderr).not.toContain("syncing from temporary full checkout"); + expect(parseFakeCrabboxOutput(result).cwd).toBe(repoRoot); + }); + it("uses a temporary full checkout when clean sparse branches differ from the Blacksmith ref", () => { const result = runWrapper( "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",