Files
VoiceStudio/tests/frontend/bootstrap-splash-hook-order.test.mjs
T
debpalashandClaude Opus 4.8 9b9df1b5eb fix(windows): production black screen (splash TDZ) + first-run console-window storm
Two Windows production-only regressions found by installing and running the
released v0.3.22 MSI on a clean machine (RTX 4070 / CUDA).

1. Black screen on every launch. The first-run splash crashes React during
   render with "Cannot access 'logs' before initialization" — an empty #root,
   i.e. a black window. Root cause: in BootstrapSplash the derived const
   `isUnrecoverable` (which reads the `logs` useState result) sat *between* two
   `useState` calls. esbuild's release minifier merges the declarations into one
   comma-list and can hoist the read ahead of its `useState` binding, producing
   a temporal-dead-zone access. Dev, unminified, and the e2e suite (which runs
   against the un-minified dev server) never trip it, so it shipped. #1159 fixed
   the source-visible ordering but left the derived value wedged among the
   hooks, so the minifier landmine remained. Fix: declare every hook before any
   derived const that reads a hook result — the minifier then has nothing to
   reorder. Verified deterministically: the shipped bundle contains exactly one
   use-before-init-useState hazard at this site; a build of this change contains
   zero, and the app renders. Guarded by tests/frontend/bootstrap-splash-hook-order.

2. Storm of console windows during first-run install. Bootstrap spawns dozens
   of helper processes (uv venv/sync, managed-python, capability probes); on
   Windows each flashed its own cmd-style console window because run_streaming
   and the direct probes never set CREATE_NO_WINDOW. Added one Windows-only
   `no_window` chokepoint in tools.rs and routed every bootstrap/tools/backend
   subprocess through it (mirrors the flag the backend spawn and nvidia-smi probe
   already set). stdout/stderr are still piped to the splash log. No-op on
   macOS/Linux — default behaviour there is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 11:29:58 -07:00

46 lines
2.5 KiB
JavaScript

// Regression guard for the v0.3.22 Windows black screen (#<pr>).
//
// The bootstrap splash rendered a blank/black window on the shipped production
// build because a derived `const` that *reads* a `useState` result was declared
// BETWEEN hook calls:
//
// const isFailed = stage === 'failed';
// const [logs, setLogs] = useState([]);
// const isUnrecoverable = isFailed && isUnrecoverableFailure(message, logs);
// const [logsOpen, setLogsOpen] = useState(true); // <-- more hooks after
//
// The production minifier (esbuild) merges consecutive declarations into one
// comma-list and can hoist `isUnrecoverable` (which reads `logs`) AHEAD of the
// `[logs] = useState([])` binding, emitting `...isUnrecoverable=..logs.., [logs]=
// useState()..` — a temporal-dead-zone access that throws during render. React
// unmounts to an empty #root and the whole app is a black screen. Dev and
// unminified builds short-circuit on `isFailed` so they never trip it; only the
// minified release bundle did (which is why the e2e suite, run against the dev
// server, missed it).
//
// The fix is to declare every hook BEFORE any derived const that reads a hook
// result. This test locks that ordering in so the regression can't silently
// return: it asserts the `logs` useState precedes the `isUnrecoverable` read,
// and that no value-binding hook (useState/useRef/useReducer) appears after that
// read within the splash component body.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const src = readFileSync(
fileURLToPath(new URL('../../frontend/src/components/BootstrapSplash.jsx', import.meta.url)),
'utf8',
);
test('BootstrapSplash: the `logs` useState is declared before the derived const that reads it', () => {
const logsIdx = src.indexOf('const [logs, setLogs] = useState');
const derivedIdx = src.indexOf('const isUnrecoverable');
assert.ok(logsIdx > 0, "anchor 'const [logs, setLogs] = useState' not found — did BootstrapSplash get refactored? Update this guard.");
assert.ok(derivedIdx > 0, "anchor 'const isUnrecoverable' not found — did BootstrapSplash get refactored? Update this guard.");
assert.ok(
logsIdx < derivedIdx,
"`logs` useState must be declared BEFORE `isUnrecoverable` reads it. With it after, the production minifier hoists the read ahead of the binding → 'Cannot access logs before initialization' → black screen (the v0.3.22 Windows regression). Keep all useState/useRef above the derived consts.",
);
});