|
5 | 5 | * `--version` and `--help` exit via commander synchronously, before async |
6 | 6 | * startup failures (e.g. the unhandled rejection from Parser.init when the |
7 | 7 | * tree-sitter wasm load fails) get a chance to fire. This script spawns the |
8 | | - * binary, lets it run for a few seconds, then kills it and asserts no fatal |
9 | | - * startup markers showed up in stdout/stderr. |
| 8 | + * binary, lets it run for a few seconds, then kills it and asserts the TUI |
| 9 | + * actually rendered a known boot screen. |
| 10 | + * |
| 11 | + * The positive check matters more than the negative one: a "did the boot |
| 12 | + * screen appear" assertion catches *any* startup failure — known fatals, |
| 13 | + * novel error messages, silent crashes, hangs, segfaults that produce no |
| 14 | + * output. Negative pattern matches are kept only for clearer diagnostics |
| 15 | + * when a known regression recurs. |
10 | 16 | * |
11 | 17 | * Designed to run on every supported platform (Linux, macOS, Windows) without |
12 | | - * extra deps. The binary doesn't need a TTY: `earlyFatalHandler` in |
13 | | - * `cli/src/index.tsx` writes its diagnostic to stdout/stderr regardless. |
| 18 | + * extra deps. The binary doesn't need a TTY: OpenTUI emits ANSI escapes to |
| 19 | + * stdout regardless, and the static text we look for renders contiguously. |
14 | 20 | * |
15 | 21 | * Usage: |
16 | 22 | * bun cli/scripts/smoke-binary.ts <path-to-binary> [seconds] |
17 | 23 | * |
18 | | - * Exits 0 if no fatal markers detected, 1 otherwise. |
| 24 | + * Exits 0 if a boot signal is detected and no fatal markers are present, 1 |
| 25 | + * otherwise. |
19 | 26 | */ |
20 | 27 |
|
21 | 28 | import { spawn } from 'child_process' |
22 | 29 | import { existsSync } from 'fs' |
23 | 30 |
|
24 | | -// Markers that indicate the CLI crashed during startup. Match what |
25 | | -// `earlyFatalHandler` writes plus the specific tree-sitter regression. |
| 31 | +// Any one of these strings appearing in stdout/stderr proves the binary |
| 32 | +// reached its post-init UI: React tree mounted, OpenTUI rendered, async |
| 33 | +// wasm init survived. Strings are static text from rendered components |
| 34 | +// (not shimmer / animated) so they survive ANSI styling as contiguous |
| 35 | +// substrings. Cover the multiple boot states the binary might land on: |
| 36 | +// |
| 37 | +// - "will run commands on your behalf" — codebuff/freebuff main surface |
| 38 | +// header (authed + session ready) |
| 39 | +// - "Press ENTER to login" / "Open this URL" — login modal (no cached |
| 40 | +// creds — typical CI smoke) |
| 41 | +// - "Pick a model to start" / waiting-room copy — freebuff queue gate |
| 42 | +// - "Free mode isn't available" — freebuff country-block screen (CI |
| 43 | +// runners with anonymized-network egress like GitHub Actions land here) |
| 44 | +// - "Enter a coding task" — chat input prompt |
| 45 | +const BOOT_SIGNAL_PATTERNS = [ |
| 46 | + /will run commands on your behalf/, |
| 47 | + /Pick a model to start/, |
| 48 | + /You're in the waiting room/, |
| 49 | + /You're next in line/, |
| 50 | + /Free mode isn't available/, |
| 51 | + /Press ENTER to login/, |
| 52 | + /Open this URL/, |
| 53 | + /Enter a coding task/, |
| 54 | +] as const |
| 55 | + |
| 56 | +// Fatal markers we already know about — kept for nicer error messages on |
| 57 | +// regressions of bugs we've already seen. The boot-signal check above is |
| 58 | +// the real gate: it fails on *any* startup problem, including ones whose |
| 59 | +// error text we never thought to add here. |
26 | 60 | const FATAL_PATTERNS = [ |
27 | 61 | /Fatal error during startup/i, |
28 | 62 | /Internal error: tree-sitter\.wasm not found/i, |
@@ -80,19 +114,35 @@ async function main(): Promise<void> { |
80 | 114 | await exited |
81 | 115 | clearTimeout(killTimer) |
82 | 116 |
|
| 117 | + const fail = (reason: string): never => { |
| 118 | + console.error(`smoke-binary: FAIL — ${reason} (exit code ${earlyExitCode}).`) |
| 119 | + console.error('--- captured output (truncated to 8KB) ---') |
| 120 | + console.error(captured.slice(0, 8 * 1024)) |
| 121 | + process.exit(1) |
| 122 | + } |
| 123 | + |
| 124 | + // Negative gate first: a known fatal marker gives us a more specific error |
| 125 | + // message than "no boot signal found" would. Both gates would fire on a |
| 126 | + // crash; preferring the negative one just makes the failure log clearer. |
83 | 127 | for (const pattern of FATAL_PATTERNS) { |
84 | 128 | if (pattern.test(captured)) { |
85 | | - console.error( |
86 | | - `smoke-binary: FAIL — output matched ${pattern} (exit code ${earlyExitCode}).`, |
87 | | - ) |
88 | | - console.error('--- captured output (truncated to 8KB) ---') |
89 | | - console.error(captured.slice(0, 8 * 1024)) |
90 | | - process.exit(1) |
| 129 | + fail(`output matched ${pattern}`) |
91 | 130 | } |
92 | 131 | } |
93 | 132 |
|
| 133 | + // Positive gate: the binary must have rendered a known boot screen. This |
| 134 | + // is the load-bearing assertion — it catches *any* startup failure (silent |
| 135 | + // crashes, hangs, novel error messages, segfaults), not just the listed |
| 136 | + // fatals. |
| 137 | + const matchedSignal = BOOT_SIGNAL_PATTERNS.find((p) => p.test(captured)) |
| 138 | + if (!matchedSignal) { |
| 139 | + fail( |
| 140 | + `binary never reached a known boot screen — checked ${BOOT_SIGNAL_PATTERNS.length} patterns`, |
| 141 | + ) |
| 142 | + } |
| 143 | + |
94 | 144 | console.log( |
95 | | - `smoke-binary: OK (exit code ${earlyExitCode}, ${captured.length} bytes captured).`, |
| 145 | + `smoke-binary: OK (matched ${matchedSignal}, exit code ${earlyExitCode}, ${captured.length} bytes captured).`, |
96 | 146 | ) |
97 | 147 | } |
98 | 148 |
|
|
0 commit comments