mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-04 13:51:50 +00:00
- Provider filter: Models-this-period sources current.topModels (history days carry empty topModels per provider); StackedBars falls back to a cost-proportional single segment like the Swift menubar - Chart windows align to the CLI (week -7, 30days -30, all = 6 months) and zero-fill a contiguous calendar window; history.daily is sparse and both sides key local YYYY-MM-DD, so lookups are safe (verified on real CLI output) - Custom range threads into Overview panels (chart, models table) and suppresses MTD/projected and vs-last-week comparisons; Compare states that custom dates fall back to the period - usePolled clears errors per attempt and sections show a StaleBanner instead of silently rendering last-good data after a failed refresh - Cache-hit denominator matches the CLI; hero savings relabeled 'Saved by applied fixes' + new 'Saved via local models' line; formatCompact everywhere; Optimize 'Fixes' count matches the rendered list App suite 163/163 (was 147), root 1613/1613.
58 lines
2.6 KiB
TypeScript
58 lines
2.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { renderHook, act } from '@testing-library/react'
|
|
|
|
import { usePolled } from './usePolled'
|
|
|
|
describe('usePolled', () => {
|
|
it('discards a stale in-flight fetch that resolves after a newer one (epoch guard)', async () => {
|
|
// A fetcher we resolve by hand, one deferred per call, so we can force a
|
|
// SLOW deps-A fetch to resolve AFTER a FAST deps-B fetch.
|
|
const resolvers: Array<(v: string) => void> = []
|
|
const fetcher = vi.fn(() => new Promise<string>(resolve => { resolvers.push(resolve) }))
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ p }: { p: string }) => usePolled(fetcher, [p]),
|
|
{ initialProps: { p: 'A' } },
|
|
)
|
|
|
|
// #0 mount fetch (deps A) — resolve it to establish a known baseline.
|
|
await act(async () => { resolvers[0]!('A0') })
|
|
expect(fetcher).toHaveBeenCalledTimes(1)
|
|
expect(result.current.data).toBe('A0')
|
|
|
|
// #1 refresh() while deps are still A → an in-flight SLOW fetch whose cancel
|
|
// handle the hook discards. Leave it unresolved for now.
|
|
act(() => { result.current.refresh() })
|
|
expect(fetcher).toHaveBeenCalledTimes(2)
|
|
|
|
// #2 deps change A→B → a FAST fetch that resolves first with fresh data.
|
|
rerender({ p: 'B' })
|
|
expect(fetcher).toHaveBeenCalledTimes(3)
|
|
await act(async () => { resolvers[2]!('B-fresh') })
|
|
expect(result.current.data).toBe('B-fresh')
|
|
|
|
// #1 (the slow deps-A fetch) now resolves LATE. It must NOT clobber B.
|
|
await act(async () => { resolvers[1]!('A-stale') })
|
|
expect(result.current.data).toBe('B-fresh')
|
|
})
|
|
|
|
it('keeps last-good data and exposes the error when a background reload fails', async () => {
|
|
const calls: Array<{ resolve: (v: string) => void; reject: (e: unknown) => void }> = []
|
|
const fetcher = vi.fn(() => new Promise<string>((resolve, reject) => { calls.push({ resolve, reject }) }))
|
|
const { result } = renderHook(() => usePolled(fetcher, []))
|
|
|
|
// Establish last-good data.
|
|
await act(async () => { calls[0]!.resolve('good') })
|
|
expect(result.current.data).toBe('good')
|
|
expect(result.current.error).toBeNull()
|
|
|
|
// A reload clears the error up front; if it fails, data is retained and the
|
|
// error is surfaced alongside it (the StaleBanner condition).
|
|
act(() => { result.current.refresh() })
|
|
expect(result.current.error).toBeNull()
|
|
await act(async () => { calls[1]!.reject({ kind: 'nonzero', message: 'boom' }) })
|
|
expect(result.current.data).toBe('good')
|
|
expect(result.current.error).toMatchObject({ kind: 'nonzero', message: 'boom' })
|
|
})
|
|
})
|