Skip to content

Commit 5ace012

Browse files
committed
refactor(cli): simplify usage query by removing unnecessary useEffect
- Remove useEffect from useUsageQuery - now returns TanStack Query directly - Update UsageBanner to read from query result instead of chat store - Remove usageData and setUsageData from chat store (no longer needed) - Update all tests to reflect new architecture - Mark fetchAndUpdateUsage as deprecated Data now flows directly from TanStack Query to components without unnecessary store synchronization, improving code clarity and reactivity.
1 parent 3e0dc95 commit 5ace012

File tree

7 files changed

+69
-100
lines changed

7 files changed

+69
-100
lines changed

cli/src/__tests__/integration/usage-refresh-on-completion.test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
1+
import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test'
22
import { QueryClient } from '@tanstack/react-query'
33

44
import { useChatStore } from '../../state/chat-store'
@@ -20,11 +20,10 @@ import * as authModule from '../../utils/auth'
2020
*/
2121
describe('Usage Refresh on SDK Completion', () => {
2222
const originalFetch = globalThis.fetch
23-
const originalGetAuthToken = authModule.getAuthToken
2423
const originalEnv = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
2524

2625
let queryClient: QueryClient
27-
let getAuthTokenMock: ReturnType<typeof mock>
26+
let getAuthTokenSpy: ReturnType<typeof spyOn>
2827

2928
beforeEach(() => {
3029
process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = 'https://test.codebuff.local'
@@ -40,8 +39,7 @@ describe('Usage Refresh on SDK Completion', () => {
4039
})
4140

4241
// Mock auth token
43-
getAuthTokenMock = mock(() => 'test-token')
44-
authModule.getAuthToken = getAuthTokenMock as any
42+
getAuthTokenSpy = spyOn(authModule, 'getAuthToken').mockReturnValue('test-token')
4543

4644
// Mock successful API response
4745
globalThis.fetch = mock(async () =>
@@ -59,7 +57,7 @@ describe('Usage Refresh on SDK Completion', () => {
5957

6058
afterEach(() => {
6159
globalThis.fetch = originalFetch
62-
authModule.getAuthToken = originalGetAuthToken
60+
getAuthTokenSpy.mockRestore()
6361
process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = originalEnv
6462
mock.restore()
6563
})
@@ -160,7 +158,7 @@ describe('Usage Refresh on SDK Completion', () => {
160158

161159
describe('unauthenticated scenarios', () => {
162160
test('should not fetch when no auth token', () => {
163-
getAuthTokenMock.mockReturnValue(undefined)
161+
getAuthTokenSpy.mockReturnValue(undefined)
164162
useChatStore.getState().setIsUsageVisible(true)
165163

166164
const fetchMock = mock(globalThis.fetch)

cli/src/components/usage-banner.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@ export const UsageBanner = () => {
1515
const { terminalWidth } = useTerminalDimensions()
1616
const theme = useTheme()
1717
const isUsageVisible = useChatStore((state) => state.isUsageVisible)
18-
const usageData = useChatStore((state) => state.usageData)
18+
const sessionCreditsUsed = useChatStore((state) => state.sessionCreditsUsed)
1919
const setIsUsageVisible = useChatStore((state) => state.setIsUsageVisible)
2020

2121
// Fetch usage data when banner is visible
22-
useUsageQuery({ enabled: isUsageVisible })
22+
const { data: apiData } = useUsageQuery({ enabled: isUsageVisible })
23+
24+
// Transform API data to usage data format
25+
const usageData = apiData
26+
? {
27+
sessionUsage: sessionCreditsUsed,
28+
remainingBalance: apiData.remainingBalance,
29+
nextQuotaReset: apiData.next_quota_reset,
30+
}
31+
: null
2332

2433
// Auto-hide banner after 60 seconds
2534
useEffect(() => {

cli/src/hooks/__tests__/use-usage-query.test.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
2-
import { renderHook, waitFor } from '@testing-library/react'
31
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2+
import { renderHook, waitFor } from '@testing-library/react'
3+
import {
4+
describe,
5+
test,
6+
expect,
7+
beforeEach,
8+
afterEach,
9+
mock,
10+
spyOn,
11+
} from 'bun:test'
412
import React from 'react'
513

6-
import { fetchUsageData, useUsageQuery, useRefreshUsage } from '../use-usage-query'
714
import { useChatStore } from '../../state/chat-store'
815
import * as authModule from '../../utils/auth'
9-
10-
import type { UsageData } from '../../state/chat-store'
16+
import {
17+
fetchUsageData,
18+
useUsageQuery,
19+
useRefreshUsage,
20+
} from '../use-usage-query'
1121

1222
describe('fetchUsageData', () => {
1323
const originalFetch = globalThis.fetch
@@ -25,18 +35,19 @@ describe('fetchUsageData', () => {
2535

2636
test('should fetch usage data successfully', async () => {
2737
const mockResponse = {
28-
type: 'usage-response',
38+
type: 'usage-response' as const,
2939
usage: 100,
3040
remainingBalance: 500,
3141
balanceBreakdown: { free: 300, paid: 200 },
3242
next_quota_reset: '2024-02-01T00:00:00.000Z',
3343
}
3444

35-
globalThis.fetch = mock(async () =>
36-
new Response(JSON.stringify(mockResponse), {
37-
status: 200,
38-
headers: { 'Content-Type': 'application/json' },
39-
}),
45+
globalThis.fetch = mock(
46+
async () =>
47+
new Response(JSON.stringify(mockResponse), {
48+
status: 200,
49+
headers: { 'Content-Type': 'application/json' },
50+
}),
4051
)
4152

4253
const result = await fetchUsageData({ authToken: 'test-token' })
@@ -63,7 +74,7 @@ describe('fetchUsageData', () => {
6374

6475
describe('useUsageQuery', () => {
6576
let queryClient: QueryClient
66-
const originalGetAuthToken = authModule.getAuthToken
77+
let getAuthTokenSpy: ReturnType<typeof spyOn>
6778
const originalEnv = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
6879

6980
function createWrapper() {
@@ -86,13 +97,15 @@ describe('useUsageQuery', () => {
8697
})
8798

8899
afterEach(() => {
89-
authModule.getAuthToken = originalGetAuthToken
100+
getAuthTokenSpy?.mockRestore()
90101
process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = originalEnv
91102
mock.restore()
92103
})
93104

94-
test('should fetch and update store when enabled', async () => {
95-
authModule.getAuthToken = mock(() => 'test-token')
105+
test('should fetch data when enabled', async () => {
106+
getAuthTokenSpy = spyOn(authModule, 'getAuthToken').mockReturnValue(
107+
'test-token',
108+
)
96109

97110
const mockResponse = {
98111
type: 'usage-response' as const,
@@ -101,11 +114,12 @@ describe('useUsageQuery', () => {
101114
next_quota_reset: '2024-02-01T00:00:00.000Z',
102115
}
103116

104-
globalThis.fetch = mock(async () =>
105-
new Response(JSON.stringify(mockResponse), {
106-
status: 200,
107-
headers: { 'Content-Type': 'application/json' },
108-
}),
117+
globalThis.fetch = mock(
118+
async () =>
119+
new Response(JSON.stringify(mockResponse), {
120+
status: 200,
121+
headers: { 'Content-Type': 'application/json' },
122+
}),
109123
)
110124

111125
const { result } = renderHook(() => useUsageQuery(), {
@@ -118,7 +132,9 @@ describe('useUsageQuery', () => {
118132
})
119133

120134
test('should not fetch when disabled', async () => {
121-
authModule.getAuthToken = mock(() => 'test-token')
135+
getAuthTokenSpy = spyOn(authModule, 'getAuthToken').mockReturnValue(
136+
'test-token',
137+
)
122138
const fetchMock = mock(async () => new Response('{}'))
123139
globalThis.fetch = fetchMock
124140

@@ -133,7 +149,9 @@ describe('useUsageQuery', () => {
133149
})
134150

135151
test('should not fetch when no auth token', async () => {
136-
authModule.getAuthToken = mock(() => undefined)
152+
getAuthTokenSpy = spyOn(authModule, 'getAuthToken').mockReturnValue(
153+
undefined,
154+
)
137155
const fetchMock = mock(async () => new Response('{}'))
138156
globalThis.fetch = fetchMock
139157

cli/src/hooks/use-usage-query.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import React from 'react'
21
import { useQuery, useQueryClient } from '@tanstack/react-query'
32

4-
import { useChatStore } from '../state/chat-store'
53
import { getAuthToken } from '../utils/auth'
64
import { logger as defaultLogger } from '../utils/logger'
75

@@ -70,17 +68,14 @@ export interface UseUsageQueryDeps {
7068
}
7169

7270
/**
73-
* Hook to fetch and manage usage data
74-
* Automatically updates the chat store when new data arrives
71+
* Hook to fetch usage data from the API
72+
* Returns TanStack Query result directly - no store synchronization needed
7573
*/
7674
export function useUsageQuery(deps: UseUsageQueryDeps = {}) {
7775
const { logger = defaultLogger, enabled = true } = deps
78-
const setUsageData = useChatStore((state) => state.setUsageData)
79-
const sessionCreditsUsed = useChatStore((state) => state.sessionCreditsUsed)
80-
8176
const authToken = getAuthToken()
8277

83-
const query = useQuery({
78+
return useQuery({
8479
queryKey: usageQueryKeys.current(),
8580
queryFn: () => fetchUsageData({ authToken: authToken!, logger }),
8681
enabled: enabled && !!authToken,
@@ -91,19 +86,6 @@ export function useUsageQuery(deps: UseUsageQueryDeps = {}) {
9186
refetchOnWindowFocus: false, // CLI doesn't have window focus
9287
refetchOnReconnect: false, // Don't auto-refetch on reconnect
9388
})
94-
95-
// Update store when data changes (replaces deprecated onSuccess)
96-
React.useEffect(() => {
97-
if (query.data) {
98-
setUsageData({
99-
sessionUsage: sessionCreditsUsed,
100-
remainingBalance: query.data.remainingBalance,
101-
nextQuotaReset: query.data.next_quota_reset,
102-
})
103-
}
104-
}, [query.data, sessionCreditsUsed, setUsageData])
105-
106-
return query
10789
}
10890

10991
/**

cli/src/state/chat-store.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export type ChatStoreState = {
3131
lastMessageMode: AgentMode | null
3232
sessionCreditsUsed: number
3333
runState: RunState | null
34-
usageData: UsageData | null
3534
isUsageVisible: boolean
3635
}
3736

@@ -61,19 +60,12 @@ type ChatStoreActions = {
6160
setLastMessageMode: (mode: AgentMode | null) => void
6261
addSessionCredits: (credits: number) => void
6362
setRunState: (runState: RunState | null) => void
64-
setUsageData: (data: UsageData | null) => void
6563
setIsUsageVisible: (visible: boolean) => void
6664
reset: () => void
6765
}
6866

6967
type ChatStore = ChatStoreState & ChatStoreActions
7068

71-
export interface UsageData {
72-
sessionUsage: number
73-
remainingBalance: number | null
74-
nextQuotaReset: string | null
75-
}
76-
7769
const initialState: ChatStoreState = {
7870
messages: [],
7971
streamingAgents: new Set<string>(),
@@ -91,7 +83,6 @@ const initialState: ChatStoreState = {
9183
lastMessageMode: null,
9284
sessionCreditsUsed: 0,
9385
runState: null,
94-
usageData: null,
9586
isUsageVisible: false,
9687
}
9788

@@ -196,11 +187,6 @@ export const useChatStore = create<ChatStore>()(
196187
state.runState = runState ? castDraft(runState) : null
197188
}),
198189

199-
setUsageData: (data) =>
200-
set((state) => {
201-
state.usageData = data
202-
}),
203-
204190
setIsUsageVisible: (visible) =>
205191
set((state) => {
206192
state.isUsageVisible = visible
@@ -226,7 +212,6 @@ export const useChatStore = create<ChatStore>()(
226212
state.runState = initialState.runState
227213
? castDraft(initialState.runState)
228214
: null
229-
state.usageData = initialState.usageData
230215
state.isUsageVisible = initialState.isUsageVisible
231216
}),
232217
})),

0 commit comments

Comments
 (0)