test: free runner output contract — name the failure, quiet the noise

Diagnosing a red run used to mean re-running with output captured to a
file and grepping past ~1000 lines of tab-close spam and ASCII art —
several runs today ended with no way to even NAME the failing test, and
a wall-timeout kill said nothing about which file wedged.

New contract: the full child stream ALWAYS lands in a per-run log file
(path printed up front); the console shows only runner lines, (fail)
results, crash markers, and the terminal summary (--verbose restores
the firehose; the strict classifier consumes the full stream in every
mode). After every run a stable epilogue names the outcome:

  [test:free] FAIL — k failing test(s) in j file(s), c crashed
  worker(s). Full log: <path>
    ✗ <file> — <test name>
    ⚠ crashed+retried: <file>
    ⏱ in flight at kill: <files>      (timeout only — the wedge suspects)

Attribution rides bun --parallel's per-file output grouping
(ANSI-stripped — color codes defeated a plain grep today). 12 new pins:
epilogue formats, crash surfacing, quiet/verbose console policy, log
completeness, in-flight-at-kill on a real hang.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-15 11:04:37 -07:00
co-authored by Claude Fable 5
parent 965b0d5a7c
commit 0ac1386481
3 changed files with 544 additions and 8 deletions
+12 -2
View File
@@ -132,15 +132,25 @@ export function killProcessGroup(child: ChildProcess, signal: NodeJS.Signals): v
}
}
/**
* Strip ANSI escapes and a trailing CR from one output line. Every line
* matcher (here and in the free runner's console filter / failure
* attribution) MUST match against this form — a prior grep for `(fail)`
* lines missed real failures because color codes sat inside the line.
*/
export function stripAnsiLine(rawLine: string): string {
return rawLine.replace(ANSI_ESCAPE, '').replace(/\r$/, '');
}
export function classifyBunTestOutputLine(rawLine: string): BunTestOutputFinding | null {
const line = rawLine.replace(ANSI_ESCAPE, '').replace(/\r$/, '');
const line = stripAnsiLine(rawLine);
if (BUN_FAIL_RESULT.test(line)) return 'failed-test';
if (line === BUN_BETWEEN_TESTS_ERROR) return 'unhandled-between-tests';
return null;
}
export function parseBunTerminalSummaryLine(rawLine: string): number | null {
const line = rawLine.replace(ANSI_ESCAPE, '').replace(/\r$/, '');
const line = stripAnsiLine(rawLine);
const match = BUN_TERMINAL_SUMMARY.exec(line);
return match ? Number.parseInt(match[1], 10) : null;
}