From c83061ba3d0e5467fca62c45e59bcfb38cfe5e3d Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 14 Aug 2026 18:33:43 -0500 Subject: [PATCH] test(desktop): cover the context gauge fetching before a turn runs --- .../app/shell/context-usage-panel.test.tsx | 121 +++++++++++------- 1 file changed, 72 insertions(+), 49 deletions(-) diff --git a/apps/desktop/src/app/shell/context-usage-panel.test.tsx b/apps/desktop/src/app/shell/context-usage-panel.test.tsx index 28d72e861a..ec854b2167 100644 --- a/apps/desktop/src/app/shell/context-usage-panel.test.tsx +++ b/apps/desktop/src/app/shell/context-usage-panel.test.tsx @@ -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 ( - { - published(snapshot) - setCurrentUsage(current => ({ ...current, ...snapshot })) - }} - requestGateway={requestGateway} - sessionId="runtime-1" - /> - ) - } - - render() - - 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( - + 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() + 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() + 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() + + 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() + + expect(screen.getByText('No context data yet')).toBeTruthy() }) })