diff --git a/docs/specs/auto-update.md b/docs/specs/auto-update.md index 4143ddb85..c0b59c413 100644 --- a/docs/specs/auto-update.md +++ b/docs/specs/auto-update.md @@ -9,7 +9,7 @@ The standalone app checks for updates on launch, where the network policy allows On launch the updater, in order: -1. **Must read and clear the post-install marker** (§localStorage) and show its notice; a reported failure suppresses this launch's check and reminder. +1. **Must read and clear the post-install marker** (§localStorage) and show its notice; a reported failure suppresses this launch's automatic check and reminders, even after dismissal. 2. Reads the network policy over the Burrow link and, where it allows (`docs/specs/remote-network.md` → "Updates"), calls `check()`: no update is silent, an update raises the approval prompt. 3. Shows the reminder if due, recording `remindedAt`. diff --git a/standalone/src/updater.test.ts b/standalone/src/updater.test.ts index a9e80469d..efd24a39b 100644 --- a/standalone/src/updater.test.ts +++ b/standalone/src/updater.test.ts @@ -254,6 +254,28 @@ describe('updater', () => { expect(mocks.check).not.toHaveBeenCalled(); }); + it.each([ + { failed: true, version: '0.5.0', error: 'EACCES' }, + { from: '0.4.0', to: '0.5.0' }, + ])('keeps a failure-marker launch quiet after dismissal: %j', async (marker) => { + vi.setSystemTime(new Date('2026-10-01T00:00:00Z')); + localStorage.setItem(STORAGE_KEY, JSON.stringify(marker)); + localStorage.setItem(CHECK_KEY, JSON.stringify({ + checkedAt: null, since: Date.now() - 9 * 24 * 60 * 60 * 1000, remindedAt: null, + })); + startUpdateCheck(); + await vi.advanceTimersByTimeAsync(0); + dismissBanner(); + await vi.advanceTimersByTimeAsync(60 * 60 * 1000); + + expect(readBannerState()).toEqual({ status: 'dismissed' }); + expect(mocks.check).not.toHaveBeenCalled(); + expect(JSON.parse(localStorage.getItem(CHECK_KEY)!).remindedAt).toBeNull(); + checkNow(); + await vi.advanceTimersByTimeAsync(0); + expect(mocks.check).toHaveBeenCalledOnce(); + }); + it('reports an unconfirmed install as failure and skips the update check', async () => { localStorage.setItem(STORAGE_KEY, JSON.stringify({ from: '0.4.0', to: '0.5.0' })); diff --git a/standalone/src/updater.ts b/standalone/src/updater.ts index 5754d43be..32e1c6aa8 100644 --- a/standalone/src/updater.ts +++ b/standalone/src/updater.ts @@ -370,9 +370,6 @@ export function startUpdateCheck(): void { console.info('[updater] a self-host build: no update check'); return; } - // The reminder again, hourly, whatever the policy: it contacts nothing, and - // never checks. - reminderTimer ??= setInterval(() => remindIfDue(Date.now()), HOUR_MS); void runUpdateCheck().catch((e) => console.error('[updater] Startup failed:', e)); } @@ -429,6 +426,10 @@ async function runUpdateCheck(): Promise { return; } + // The reminder again, hourly, whatever the policy: it contacts nothing, and + // never checks. + reminderTimer ??= setInterval(() => remindIfDue(Date.now()), HOUR_MS); + await new Promise((resolve) => setTimeout(resolve, 5_000)); // Read at the check, so a change made meanwhile counts