test(desktop): cover the context gauge fetching before a turn runs
This commit is contained in:
committed by
brooklyn!
parent
25d1c8d740
commit
c83061ba3d
@@ -1,12 +1,13 @@
|
||||
import { act, cleanup, render, waitFor } from '@testing-library/react'
|
||||
import { useState } from 'react'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ContextBreakdown, UsageStats } from '@/types/hermes'
|
||||
|
||||
import { ContextUsagePanel } from './context-usage-panel'
|
||||
import { useContextBreakdown } from './hooks/use-context-breakdown'
|
||||
|
||||
const initialUsage: UsageStats = {
|
||||
const usage: UsageStats = {
|
||||
calls: 1,
|
||||
context_max: 272_000,
|
||||
context_percent: 47,
|
||||
@@ -30,64 +31,86 @@ afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('ContextUsagePanel', () => {
|
||||
it('publishes once without refetching when publication recreates the callback', async () => {
|
||||
describe('useContextBreakdown', () => {
|
||||
it('fetches for a session that has not run a turn yet', async () => {
|
||||
const requestGateway = vi.fn().mockResolvedValue(breakdown)
|
||||
const published = vi.fn()
|
||||
const renderedUsage: UsageStats[] = []
|
||||
|
||||
function Harness() {
|
||||
const [currentUsage, setCurrentUsage] = useState(initialUsage)
|
||||
renderedUsage.push(currentUsage)
|
||||
const { result } = renderHook(() =>
|
||||
useContextBreakdown({ busy: false, enabled: true, requestGateway, sessionId: 'runtime-1' })
|
||||
)
|
||||
|
||||
return (
|
||||
<ContextUsagePanel
|
||||
currentUsage={currentUsage}
|
||||
onUsageSnapshot={snapshot => {
|
||||
published(snapshot)
|
||||
setCurrentUsage(current => ({ ...current, ...snapshot }))
|
||||
}}
|
||||
requestGateway={requestGateway}
|
||||
sessionId="runtime-1"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Harness />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(published).toHaveBeenCalledWith({
|
||||
context_max: 272_000,
|
||||
context_percent: 89,
|
||||
context_used: 241_400
|
||||
})
|
||||
expect(renderedUsage.at(-1)?.context_used).toBe(241_400)
|
||||
})
|
||||
await act(async () => {})
|
||||
|
||||
expect(requestGateway).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() => expect(result.current.breakdown).toEqual(breakdown))
|
||||
expect(requestGateway).toHaveBeenCalledWith('session.context_breakdown', { session_id: 'runtime-1' })
|
||||
})
|
||||
|
||||
it('refetches when the session or gateway requester changes', async () => {
|
||||
const firstGateway = vi.fn().mockResolvedValue(breakdown)
|
||||
const secondGateway = vi.fn().mockResolvedValue(breakdown)
|
||||
it('does not fetch while the gauge is hidden, and fetches once it is shown', async () => {
|
||||
const requestGateway = vi.fn().mockResolvedValue(breakdown)
|
||||
|
||||
const { rerender } = render(
|
||||
<ContextUsagePanel currentUsage={initialUsage} requestGateway={firstGateway} sessionId="runtime-1" />
|
||||
const { rerender } = renderHook(
|
||||
({ enabled }) => useContextBreakdown({ busy: false, enabled, requestGateway, sessionId: 'runtime-1' }),
|
||||
{ initialProps: { enabled: false } }
|
||||
)
|
||||
|
||||
await waitFor(() => expect(firstGateway).toHaveBeenCalledTimes(1))
|
||||
expect(requestGateway).not.toHaveBeenCalled()
|
||||
|
||||
rerender(<ContextUsagePanel currentUsage={initialUsage} requestGateway={firstGateway} sessionId="runtime-2" />)
|
||||
rerender({ enabled: true })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(firstGateway).toHaveBeenCalledTimes(2)
|
||||
expect(firstGateway).toHaveBeenLastCalledWith('session.context_breakdown', { session_id: 'runtime-2' })
|
||||
})
|
||||
await waitFor(() => expect(requestGateway).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
rerender(<ContextUsagePanel currentUsage={initialUsage} requestGateway={secondGateway} sessionId="runtime-2" />)
|
||||
it('skips the estimate mid-turn — the gateway streams measured usage then', () => {
|
||||
const requestGateway = vi.fn().mockResolvedValue(breakdown)
|
||||
|
||||
await waitFor(() => expect(secondGateway).toHaveBeenCalledTimes(1))
|
||||
renderHook(() => useContextBreakdown({ busy: true, enabled: true, requestGateway, sessionId: 'runtime-1' }))
|
||||
|
||||
expect(requestGateway).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refetches on a session switch and never reports the previous session numbers', async () => {
|
||||
const requestGateway = vi.fn().mockResolvedValue(breakdown)
|
||||
|
||||
const { rerender, result } = renderHook(
|
||||
({ sessionId }) => useContextBreakdown({ busy: false, enabled: true, requestGateway, sessionId }),
|
||||
{ initialProps: { sessionId: 'runtime-1' } }
|
||||
)
|
||||
|
||||
await waitFor(() => expect(result.current.breakdown).toEqual(breakdown))
|
||||
|
||||
// Switching sessions must drop the numbers immediately — painting them
|
||||
// under the new session's name would be a lie until its own fetch lands.
|
||||
requestGateway.mockImplementation(() => new Promise(() => undefined))
|
||||
rerender({ sessionId: 'runtime-2' })
|
||||
|
||||
expect(result.current.breakdown).toBeNull()
|
||||
expect(requestGateway).toHaveBeenLastCalledWith('session.context_breakdown', { session_id: 'runtime-2' })
|
||||
})
|
||||
|
||||
it('reports the measured occupancy the backend sends, not just the estimate', async () => {
|
||||
// `context_used` on the payload is already the measured figure once a turn
|
||||
// has run — the estimate is the backend's own fallback, not a second value
|
||||
// the client has to choose between.
|
||||
const measured: ContextBreakdown = { ...breakdown, context_used: 12_000 }
|
||||
const requestGateway = vi.fn().mockResolvedValue(measured)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useContextBreakdown({ busy: false, enabled: true, requestGateway, sessionId: 'runtime-1' })
|
||||
)
|
||||
|
||||
await waitFor(() => expect(result.current.breakdown?.context_used).toBe(12_000))
|
||||
})
|
||||
})
|
||||
|
||||
describe('ContextUsagePanel', () => {
|
||||
it('renders the usage it is handed, so the popover matches the bar', () => {
|
||||
render(<ContextUsagePanel breakdown={breakdown} loading={false} usage={usage} />)
|
||||
|
||||
expect(screen.getByText('47% Full')).toBeTruthy()
|
||||
expect(screen.getByText('Conversation')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('says so when there is no breakdown rather than painting an empty bar', () => {
|
||||
render(<ContextUsagePanel breakdown={null} loading={false} usage={usage} />)
|
||||
|
||||
expect(screen.getByText('No context data yet')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user