fix(desktop): boot-timer race — handle rendererReady ping arriving before arm
CRITICAL BUG. In real SPAs, useEffect fires during document load (microtask after bundle execute + React render), which is BEFORE did-finish-load fires (after window.onload). Without this fix, the ping arrived when bootArmed=false (no-op), then did-finish-load armed a timer nothing would clear → 20s later every successful packaged build falsely entered recovery. Caught by smoke scenario 13 (positive control: page that DOES ping should NOT recover). The smoke proved the page's script ran AND the ping was sent, yet recovery still fired. Fix: module-level pingReceivedThisNav flag, reset on did-navigate, set in handleRendererReady, checked in armBootTimer (early-return if true). Late-ping case (ping after arm) preserved via existing 'if (bootArmed) clearBootTimer()'. Also exports resetBootTimerStateForTest() to ensure full module-state isolation between tests (pingReceivedThisNav is module-level and must not bleed across test cases in the same run). 3 new tests pin the early-ping, late-ping, and per-nav persistence semantics. 48/48 tests pass. Build clean. Spec + docs updated.
This commit is contained in:
@@ -9,6 +9,8 @@ import {
|
||||
armBootTimer,
|
||||
clearBootTimer,
|
||||
isBootArmed,
|
||||
handleRendererReady,
|
||||
resetBootTimerStateForTest,
|
||||
} from './recovery';
|
||||
|
||||
// Mock electron with isPackaged=true so the real arm path executes in all tests below.
|
||||
@@ -272,7 +274,9 @@ function fakeWin(url: string): FakeWindow {
|
||||
|
||||
describe('armBootTimer / clearBootTimer', () => {
|
||||
beforeEach(() => {
|
||||
clearBootTimer();
|
||||
// Reset all module-level boot-timer state including pingReceivedThisNav so
|
||||
// tests that call handleRendererReady() don't pollute subsequent tests.
|
||||
resetBootTimerStateForTest();
|
||||
});
|
||||
|
||||
it('arms when URL is http://', () => {
|
||||
@@ -303,4 +307,38 @@ describe('armBootTimer / clearBootTimer', () => {
|
||||
clearBootTimer();
|
||||
expect(isBootArmed()).toBe(false);
|
||||
});
|
||||
|
||||
it('does NOT arm if rendererReady arrived before armBootTimer (early-ping case)', () => {
|
||||
// Simulate SPA timing: useEffect fires (microtask) before did-finish-load.
|
||||
// The ping arrives when bootArmed=false, then armBootTimer is called by
|
||||
// did-finish-load. Without the pingReceivedThisNav flag this would arm a
|
||||
// 20s timer that nothing clears → false renderer-stalled recovery.
|
||||
handleRendererReady();
|
||||
armBootTimer(fakeWin('http://localhost:3005/') as never);
|
||||
expect(isBootArmed()).toBe(false);
|
||||
});
|
||||
|
||||
it('clears existing timer if rendererReady arrives after armBootTimer (late-ping case)', () => {
|
||||
// Simulate the less-common ordering: did-finish-load fires first (arms timer),
|
||||
// then the ping arrives. Preserved existing behavior — ping clears the timer.
|
||||
armBootTimer(fakeWin('http://localhost:3005/') as never);
|
||||
expect(isBootArmed()).toBe(true);
|
||||
handleRendererReady();
|
||||
expect(isBootArmed()).toBe(false);
|
||||
});
|
||||
|
||||
it('flag is per-navigation: after clearBootTimer reset, a subsequent arm should still be blocked by the set flag', () => {
|
||||
// Verify that pingReceivedThisNav persists across clearBootTimer calls until
|
||||
// a real did-navigate resets it. The module-level flag is only reset by
|
||||
// did-navigate (wired in attachRecoveryHandlers). Here we confirm the flag
|
||||
// behaviour in isolation: ping → arm (blocked) → clear → arm again (still blocked).
|
||||
// The per-nav reset is integration-tested by smoke scenario 4 + 13 together.
|
||||
handleRendererReady();
|
||||
armBootTimer(fakeWin('http://localhost:3005/') as never);
|
||||
expect(isBootArmed()).toBe(false);
|
||||
// clearBootTimer resets bootArmed but NOT pingReceivedThisNav
|
||||
clearBootTimer();
|
||||
armBootTimer(fakeWin('http://localhost:3005/') as never);
|
||||
expect(isBootArmed()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user