mirror of https://github.com/garrytan/gstack.git
fix(evals): preflight fails fast on spawn error, timeout, and exit 127
The ping only grepped stdout for two connection strings — a missing claude binary, a 30s timeout kill, or command-not-found all returned 'ok', and the fleet then burned ~30 shard timeouts discovering the outage one child at a time. Cross-model finding (testing specialist + codex adversarial). Other non-zero exits stay deliberately fail-open: a flaky preflight must not block a runnable suite; pinned both ways.
This commit is contained in:
parent
9e72b4ac7f
commit
1e35386c54
|
|
@ -50,4 +50,33 @@ describe('preflightAnthropicApi', () => {
|
|||
expect(result).toBe('ok');
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
// Failure modes that guarantee every shard fails too must fail the
|
||||
// preflight — previously a missing binary, a timeout kill, and exit 127
|
||||
// all returned 'ok' and the fleet burned its own discovery of the outage.
|
||||
const shapedSpawn = (shape: Partial<ReturnType<typeof import('child_process').spawnSync>>) =>
|
||||
((() => ({ stdout: Buffer.from(''), stderr: Buffer.from(''), status: 0, ...shape })) as unknown as
|
||||
typeof import('child_process').spawnSync);
|
||||
|
||||
test('spawn error (unlaunchable shell/binary) throws', () => {
|
||||
expect(() =>
|
||||
preflightAnthropicApi({}, shapedSpawn({ error: new Error('spawn sh ENOENT') })),
|
||||
).toThrow(/could not run/);
|
||||
});
|
||||
|
||||
test('timeout kill (signal set) throws', () => {
|
||||
expect(() =>
|
||||
preflightAnthropicApi({}, shapedSpawn({ signal: 'SIGTERM' })),
|
||||
).toThrow(/timed out/);
|
||||
});
|
||||
|
||||
test('exit 127 (claude not found) throws', () => {
|
||||
expect(() =>
|
||||
preflightAnthropicApi({}, shapedSpawn({ status: 127 })),
|
||||
).toThrow(/not found on PATH/);
|
||||
});
|
||||
|
||||
test('other non-zero exits stay fail-open (a flaky preflight must not block a runnable suite)', () => {
|
||||
expect(preflightAnthropicApi({}, shapedSpawn({ status: 1, stdout: Buffer.from('transient') }))).toBe('ok');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,20 @@ export function preflightAnthropicApi(
|
|||
['-c', 'echo "ping" | claude -p --max-turns 1 --output-format stream-json --verbose --dangerously-skip-permissions'],
|
||||
{ stdio: 'pipe', timeout: 30_000 },
|
||||
);
|
||||
// Fail fast on the failure modes that guarantee EVERY shard fails too:
|
||||
// spawn error (sh/claude unlaunchable), a timeout kill (signal set), or
|
||||
// exit 127 (command not found). Anything else stays fail-open — a flaky
|
||||
// preflight must not block a run the shards could complete (auth prompts
|
||||
// and transient non-zero exits are the shards' problem to report).
|
||||
if (check.error) {
|
||||
throw new Error(`Anthropic preflight could not run (${check.error.message}) — aborting E2E suite before spawning shards.`);
|
||||
}
|
||||
if (check.signal) {
|
||||
throw new Error(`Anthropic preflight timed out (killed with ${check.signal}) — aborting E2E suite. Check connectivity/auth and retry.`);
|
||||
}
|
||||
if (check.status === 127) {
|
||||
throw new Error('Anthropic preflight: `claude` not found on PATH (exit 127) — aborting E2E suite before spawning shards.');
|
||||
}
|
||||
const output = check.stdout?.toString() || '';
|
||||
if (output.includes('ConnectionRefused') || output.includes('Unable to connect')) {
|
||||
throw new Error('Anthropic API unreachable — aborting E2E suite. Fix connectivity and retry.');
|
||||
|
|
|
|||
Loading…
Reference in New Issue