From dcb6b0dd6f6a2be647d80cae29fc83e4db767b81 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 23 Jun 2026 06:38:27 +0800 Subject: [PATCH] fix(ci): restore macOS and Windows QA gates Restores Azure native Windows hydrated node_modules bootstrap, fixes the macOS settings SwiftFormat drift, and stabilizes lifecycle process-group CI proof. Proof: - `tbx_01kvrpr5kfc58wdnakx2zkc4k6`: `corepack pnpm test:serial test/scripts/plugin-lifecycle-measure.test.ts` passed. - `tbx_01kvrpvcrmsxgyb886pa127qq3`: `OPENCLAW_TESTBOX=1 ... corepack pnpm check:changed` passed. - `tbx_01kvrpzpafmp27tyb4tg9yvwvz`: touched-file `format:check` passed. - PR CI `27988226071` passed, including `macos-node`, `macos-swift`, and `checks-node-compact-small-whole-2`. --- .../Sources/OpenClaw/GeneralSettings.swift | 1 - scripts/crabbox-wrapper.mjs | 6 +- test/scripts/crabbox-wrapper.test.ts | 26 ++++++ test/scripts/plugin-lifecycle-measure.test.ts | 87 +++++++++---------- 4 files changed, 74 insertions(+), 46 deletions(-) diff --git a/apps/macos/Sources/OpenClaw/GeneralSettings.swift b/apps/macos/Sources/OpenClaw/GeneralSettings.swift index 62844c10de0..6412a76eb2b 100644 --- a/apps/macos/Sources/OpenClaw/GeneralSettings.swift +++ b/apps/macos/Sources/OpenClaw/GeneralSettings.swift @@ -680,7 +680,6 @@ struct GeneralSettings: View { case .missingNode, .missingGateway, .incompatible, .error: .orange } } - } private enum RemoteStatus: Equatable { diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index 93b089e2d6e..7385c0ea390 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -1987,6 +1987,10 @@ function isAwsMacosRemoteTarget(commandArgs, providerName) { ); } +function isHydratedNativeWindowsProvider(providerName) { + return providerName === "aws" || providerName === "azure"; +} + function remoteWindowsHydratedNodeModulesBootstrap() { return [ "$openclawModulesDir = $env:PNPM_CONFIG_MODULES_DIR", @@ -2004,7 +2008,7 @@ function injectRemoteWindowsHydratedNodeModulesBootstrap(commandArgs, providerNa const runtimeEntrypoint = commandRuntimeEntrypoint(runCommandArgs(commandArgs)); if ( commandArgs[0] !== "run" || - providerName !== "aws" || + !isHydratedNativeWindowsProvider(providerName) || !isNativeWindowsRemoteTarget(commandArgs) || !hasOption(commandArgs, "--id") || !runtimeEntrypoint diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index e8de320d8ac..d65440d94b5 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -2407,6 +2407,32 @@ describe.concurrent("scripts/crabbox-wrapper", () => { expect(remoteCommand).toContain("corepack pnpm check:changed"); }); + it("restores hydrated node_modules before Azure native Windows shell commands", () => { + const result = runWrapper("provider: hetzner, aws, azure, local-container\n", [ + "run", + "--provider", + "azure", + "--target", + "windows", + "--windows-mode", + "normal", + "--id", + "cbx_test", + "--shell", + "--", + "corepack pnpm check:changed", + ]); + + const output = parseFakeCrabboxOutput(result); + const remoteCommand = output.args.at(-1) ?? ""; + expect(result.status).toBe(0); + expect(output.args).toContain("--shell"); + expect(remoteCommand).toContain("$openclawModulesDir = $env:PNPM_CONFIG_MODULES_DIR"); + expect(remoteCommand).toContain('mklink /J "$openclawSelfModules" "$openclawModulesDir"'); + expect(remoteCommand).toContain('mklink /J "$openclawWorkspaceModules" "$openclawModulesDir"'); + expect(remoteCommand).toContain("corepack pnpm check:changed"); + }); + it("restores hydrated node_modules before AWS native Windows direct commands", () => { const result = runWrapper("provider: hetzner, aws, azure, local-container\n", [ "run", diff --git a/test/scripts/plugin-lifecycle-measure.test.ts b/test/scripts/plugin-lifecycle-measure.test.ts index c38f53caa90..7ed3361ae96 100644 --- a/test/scripts/plugin-lifecycle-measure.test.ts +++ b/test/scripts/plugin-lifecycle-measure.test.ts @@ -15,9 +15,6 @@ import { afterEach, describe, expect, it } from "vitest"; const tempDirs: string[] = []; const scriptPath = "scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs"; -const hasTimeoutCommand = - process.platform === "linux" && - spawnSync("bash", ["-lc", "command -v timeout >/dev/null 2>&1"]).status === 0; function makeTempDir(): string { const dir = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-lifecycle-measure-")); @@ -317,8 +314,8 @@ describe("plugin lifecycle resource sampler", () => { encoding: "utf8", env: { ...process.env, - OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "150", - OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "100", + OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "1000", + OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "200", PID_FILE: pidFile, }, timeout: 5000, @@ -340,49 +337,51 @@ describe("plugin lifecycle resource sampler", () => { }, ); - it.runIf(hasTimeoutCommand)("forwards external termination to the measured process group", () => { - const dir = makeTempDir(); - const summary = path.join(dir, "summary.tsv"); - const pidFile = path.join(dir, "descendant.pid"); - let descendantPid; + it.runIf(process.platform === "linux")( + "forwards external termination to the measured process group", + async () => { + const dir = makeTempDir(); + const summary = path.join(dir, "summary.tsv"); + const pidFile = path.join(dir, "descendant.pid"); + let descendantPid; - try { - const result = spawnSync( - "timeout", - [ - "--kill-after=1s", - "0.2s", - "node", - scriptPath, - summary, - "external-stop", - "--", - "bash", - "-lc", - 'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' & wait', - ], - { - cwd: process.cwd(), - encoding: "utf8", - env: { - ...process.env, - OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "5000", - OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "100", - PID_FILE: pidFile, + try { + const result = spawn( + process.execPath, + [ + scriptPath, + summary, + "external-stop", + "--", + "bash", + "-lc", + 'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' & wait', + ], + { + cwd: process.cwd(), + env: { + ...process.env, + OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "5000", + OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "200", + PID_FILE: pidFile, + }, + stdio: "ignore", }, - timeout: 5000, - }, - ); + ); - descendantPid = Number.parseInt(readFileSync(pidFile, "utf8"), 10); - expect(result.status).toBe(124); - expect(waitForPidExit(descendantPid, 1000)).toBe(true); - } finally { - if (descendantPid > 0 && pidExists(descendantPid)) { - process.kill(descendantPid, "SIGKILL"); + expect(waitForPath(pidFile, 2000)).toBe(true); + descendantPid = Number.parseInt(readFileSync(pidFile, "utf8"), 10); + result.kill("SIGTERM"); + const close = await waitForChildClose(result, 5000); + expect(close.signal).toBe("SIGTERM"); + expect(waitForPidExit(descendantPid, 1000)).toBe(true); + } finally { + if (descendantPid > 0 && pidExists(descendantPid)) { + process.kill(descendantPid, "SIGKILL"); + } } - } - }); + }, + ); it.runIf(process.platform === "linux")( "exits promptly when externally terminated phases stop during grace",