diff --git a/scripts/run-tests.cjs b/scripts/run-tests.cjs index 1e065d986..53f4a8964 100644 --- a/scripts/run-tests.cjs +++ b/scripts/run-tests.cjs @@ -126,9 +126,23 @@ function main() { .join(' ')}`, ); + // Default concurrency: 4 on Linux/macOS, 2 on Windows. + // + // Windows has significantly higher per-subprocess overhead than Linux/macOS: + // - Windows Defender scans each spawned process + // - NTFS has higher file-system latency under concurrent access + // - synckit worker_threads (used by the SDK bridge in gsd-tools.cjs) spawn + // native threads that contend on SharedArrayBuffer + Atomics.wait; under + // Node 24 on Windows, 4-way concurrent gsd-tools invocations (each spawning + // a synckit worker) caused intermittent process crashes with empty stderr — + // a signature of OS-level resource exhaustion killing worker threads before + // they could flush. Reducing to 2 halves the peak concurrent worker count. + // + // Operator override via TEST_CONCURRENCY env var for local debugging. + const defaultConcurrency = process.platform === 'win32' ? 2 : 4; const concurrency = process.env.TEST_CONCURRENCY ? `--test-concurrency=${process.env.TEST_CONCURRENCY}` - : '--test-concurrency=4'; + : `--test-concurrency=${defaultConcurrency}`; // Windows `CreateProcess` caps the full command line at 32,767 chars // (lpCommandLine). With 500+ test paths the spawn fails instantly with no diff --git a/tests/helpers.cjs b/tests/helpers.cjs index 109c9838a..9c9869dfc 100644 --- a/tests/helpers.cjs +++ b/tests/helpers.cjs @@ -62,10 +62,18 @@ function runGsdTools(args, cwd = process.cwd(), env = {}) { } return { success: true, output: result.trim(), exitCode: 0 }; } catch (err) { + const stderrRaw = err.stderr?.toString().trim() || ''; + // Prefer actual stderr content; fall back to err.message (which contains + // the command invocation). If stderr is empty, append a note so CI logs + // show "stderr: (empty)" rather than silently losing the fact that the + // child process produced no error output — empty stderr with a non-zero + // exit code is a signal of OS-level crash (OOM kill, worker thread fatal + // error) rather than a gsd-tools application error. + const error = stderrRaw || `${err.message} [stderr: (empty) exit:${err.status ?? 1}]`; return { success: false, output: err.stdout?.toString().trim() || '', - error: err.stderr?.toString().trim() || err.message, + error, exitCode: err.status ?? 1, }; }