Skip to content

Commit 6f819ce

Browse files
jahoomaclaude
andcommitted
Assert boot screen renders, not just absence of fatals
The previous smoke test and e2e test both checked for known error markers ("Fatal error during startup", etc.). That misses anything we didn't think to add — novel error messages, silent crashes, hangs, segfaults that produce no stderr. Switch both to a positive signal: assert the binary actually rendered a known boot screen. If something goes wrong we don't anticipate, the boot text never appears and the test fails with a clear "binary never reached a known boot screen" diagnostic. Negative pattern matches stay for clearer error messages on regressions of bugs we've already seen. - cli/scripts/smoke-binary.ts: gate pass/fail on at least one of N boot signals appearing in stdout/stderr (chat surface header, login modal, freebuff queue states, freebuff country-block screen, chat input prompt). Verified locally: passes on real binaries, fails on a stub that hangs without rendering. - freebuff/e2e/tests/startup.e2e.test.ts: wait for the FREEBUFF ASCII logo's F+R crossbar pattern (`█████╗ ██████╔╝`). The logo renders for every valid boot state — including the country-block screen that GitHub Actions runners hit because their egress is flagged as anonymized network — so this assertion survives the geo gate that was tripping the previous "Pick a model to start" wait. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 065eefa commit 6f819ce

2 files changed

Lines changed: 78 additions & 26 deletions

File tree

cli/scripts/smoke-binary.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,58 @@
55
* `--version` and `--help` exit via commander synchronously, before async
66
* startup failures (e.g. the unhandled rejection from Parser.init when the
77
* 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.
1016
*
1117
* 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.
1420
*
1521
* Usage:
1622
* bun cli/scripts/smoke-binary.ts <path-to-binary> [seconds]
1723
*
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.
1926
*/
2027

2128
import { spawn } from 'child_process'
2229
import { existsSync } from 'fs'
2330

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.
2660
const FATAL_PATTERNS = [
2761
/Fatal error during startup/i,
2862
/Internal error: tree-sitter\.wasm not found/i,
@@ -80,19 +114,35 @@ async function main(): Promise<void> {
80114
await exited
81115
clearTimeout(killTimer)
82116

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.
83127
for (const pattern of FATAL_PATTERNS) {
84128
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}`)
91130
}
92131
}
93132

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+
94144
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).`,
96146
)
97147
}
98148

freebuff/e2e/tests/startup.e2e.test.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,24 @@ describe('Freebuff: Startup', () => {
1515
})
1616

1717
test(
18-
'binary reaches the model selection screen',
18+
'binary renders its boot screen',
1919
async () => {
2020
const binary = requireFreebuffBinary()
2121
session = await FreebuffSession.start(binary)
2222

23-
// Wait for the model selector to render. This proves the binary survived
24-
// module init (including the eager tree-sitter Parser.init that crashed
25-
// Windows binaries after the OpenTUI 0.2.2 upgrade), passed the auth /
26-
// session API call, and successfully mounted the React tree. A pure
27-
// "non-empty output" check would pass on a half-rendered crash screen.
28-
const output = await session.waitForText('Pick a model to start')
29-
30-
// earlyFatalHandler in cli/src/index.tsx writes this to stderr on
31-
// unhandled rejections during startup. Belt-and-braces: the wait above
32-
// would already have timed out, but if some race ever surfaces a fatal
33-
// *after* the model selector renders, we still want it to fail.
23+
// The 3rd row of the FREEBUFF ASCII logo: the crossbars of F and R
24+
// adjacent. Picked because the logo renders for *every* valid boot
25+
// state — model picker, waiting room, country-blocked (which is what
26+
// CI runners hit, since GitHub Actions egress is flagged as anonymized
27+
// network) — but never appears if module init crashes before React
28+
// mounts (the post-OpenTUI-upgrade tree-sitter wasm regression). This
29+
// gives us a positive "boot succeeded" signal that's robust against
30+
// novel error modes, not just the ones we listed below.
31+
const output = await session.waitForText('█████╗ ██████╔╝')
32+
33+
// Belt-and-braces: known fatal markers should never coexist with a
34+
// rendered logo, but if some race ever surfaces one we still want to
35+
// see it called out clearly rather than buried in raw output.
3436
expect(output).not.toContain('Fatal error during startup')
3537
expect(output).not.toContain('Internal error: tree-sitter.wasm not found')
3638
expect(output).not.toContain('FATAL')

0 commit comments

Comments
 (0)