From 32f91503bed868284dbc79e9485ca30f9bce8bae Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 24 May 2026 00:37:29 +0200 Subject: [PATCH] fix(scripts): harden Windows QA runners --- CHANGELOG.md | 2 ++ docs/.generated/config-baseline.sha256 | 8 ++--- extensions/meeting-notes/package.json | 2 +- scripts/check-gateway-cpu-scenarios.mjs | 38 ++++++++++++++--------- scripts/check-plugin-gateway-gauntlet.mjs | 20 +++++++++--- scripts/check-tsgo-core-boundary.mjs | 10 ++++-- scripts/e2e/kitchen-sink-rpc-walk.mjs | 2 +- scripts/lib/run-extension-oxlint.mjs | 11 +++++-- scripts/lib/vitest-batch-runner.mjs | 29 +++++++++-------- scripts/openclaw-prepack.ts | 20 +++++++++--- scripts/profile-tsgo.mjs | 11 +++++-- scripts/release-preflight.mjs | 24 ++++++-------- scripts/run-vitest-profile.mjs | 27 +++++++++++++--- scripts/test-live-media.ts | 15 ++------- test/scripts/run-vitest-profile.test.ts | 21 +++++++++++++ 15 files changed, 158 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 180a7f81f24..92f3cce81c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ Docs: https://docs.openclaw.ai - OpenAI/images: route Codex API-key image generation through the native OpenAI Images API instead of the Codex OAuth streaming backend, avoiding 401s from valid API keys. - Agents/OpenAI completions: omit empty tool payload fields for proxy-like OpenAI-compatible endpoints so strict vLLM-style servers accept tool-free turns. (#85835) Thanks @rendrag-git. - Sandbox: keep workspace skill mounts read-only for remote container-cwd file operations and reject symlinked skill roots before creating protected overlays. (#85591) Thanks @jason-allen-oneal. +- Scripts/Windows: route remaining QA, release, profile, and live-media `pnpm` launches through the managed runner so native Windows avoids brittle `.cmd` execution and shell-argv warnings. +- Release: align generated config/API baselines and the meeting-notes plugin version so release preflight stays green on native Windows. - Checks/Windows: route full `pnpm check` stage commands through the managed child runner so Windows avoids Node shell-argv deprecation warnings there too. - Checks/Windows: run managed child commands through explicit `cmd.exe` wrapping instead of Node shell mode with argv, avoiding Node 24 subprocess deprecation warnings during changed checks. - Gateway: omit internal stream-error placeholder entries from agent prompt history so failed assistant turns are not replayed as model-authored text. (#85652) Thanks @anyech. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index a7235822080..0b38f512e7f 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -5482b1a125a5c41856f6f49dfd70e2efe9e52a7cc0e2d4c24a56d99adfeda6be config-baseline.json -3d686075da4d4f6c6319c3247e93f486a6c48314a28a2961cd4acab7f3fa5389 config-baseline.core.json -11839c7a1b858c66075156f0e203aa8367cd8321047684679a18e18b7c8fe1f7 config-baseline.channel.json -5c214ab364011fd95735755f9fa4298aa4de8ad81144ae8dd08d969bb7ba318b config-baseline.plugin.json +e0fee2c8da83aa14f45b9e00bde313af1d7e191c3b6ab892efea5799d9a1e9fe config-baseline.json +c746fff7ae66db26fbad9add82c2f4cb23570cb18582b48c0b7061046a7e07fa config-baseline.core.json +859b021f65400df22c95ae55b074cf26c83d3a0bfadb3fceeaca522f6ea391ae config-baseline.channel.json +74441e331aabb3026784c148d4ee5ce3f489a15ed87ffd9b7ba0c5e2a7bc93be config-baseline.plugin.json diff --git a/extensions/meeting-notes/package.json b/extensions/meeting-notes/package.json index 2f350a24fd6..dd2a62c4ca1 100644 --- a/extensions/meeting-notes/package.json +++ b/extensions/meeting-notes/package.json @@ -1,6 +1,6 @@ { "name": "@openclaw/meeting-notes", - "version": "2026.5.21", + "version": "2026.5.22", "private": true, "description": "OpenClaw meeting notes plugin", "type": "module", diff --git a/scripts/check-gateway-cpu-scenarios.mjs b/scripts/check-gateway-cpu-scenarios.mjs index 3d8a99426b7..34ae4da0c15 100644 --- a/scripts/check-gateway-cpu-scenarios.mjs +++ b/scripts/check-gateway-cpu-scenarios.mjs @@ -5,6 +5,7 @@ import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import { collectGatewayCpuObservations } from "./lib/plugin-gateway-gauntlet.mjs"; +import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs"; const DEFAULT_STARTUP_CASES = ["default", "oneInternalHook", "allInternalHooks"]; const DEFAULT_QA_SCENARIOS = [ @@ -136,20 +137,26 @@ function readJsonIfExists(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8")); } -function runStep(name, command, args) { +function runStep(name, command, args, options = {}) { console.error(`[gateway-cpu] start ${name}`); const result = spawnSync(command, args, { cwd: process.cwd(), env: process.env, stdio: "inherit", + ...options, }); const status = result.status ?? (result.signal ? 1 : 0); console.error(`[gateway-cpu] ${status === 0 ? "pass" : "fail"} ${name}`); return { name, status, signal: result.signal ?? null }; } -function pnpmCommand() { - return process.platform === "win32" ? "pnpm.cmd" : "pnpm"; +function pnpmCommand(args) { + return createPnpmRunnerSpawnSpec({ + cwd: process.cwd(), + env: process.env, + pnpmArgs: args, + stdio: "inherit", + }); } function toRepoRelativePath(absolutePath) { @@ -187,19 +194,20 @@ async function main() { } if (!options.skipQa) { + const qaCommand = pnpmCommand([ + "openclaw", + "qa", + "suite", + "--provider-mode", + "mock-openai", + "--concurrency", + "1", + "--output-dir", + qaOutputArg, + ...options.qaScenarios.flatMap((id) => ["--scenario", id]), + ]); steps.push( - runStep("qa suite", pnpmCommand(), [ - "openclaw", - "qa", - "suite", - "--provider-mode", - "mock-openai", - "--concurrency", - "1", - "--output-dir", - qaOutputArg, - ...options.qaScenarios.flatMap((id) => ["--scenario", id]), - ]), + runStep("qa suite", qaCommand.command, qaCommand.args, qaCommand.options), ); } diff --git a/scripts/check-plugin-gateway-gauntlet.mjs b/scripts/check-plugin-gateway-gauntlet.mjs index b9e0cf2f28c..f96f310a0fd 100644 --- a/scripts/check-plugin-gateway-gauntlet.mjs +++ b/scripts/check-plugin-gateway-gauntlet.mjs @@ -14,6 +14,7 @@ import { discoverBundledPluginManifests, selectPluginEntries, } from "./lib/plugin-gateway-gauntlet.mjs"; +import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs"; const DEFAULT_QA_SCENARIOS = [ "channel-chat-baseline", @@ -231,8 +232,13 @@ function parsePositiveNumber(raw, label) { return value; } -function pnpmCommand() { - return process.platform === "win32" ? "pnpm.cmd" : "pnpm"; +function pnpmCommand(args, { cwd, env }) { + return createPnpmRunnerSpawnSpec({ + cwd, + env, + pnpmArgs: args, + stdio: "pipe", + }); } function openclawCommand(repoRoot, args) { @@ -361,6 +367,7 @@ function runMeasuredCommand(params) { encoding: "utf8", timeout: params.timeoutMs, maxBuffer: 16 * 1024 * 1024, + ...(mode === "none" ? (params.spawnOptions ?? {}) : {}), }); const wallMs = performance.now() - started; const status = result.status ?? (result.signal ? 1 : 0); @@ -509,13 +516,16 @@ async function main() { const rows = []; if (!options.skipPrebuild && (selectedPlugins.length > 0 || !options.skipQa)) { process.stderr.write("[plugin-gauntlet] prebuild\n"); + const prebuildEnv = buildGauntletPrebuildEnv(env, { includePrivateQa: !options.skipQa }); + const prebuildCommand = pnpmCommand(["build"], { cwd: repoRoot, env: prebuildEnv }); rows.push( runMeasuredCommand({ cwd: repoRoot, - env: buildGauntletPrebuildEnv(env, { includePrivateQa: !options.skipQa }), + env: prebuildEnv, logDir: path.join(options.outputDir, "logs", "prebuild"), - command: pnpmCommand(), - args: ["build"], + command: prebuildCommand.command, + args: prebuildCommand.args, + spawnOptions: prebuildCommand.options, label: "prebuild", phase: "prebuild", timeoutMs: options.buildTimeoutMs, diff --git a/scripts/check-tsgo-core-boundary.mjs b/scripts/check-tsgo-core-boundary.mjs index 15c849b600e..9d4999af76e 100644 --- a/scripts/check-tsgo-core-boundary.mjs +++ b/scripts/check-tsgo-core-boundary.mjs @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import path from "node:path"; +import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs"; const repoRoot = path.resolve(import.meta.dirname, ".."); const tsgoPath = path.join(repoRoot, "node_modules", ".bin", "tsgo"); @@ -23,11 +24,16 @@ function normalizeFilePath(filePath) { } function listGraphFiles(graph) { - const result = spawnSync(tsgoPath, ["-p", graph.config, "--pretty", "false", "--listFilesOnly"], { + const tsgo = createManagedCommandInvocation({ + args: ["-p", graph.config, "--pretty", "false", "--listFilesOnly"], + bin: tsgoPath, + }); + const result = spawnSync(tsgo.command, tsgo.args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 256 * 1024 * 1024, - shell: process.platform === "win32", + shell: tsgo.shell, + windowsVerbatimArguments: tsgo.windowsVerbatimArguments, }); if (result.error) { throw result.error; diff --git a/scripts/e2e/kitchen-sink-rpc-walk.mjs b/scripts/e2e/kitchen-sink-rpc-walk.mjs index a7126f6623a..916363c1216 100644 --- a/scripts/e2e/kitchen-sink-rpc-walk.mjs +++ b/scripts/e2e/kitchen-sink-rpc-walk.mjs @@ -431,7 +431,7 @@ async function waitForGatewayReady(child, port, logPath) { lastError = error instanceof Error ? error.message : String(error); } if (fs.existsSync(logPath) && fs.readFileSync(logPath, "utf8").includes("[gateway] ready")) { - return; + lastError = `${lastError}; gateway log reported ready before HTTP readiness`; } await delay(250); } diff --git a/scripts/lib/run-extension-oxlint.mjs b/scripts/lib/run-extension-oxlint.mjs index c069c7f89cf..ef5823555e4 100644 --- a/scripts/lib/run-extension-oxlint.mjs +++ b/scripts/lib/run-extension-oxlint.mjs @@ -6,6 +6,7 @@ import { acquireLocalHeavyCheckLockSync, applyLocalOxlintPolicy, } from "./local-heavy-check-runtime.mjs"; +import { createManagedCommandInvocation } from "./managed-child-process.mjs"; export function runExtensionOxlint(params) { const repoRoot = process.cwd(); @@ -39,10 +40,16 @@ export function runExtensionOxlint(params) { const baseArgs = ["-c", tempConfigPath, ...process.argv.slice(2), ...extensionFiles]; const { args: finalArgs, env } = applyLocalOxlintPolicy(baseArgs, process.env); - const result = spawnSync(oxlintPath, finalArgs, { + const oxlint = createManagedCommandInvocation({ + args: finalArgs, + bin: oxlintPath, + env, + }); + const result = spawnSync(oxlint.command, oxlint.args, { stdio: "inherit", env, - shell: process.platform === "win32", + shell: oxlint.shell, + windowsVerbatimArguments: oxlint.windowsVerbatimArguments, }); if (result.error) { diff --git a/scripts/lib/vitest-batch-runner.mjs b/scripts/lib/vitest-batch-runner.mjs index c6feba253b4..e1b4689a616 100644 --- a/scripts/lib/vitest-batch-runner.mjs +++ b/scripts/lib/vitest-batch-runner.mjs @@ -1,6 +1,6 @@ -import { spawn } from "node:child_process"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { spawnPnpmRunner } from "../pnpm-runner.mjs"; import { installVitestProcessGroupCleanup, shouldUseDetachedVitestProcessGroup, @@ -9,21 +9,24 @@ import { const scriptFile = fileURLToPath(import.meta.url); const scriptDir = path.dirname(scriptFile); const repoRoot = path.resolve(scriptDir, "../.."); -const pnpm = "pnpm"; export async function runVitestBatch(params) { return await new Promise((resolve, reject) => { - const child = spawn( - pnpm, - ["exec", "vitest", "run", "--config", params.config, ...params.targets, ...params.args], - { - cwd: repoRoot, - detached: shouldUseDetachedVitestProcessGroup(), - stdio: "inherit", - shell: process.platform === "win32", - env: params.env, - }, - ); + const child = spawnPnpmRunner({ + cwd: repoRoot, + detached: shouldUseDetachedVitestProcessGroup(), + env: params.env, + pnpmArgs: [ + "exec", + "vitest", + "run", + "--config", + params.config, + ...params.targets, + ...params.args, + ], + stdio: "inherit", + }); const teardownChildCleanup = installVitestProcessGroupCleanup({ child }); child.on("error", (error) => { diff --git a/scripts/openclaw-prepack.ts b/scripts/openclaw-prepack.ts index c1f3d564989..f037afe5c99 100644 --- a/scripts/openclaw-prepack.ts +++ b/scripts/openclaw-prepack.ts @@ -1,10 +1,11 @@ #!/usr/bin/env -S node --import tsx -import { spawnSync } from "node:child_process"; +import { spawnSync, type SpawnSyncOptions } from "node:child_process"; import { existsSync, readdirSync } from "node:fs"; import { pathToFileURL } from "node:url"; import { formatErrorMessage } from "../src/infra/errors.ts"; import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts"; +import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs"; const requiredPreparedPathGroups = [ ["dist/index.js", "dist/index.mjs"], ["dist/control-ui/index.html"], @@ -90,10 +91,11 @@ function ensurePreparedArtifacts(): void { process.exit(1); } -function run(command: string, args: string[]): void { +function run(command: string, args: string[], options: SpawnSyncOptions = {}): void { const result = spawnSync(command, args, { stdio: "inherit", env: process.env, + ...options, }); if (result.status === 0) { return; @@ -101,6 +103,15 @@ function run(command: string, args: string[]): void { process.exit(result.status ?? 1); } +function runPnpm(args: string[]): void { + const command = createPnpmRunnerSpawnSpec({ + env: process.env, + pnpmArgs: args, + stdio: "inherit", + }); + run(command.command, command.args, command.options); +} + function runBuildSmoke(): void { run(process.execPath, ["scripts/test-built-bundled-channel-entry-smoke.mjs"]); } @@ -110,9 +121,8 @@ async function writeDistInventory(): Promise { } async function main(): Promise { - const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; - run(pnpmCommand, ["build"]); - run(pnpmCommand, ["ui:build"]); + runPnpm(["build"]); + runPnpm(["ui:build"]); ensurePreparedArtifacts(); await writeDistInventory(); runBuildSmoke(); diff --git a/scripts/profile-tsgo.mjs b/scripts/profile-tsgo.mjs index c378c197dc3..2e7151f4dd9 100644 --- a/scripts/profile-tsgo.mjs +++ b/scripts/profile-tsgo.mjs @@ -8,6 +8,7 @@ import { applyLocalTsgoPolicy, shouldAcquireLocalHeavyCheckLockForTsgo, } from "./lib/local-heavy-check-runtime.mjs"; +import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs"; const repoRoot = path.resolve(import.meta.dirname, ".."); const artifactRoot = path.resolve(repoRoot, ".artifacts/tsgo-profile"); @@ -139,12 +140,18 @@ function runTsgo(label, args, params = {}) { const startedAt = Date.now(); try { - const result = spawnSync(tsgoPath, finalArgs, { + const tsgo = createManagedCommandInvocation({ + args: finalArgs, + bin: tsgoPath, + env, + }); + const result = spawnSync(tsgo.command, tsgo.args, { cwd: repoRoot, env, encoding: "utf8", maxBuffer: params.maxBuffer ?? 128 * 1024 * 1024, - shell: process.platform === "win32", + shell: tsgo.shell, + windowsVerbatimArguments: tsgo.windowsVerbatimArguments, }); const elapsedMs = Date.now() - startedAt; const stdout = result.stdout ?? ""; diff --git a/scripts/release-preflight.mjs b/scripts/release-preflight.mjs index deb68778b8e..1906fcfe26f 100644 --- a/scripts/release-preflight.mjs +++ b/scripts/release-preflight.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { spawn } from "node:child_process"; +import { runManagedCommand } from "./lib/managed-child-process.mjs"; const args = new Set(process.argv.slice(2)); const fix = args.has("--fix"); @@ -9,8 +9,6 @@ if (fix && args.has("--check")) { process.exit(1); } -const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; - const fixCommands = [ { name: "plugin versions", args: ["plugins:sync"] }, { name: "plugin inventory", args: ["plugins:inventory:gen"] }, @@ -77,19 +75,15 @@ async function runAll(commands) { async function runCommand(command) { console.log(`\n[release-preflight] ${command.name}: pnpm ${command.args.join(" ")}`); - const child = spawn(pnpm, command.args, { - stdio: "inherit", - shell: false, - }); - return await new Promise((resolve) => { - child.once("error", (error) => { - console.error(error); - resolve(1); + try { + return await runManagedCommand({ + args: command.args, + bin: "pnpm", }); - child.once("close", (status) => { - resolve(status ?? 1); - }); - }); + } catch (error) { + console.error(error); + return 1; + } } function printFailures(title, failures) { diff --git a/scripts/run-vitest-profile.mjs b/scripts/run-vitest-profile.mjs index 477db53edc7..d064ab1ad74 100644 --- a/scripts/run-vitest-profile.mjs +++ b/scripts/run-vitest-profile.mjs @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { formatErrorMessage } from "./lib/error-format.mjs"; +import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs"; export function parseArgs(argv) { const args = { @@ -74,6 +75,25 @@ export function buildVitestProfileCommand({ mode, outputDir }) { }; } +export function buildVitestProfileSpawnSpec(plan, runnerOptions = {}) { + if (plan.command === "pnpm") { + return createPnpmRunnerSpawnSpec({ + ...runnerOptions, + env: runnerOptions.env ?? process.env, + pnpmArgs: plan.args, + stdio: "inherit", + }); + } + return { + args: plan.args, + command: plan.command, + options: { + env: process.env, + stdio: "inherit", + }, + }; +} + function main() { const parsed = parseArgs(process.argv.slice(2)); const outputDir = resolveVitestProfileDir(parsed); @@ -86,11 +106,8 @@ function main() { console.log(`[run-vitest-profile] writing ${parsed.mode} profiles to ${outputDir}`); - const result = spawnSync(plan.command, plan.args, { - stdio: "inherit", - shell: process.platform === "win32" && plan.command === "pnpm", - env: process.env, - }); + const spawnSpec = buildVitestProfileSpawnSpec(plan); + const result = spawnSync(spawnSpec.command, spawnSpec.args, spawnSpec.options); if (result.error) { throw result.error; diff --git a/scripts/test-live-media.ts b/scripts/test-live-media.ts index a7b26067006..8b8357192e6 100644 --- a/scripts/test-live-media.ts +++ b/scripts/test-live-media.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S node --import tsx -import { spawn, type ChildProcess } from "node:child_process"; +import type { ChildProcess } from "node:child_process"; import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; import { collectProviderApiKeys } from "../src/agents/live-auth-keys.js"; @@ -94,19 +94,10 @@ export type SuiteRunPlan = { }; function spawnLivePnpm(params: { pnpmArgs: string[]; env: NodeJS.ProcessEnv }): ChildProcess { - const npmExecPath = process.env.npm_execpath?.trim(); - if (npmExecPath) { - return spawn(process.execPath, [npmExecPath, ...params.pnpmArgs], { - stdio: "inherit", - env: params.env, - shell: false, - }); - } - - return spawn(process.platform === "win32" ? "pnpm.cmd" : "pnpm", params.pnpmArgs, { + return _spawnPnpmRunner({ + pnpmArgs: params.pnpmArgs, stdio: "inherit", env: params.env, - shell: false, }); } diff --git a/test/scripts/run-vitest-profile.test.ts b/test/scripts/run-vitest-profile.test.ts index 4aad8a2970c..9eb37822ce0 100644 --- a/test/scripts/run-vitest-profile.test.ts +++ b/test/scripts/run-vitest-profile.test.ts @@ -2,6 +2,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { + buildVitestProfileSpawnSpec, buildVitestProfileCommand, parseArgs, resolveVitestProfileDir, @@ -58,6 +59,26 @@ describe("scripts/run-vitest-profile", () => { ); }); + it("uses the Windows-safe pnpm fallback for runner profiling", () => { + const spawnSpec = buildVitestProfileSpawnSpec( + { + command: "pnpm", + args: ["vitest", "run"], + }, + { + comSpec: "C:\\Windows\\System32\\cmd.exe", + env: {}, + npmExecPath: "", + platform: "win32", + }, + ); + + expect(spawnSpec.options.shell).toBe(false); + expect(spawnSpec.command).toBe("C:\\Windows\\System32\\cmd.exe"); + expect(spawnSpec.options.windowsVerbatimArguments).toBe(true); + expect(spawnSpec.args).toEqual(["/d", "/s", "/c", "pnpm.cmd vitest run"]); + }); + it("parses mode and explicit output dir", () => { expect(parseArgs(["runner", "--output-dir", "/tmp/out"])).toEqual({ mode: "runner",