889e0cdb00
* fix(runtime): make error capture first-wins and crash-safe against a racing unmount Two confirmed bugs in the InternalErrorBoundary's onErrorCaptured: BUG #2 — a component error was silently swallowed when host code threw during an update flush and then synchronously called app.unmount() in the same task. The exit was routed entirely through `void nextTick(() => exitWithError(e))`, so pendingExitError was not recorded until that deferred microtask ran; the racing unmount's resolveExit() read it as undefined and RESOLVED the exit promise clean instead of REJECTING with the error. Fix: record the error SYNCHRONOUSLY via a new recordExitError() bridge (first-wins: only sets pendingExitError if no exit is already decided), while keeping teardown DEFERRED via nextTick. Deferring teardown is load-bearing — teardown()'s final mountedCommit() paints the ErrorOverview frame, and the boundary's errored->true re-render must commit before it; a synchronous exit would drop the overview frame on non-interactive/non-debug mounts. Frame/paint timing is now byte-identical to before in every mode. BUG #5 — two descendants throwing in the same synchronous flush left the displayed overview (caught, last-wins) and the rejected error (pendingExitError, first-wins) disagreeing. Fix: guard the capture body with `if (!errored.value)` so the first thrown error drives both the display and the rejection (e17). Tests: the racing-unmount swallow (interactive/debug AND non-interactive/ non-debug), the two-throw display/reject agreement, and frame-painting guards that pin the overview behavior to main in each mode. Also corrected a stale exit-chain comment in @vue-tui/testing's render(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(runtime): exit() must not clobber an error already recorded by the boundary (first-wins) Final review found an asymmetry: recordExitError() first-wins-guards its write, but appContext.exit() recorded the error unconditionally. So a captured throw (Error1, shown in the overview, recorded via recordExitError) followed by a racing exit(Error2) before the deferred teardown made waitUntilExit() reject Error2 while the overview displayed Error1 — the BUG #5 display/reject disagreement through a different door. Fix: exit() uses `pendingExitError ??= errorOrResult`, so it keeps a synchronously-recorded error. Identical to `=` in every other case (pendingExitError is undefined on a normal first exit()). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>