test(dictation): pin the churn clock and the window's lower bound

The two 250-refresh churn tests measured real elapsed time against a 5s
window, so a loaded runner that took longer than one window to run the
loop would roll the window and go red. Pin Date.now in both.

Pinning alone leaves CHURN_WINDOW_MS unpinned below: shrinking it 5_000 ->
50 kept all 13 tests green, yet a 50ms window can never accumulate 250
refreshes and the detector would be dead. Add a storm spread across most
of one window so the constant has to be wide enough to hold a sustained
storm, not just an instantaneous burst.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-02 01:04:34 -07:00
co-authored by Orca
parent 7f4bb238fc
commit 069f71bd58
2 changed files with 22 additions and 0 deletions
@@ -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<DictationTestStore>(dictationSlice)
for (let refresh = 0; refresh < 250; refresh += 1) {
@@ -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.