fix(scripts): harden Windows QA runners

This commit is contained in:
Vincent Koc
2026-05-24 00:37:29 +02:00
parent acf265d4d5
commit 32f91503be
15 changed files with 158 additions and 82 deletions

View File

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

View File

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

View File

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

View File

@@ -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),
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

@@ -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<void> {
}
async function main(): Promise<void> {
const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
run(pnpmCommand, ["build"]);
run(pnpmCommand, ["ui:build"]);
runPnpm(["build"]);
runPnpm(["ui:build"]);
ensurePreparedArtifacts();
await writeDistInventory();
runBuildSmoke();

View File

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

View File

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

View File

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

View File

@@ -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,
});
}

View File

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