diff --git a/src/renderer/src/components/settings/voice-model-download-progress-storm.test.tsx b/src/renderer/src/components/settings/voice-model-download-progress-storm.test.tsx index 4744c57988d..a482ca54e2b 100644 --- a/src/renderer/src/components/settings/voice-model-download-progress-storm.test.tsx +++ b/src/renderer/src/components/settings/voice-model-download-progress-storm.test.tsx @@ -112,6 +112,7 @@ afterEach(() => { act(() => root?.unmount()) document.body.replaceChildren() vi.unstubAllGlobals() + vi.restoreAllMocks() }) /** @@ -191,6 +192,9 @@ describe('speech model download progress storm (render pressure)', () => { }) it('records the churn breadcrumb when refreshes outrun coalescing', async () => { + // Why pin Date.now: the threshold is per 5s window, so a loaded machine that + // takes longer than one window to run the loop would measure itself, not the rule. + vi.spyOn(Date, 'now').mockReturnValue(1_000_000) // Own store instance: the churn window is per-slice state the other tests advance. const churnStore = create(dictationSlice) for (let refresh = 0; refresh < 250; refresh += 1) { diff --git a/src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts b/src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts index 3f0bd908e1e..c0a8d07bd52 100644 --- a/src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts +++ b/src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts @@ -124,6 +124,7 @@ describe('dictation model-state stabilisation', () => { }) it('reports churn once the renderer is driven far past whole-percent pacing', async () => { + pinClock() const store = newStore() reply = [{ id: 'whisper-tiny', status: 'downloading', progress: 0.42 }] for (let refresh = 0; refresh < 250; refresh += 1) { @@ -135,6 +136,23 @@ describe('dictation model-state stabilisation', () => { expect(churn[0].data).toMatchObject({ refreshes: 250, noOpRefreshes: 249 }) }) + // Why spread across the window: a burst-only test passes for any window length, + // including one too short for a real storm to ever accumulate the threshold. The + // window has to be wide enough to hold a sustained storm, not just an instant one. + it('still reports a storm that is spread across most of one window', async () => { + const clock = pinClock() + const store = newStore() + reply = [{ id: 'whisper-tiny', status: 'downloading', progress: 0.42 }] + for (let refresh = 0; refresh < 250; refresh += 1) { + await store.getState().refreshModelStates() + clock.advance(19) + } + + const churn = churnBreadcrumbs() + expect(churn).toHaveLength(1) + expect(churn[0].data).toMatchObject({ refreshes: 250, windowMs: 249 * 19 }) + }) + // Stopping at exactly the threshold cannot tell "fires once" from "fires on every // refresh after", and the second floods the 30-entry ring this breadcrumb is // coalesced into to protect.