mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-15 09:25:28 +02:00
fix(test-runner): cancellation terminates the run; win32 kills the whole tree
Installing SIGINT/SIGTERM forwarders suppresses Node's default terminate-on-signal, so a cancelled run killed the current child and kept LAUNCHING shards — observed as paid runs continuing to burn API spend after Ctrl-C (codex adversarial, repro'd ALIVE_AFTER_SIGTERM). The first signal now also schedules the parent's own exit after the children's SIGKILL grace, and both shard pools consult isTerminationRequested() before taking new work. On win32, killProcessGroup uses taskkill /T /F — detached:true creates no killable group there, and a bare child.kill orphaned every grandchild (ports, locks, and the inherited pipes that kept close from firing). Also: the tree-mutating serial shard prints dirty generated artifacts when it dies mid-regeneration, and --shard CI-matrix mode gets the same size-scaled wall deadline as full-suite mode.
This commit is contained in:
@@ -77,13 +77,14 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { spawn, spawnSync } from 'child_process';
|
||||
import { StringDecoder } from 'node:string_decoder';
|
||||
import { isPaidTestFile } from '../test/helpers/paid-test-set';
|
||||
import {
|
||||
BunTestOutputClassifier,
|
||||
exactTestFileSelectors,
|
||||
installChildSignalForwarding,
|
||||
isTerminationRequested,
|
||||
killProcessGroup,
|
||||
strictTestExitCode,
|
||||
stripAnsiLine,
|
||||
@@ -1081,16 +1082,38 @@ async function main(): Promise<number> {
|
||||
})),
|
||||
);
|
||||
let worst = Math.max(...outcomes.map((o) => exitCodeFor(o.status)));
|
||||
if (mutators.length > 0) {
|
||||
// Cancellation stops the run: don't launch the serial tree-mutating shard
|
||||
// after a SIGINT/SIGTERM already killed the parallel phase.
|
||||
if (mutators.length > 0 && !isTerminationRequested()) {
|
||||
const mutatorOutcome = await runFreeShard(mutators, totalShards, totalShards, {
|
||||
wallTimeoutMs: shardTimeout(mutators.length),
|
||||
verbose: options.verbose,
|
||||
});
|
||||
worst = Math.max(worst, exitCodeFor(mutatorOutcome.status));
|
||||
if (mutatorOutcome.status !== 'passed') {
|
||||
// Mutator safety rests on each test restoring default state itself; a
|
||||
// SIGKILL at the wall deadline (or a mid-regeneration crash) defeats
|
||||
// that by construction. Say so, loudly, before someone commits
|
||||
// regenerated SKILL.md / .agents artifacts by accident.
|
||||
const dirty = spawnSyncGitStatusGenerated();
|
||||
if (dirty.length > 0) {
|
||||
console.error('[test:free] ⚠ tree-mutating shard did not finish cleanly — generated artifacts may be mid-regeneration:');
|
||||
for (const line of dirty.slice(0, 20)) console.error(`[test:free] ${line}`);
|
||||
console.error('[test:free] restore with: bun run gen:skill-docs (or git checkout -- <paths>)');
|
||||
}
|
||||
}
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
|
||||
/** Dirty generated artifacts (SKILL.md / host outputs) after a failed mutator shard. */
|
||||
function spawnSyncGitStatusGenerated(): string[] {
|
||||
const result = spawnSync('git', ['status', '--porcelain'], { cwd: ROOT, encoding: 'utf8' });
|
||||
if (result.status !== 0 || !result.stdout) return [];
|
||||
return result.stdout.split('\n').filter((line) =>
|
||||
/SKILL\.md$/.test(line) || line.includes('.agents/') || line.includes('.factory/'));
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
try {
|
||||
process.exitCode = await main();
|
||||
|
||||
Reference in New Issue
Block a user