From 31bb69f9b4d08865147d6447024eeffee8352292 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 31 Aug 2026 04:34:57 +0000 Subject: [PATCH] fix: claude CLI version resolves in the runner parent, never on a test thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eng-review finding: getClaudeCliVersion's fallback is a SYNCHRONOUS spawnSync on the same thread that polls concurrent PTY/session tests — the judgePtyState blocking class this overhaul kills elsewhere. The paid runner parent now resolves it once (cached) and stamps GSTACK_CLAUDE_CLI_VERSION into every shard's env; eval-store short-circuits on the env var, and the fallback spawn's budget tightens 10s -> 3s (bounded one-time stall, records 'unknown' on a slow CLI). Co-Authored-By: Claude Fable 5 --- scripts/test-paid-shards.ts | 8 +++++++- test/helpers/eval-store.ts | 14 +++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts/test-paid-shards.ts b/scripts/test-paid-shards.ts index af9bcdfaf..077e598c9 100644 --- a/scripts/test-paid-shards.ts +++ b/scripts/test-paid-shards.ts @@ -64,7 +64,7 @@ import { } from './test-strict-output'; import { PAID_TEST_GLOBS, isPaidTestFile } from '../test/helpers/paid-test-set'; import { PERIODIC_CI_EXCLUDE } from '../test/helpers/periodic-exclude-data'; -import { getProjectEvalDir } from '../test/helpers/eval-store'; +import { getProjectEvalDir, getClaudeCliVersion } from '../test/helpers/eval-store'; import { preflightAnthropicApi } from '../test/helpers/anthropic-preflight'; import { detectBaseBranch, @@ -493,6 +493,12 @@ export async function runPaidShard( if (options.evalDirBase) { env.GSTACK_EVAL_DIR = path.join(options.evalDirBase, 'shards', shardSlug(files)); } + // Resolve `claude --version` ONCE in the parent (cached across shards) and + // hand it to every child: eval-store's fallback is a synchronous spawn on + // the same thread that polls PTY sessions, so children must never pay it. + if (!env.GSTACK_CLAUDE_CLI_VERSION) { + env.GSTACK_CLAUDE_CLI_VERSION = getClaudeCliVersion(); + } const startedAt = Date.now(); log(`${label} START ${files.join(' ')} (timeout ${Math.round(timeoutMs / 1000)}s)`); diff --git a/test/helpers/eval-store.ts b/test/helpers/eval-store.ts index 65c586ca7..53a3d6201 100644 --- a/test/helpers/eval-store.ts +++ b/test/helpers/eval-store.ts @@ -786,11 +786,23 @@ function getVersion(): string { // claude-CLI TUI drift only after long flake hunts — stamping the version // into every run record makes that correlation a grep instead of an // archaeology dig. +// +// GSTACK_CLAUDE_CLI_VERSION short-circuits the spawn entirely: the paid +// runner's parent resolves the version once and passes it to every shard, +// so test processes never block on it. The fallback spawn is SYNCHRONOUS on +// the same thread that polls PTY sessions — the judgePtyState blocking +// class — so its budget is a tight 3s, not a generous one: a slow/hung CLI +// costs one bounded stall per process and records 'unknown'. let claudeCliVersionCache: string | null = null; export function getClaudeCliVersion(): string { if (claudeCliVersionCache !== null) return claudeCliVersionCache; + const fromEnv = process.env.GSTACK_CLAUDE_CLI_VERSION; + if (fromEnv) { + claudeCliVersionCache = fromEnv; + return claudeCliVersionCache; + } try { - const result = spawnSync('claude', ['--version'], { stdio: 'pipe', timeout: 10_000 }); + const result = spawnSync('claude', ['--version'], { stdio: 'pipe', timeout: 3_000 }); claudeCliVersionCache = result.stdout?.toString().split('\n')[0].trim() || 'unknown'; } catch { claudeCliVersionCache = 'unknown';