diff --git a/apps/desktop/src/store/updates.test.ts b/apps/desktop/src/store/updates.test.ts index 494d65319..5ecb9c52c 100644 --- a/apps/desktop/src/store/updates.test.ts +++ b/apps/desktop/src/store/updates.test.ts @@ -51,7 +51,10 @@ const { applyUpdates, $updateApply, $updateOverlayOpen, - resetUpdateApplyState + resetUpdateApplyState, + startUpdatePoller, + stopUpdatePoller, + $updateStatus } = await import('./updates') const { setConnection } = await import('./session') @@ -454,3 +457,72 @@ describe('applyBackendUpdate recovery', () => { expect($backendUpdateApply.get().stage).toBe('error') }) }) + +describe('startUpdatePoller', () => { + const checkMock = vi.fn() + const onProgressMock = vi.fn() + const listeners: Record = {} + + beforeEach(() => { + storage.clear() + checkMock.mockReset() + onProgressMock.mockReset() + Object.keys(listeners).forEach(k => delete listeners[k]) + checkMock.mockResolvedValue({ + supported: true, + behind: 5, + targetSha: 'sha-abc', + fetchedAt: 0 + }) + $updateStatus.set(null) + ;(globalThis as unknown as { window: unknown }).window = { + hermesDesktop: { updates: { check: checkMock, onProgress: onProgressMock } }, + addEventListener: vi.fn((event: string, handler: Function) => { + listeners[event] = handler + }), + removeEventListener: vi.fn() + } + vi.useFakeTimers() + stopUpdatePoller() + }) + + afterEach(() => { + stopUpdatePoller() + delete (globalThis as unknown as { window?: unknown }).window + vi.useRealTimers() + }) + + it('calls checkUpdates() on startup so the version pill populates immediately', async () => { + startUpdatePoller() + + // checkUpdates() is async — flush microtasks without advancing the 30-min interval. + await vi.advanceTimersByTimeAsync(0) + + expect(checkMock).toHaveBeenCalled() + expect($updateStatus.get()?.behind).toBe(5) + }) + + it('calls checkUpdates() on each interval tick', async () => { + startUpdatePoller() + await vi.advanceTimersByTimeAsync(0) + checkMock.mockClear() + + await vi.advanceTimersByTimeAsync(30 * 60 * 1000) + + expect(checkMock).toHaveBeenCalled() + }) + + it('calls checkUpdates() when the window regains focus', async () => { + startUpdatePoller() + await vi.advanceTimersByTimeAsync(0) + checkMock.mockClear() + + // Invoke the registered focus handler directly (the mock window doesn't + // propagate DOM events, so call the stored listener). + listeners['focus']?.() + + await vi.advanceTimersByTimeAsync(0) + + expect(checkMock).toHaveBeenCalled() + }) +}) diff --git a/apps/desktop/src/store/updates.ts b/apps/desktop/src/store/updates.ts index eb70afcb3..5efe64771 100644 --- a/apps/desktop/src/store/updates.ts +++ b/apps/desktop/src/store/updates.ts @@ -611,6 +611,7 @@ export function startUpdatePoller(): void { } pollerStarted = true + void checkUpdates() void checkBackendUpdates() void refreshDesktopVersion() bridge.onProgress(ingestProgress) @@ -633,6 +634,7 @@ export function startUpdatePoller(): void { window.addEventListener('focus', onFocus) backgroundTimer = setInterval( () => { + void checkUpdates() void checkBackendUpdates() }, 30 * 60 * 1000 @@ -660,6 +662,7 @@ function onFocus() { } lastFocusAt = now + void checkUpdates() void checkBackendUpdates() void refreshDesktopVersion() }