Skip to content

Commit a3be3d7

Browse files
committed
refactor: extract validation state logic into custom hook
- Create useValidationState hook to encapsulate validation retry logic - Add test helper functions to reduce code duplication - Move validation state management from index.tsx to dedicated hook - Add runGetUserInfo helper in SDK tests for cleaner test code
1 parent 4f7405f commit a3be3d7

File tree

4 files changed

+142
-133
lines changed

4 files changed

+142
-133
lines changed

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

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ import type { Logger } from '@codebuff/common/types/contracts/logger'
1010
* network errors and authentication errors.
1111
*/
1212

13+
const baseUser = (overrides?: Partial<{ id: string; email: string; discord_id: string | null }>) => ({
14+
id: 'user-123',
15+
email: 'test@example.com',
16+
discord_id: null,
17+
...overrides,
18+
})
19+
20+
const mockResolves = (value: Awaited<ReturnType<GetUserInfoFromApiKeyFn>>) =>
21+
mock<GetUserInfoFromApiKeyFn>(async () => value)
22+
23+
const mockRejects = (error: Error & { code?: string }) =>
24+
mock<GetUserInfoFromApiKeyFn>(async () => {
25+
throw error
26+
})
27+
1328
describe('validateApiKey', () => {
1429
let mockLogger: Logger
1530
let mockGetUserInfoFromApiKey: ReturnType<typeof mock<GetUserInfoFromApiKeyFn>>
@@ -29,11 +44,7 @@ describe('validateApiKey', () => {
2944

3045
describe('successful validation', () => {
3146
test('returns user info for valid API key', async () => {
32-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => ({
33-
id: 'user-123',
34-
email: 'test@example.com',
35-
discord_id: null,
36-
}))
47+
mockGetUserInfoFromApiKey = mockResolves(baseUser())
3748

3849
const result = await validateApiKey({
3950
apiKey: 'valid-key',
@@ -49,11 +60,9 @@ describe('validateApiKey', () => {
4960
})
5061

5162
test('passes correct parameters to getUserInfoFromApiKey', async () => {
52-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => ({
53-
id: 'user-456',
54-
email: 'user@test.com',
55-
discord_id: null,
56-
}))
63+
mockGetUserInfoFromApiKey = mockResolves(
64+
baseUser({ id: 'user-456', email: 'user@test.com' }),
65+
)
5766

5867
await validateApiKey({
5968
apiKey: 'test-api-key',
@@ -75,9 +84,7 @@ describe('validateApiKey', () => {
7584
) as any
7685
networkError.code = 'NETWORK_ERROR'
7786

78-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => {
79-
throw networkError
80-
})
87+
mockGetUserInfoFromApiKey = mockRejects(networkError)
8188

8289
try {
8390
await validateApiKey({
@@ -96,9 +103,7 @@ describe('validateApiKey', () => {
96103
const networkError = new Error('Network timeout') as any
97104
networkError.code = 'NETWORK_ERROR'
98105

99-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => {
100-
throw networkError
101-
})
106+
mockGetUserInfoFromApiKey = mockRejects(networkError)
102107

103108
try {
104109
await validateApiKey({
@@ -118,7 +123,7 @@ describe('validateApiKey', () => {
118123

119124
describe('authentication errors', () => {
120125
test('throws AUTH_FAILED error for invalid credentials', async () => {
121-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => null)
126+
mockGetUserInfoFromApiKey = mockResolves(null)
122127

123128
try {
124129
await validateApiKey({
@@ -136,9 +141,7 @@ describe('validateApiKey', () => {
136141
const authError = new Error('Authentication failed') as any
137142
authError.code = 'AUTH_FAILED'
138143

139-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => {
140-
throw authError
141-
})
144+
mockGetUserInfoFromApiKey = mockRejects(authError)
142145

143146
try {
144147
await validateApiKey({
@@ -157,9 +160,7 @@ describe('validateApiKey', () => {
157160
const authError = new Error('Auth failed') as any
158161
authError.code = 'AUTH_FAILED'
159162

160-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => {
161-
throw authError
162-
})
163+
mockGetUserInfoFromApiKey = mockRejects(authError)
163164

164165
try {
165166
await validateApiKey({
@@ -181,9 +182,7 @@ describe('validateApiKey', () => {
181182
test('logs and re-throws unknown errors', async () => {
182183
const unknownError = new Error('Something went wrong')
183184

184-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => {
185-
throw unknownError
186-
})
185+
mockGetUserInfoFromApiKey = mockRejects(unknownError)
187186

188187
try {
189188
await validateApiKey({
@@ -203,7 +202,7 @@ describe('validateApiKey', () => {
203202
})
204203

205204
test('handles null response as authentication error', async () => {
206-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => null)
205+
mockGetUserInfoFromApiKey = mockResolves(null)
207206

208207
try {
209208
await validateApiKey({
@@ -249,9 +248,7 @@ describe('validateApiKey', () => {
249248
const error = new Error('Test error') as any
250249
error.code = testCase.errorCode
251250

252-
mockGetUserInfoFromApiKey = mock<GetUserInfoFromApiKeyFn>(async () => {
253-
throw error
254-
})
251+
mockGetUserInfoFromApiKey = mockRejects(error)
255252

256253
try {
257254
await validateApiKey({
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useState, useRef, useCallback, useEffect } from 'react'
2+
3+
import { validateAgentsWithNetworkHandling } from '../utils/validate-agents-wrapper'
4+
import { loadAgentDefinitions } from '../utils/load-agent-definitions'
5+
import { logger } from '../utils/logger'
6+
7+
type LoadedAgentsData = {
8+
agents: Array<{ id: string; displayName: string }>
9+
agentsDir: string
10+
} | null
11+
12+
type UseValidationStateOptions = {
13+
loadedAgentsData: LoadedAgentsData
14+
initialValidationErrors: Array<{ id: string; message: string }>
15+
initialValidationNetworkError: string | null
16+
retryIntervalMs?: number
17+
}
18+
19+
export function useValidationState({
20+
loadedAgentsData,
21+
initialValidationErrors,
22+
initialValidationNetworkError,
23+
retryIntervalMs = 5000,
24+
}: UseValidationStateOptions) {
25+
const [validationErrors, setValidationErrors] = useState(
26+
initialValidationErrors,
27+
)
28+
const [validationNetworkError, setValidationNetworkError] = useState(
29+
initialValidationNetworkError,
30+
)
31+
const isValidationInFlight = useRef(false)
32+
33+
const refreshValidationState = useCallback(async () => {
34+
if (!loadedAgentsData || isValidationInFlight.current) {
35+
return
36+
}
37+
38+
isValidationInFlight.current = true
39+
try {
40+
const agentDefinitions = loadAgentDefinitions()
41+
const validationResult = await validateAgentsWithNetworkHandling(
42+
agentDefinitions,
43+
{ remote: true },
44+
)
45+
46+
setValidationErrors(validationResult.validationErrors)
47+
setValidationNetworkError(validationResult.networkError)
48+
} catch (error) {
49+
logger.warn(
50+
{
51+
error: error instanceof Error ? error.message : String(error),
52+
},
53+
'Agent validation retry failed',
54+
)
55+
} finally {
56+
isValidationInFlight.current = false
57+
}
58+
}, [loadedAgentsData])
59+
60+
useEffect(() => {
61+
if (!loadedAgentsData || !validationNetworkError) {
62+
return
63+
}
64+
65+
const interval = setInterval(() => {
66+
void refreshValidationState()
67+
}, retryIntervalMs)
68+
69+
return () => clearInterval(interval)
70+
}, [
71+
loadedAgentsData,
72+
validationNetworkError,
73+
refreshValidationState,
74+
retryIntervalMs,
75+
])
76+
77+
return {
78+
validationErrors,
79+
validationNetworkError,
80+
refreshValidationState,
81+
}
82+
}

cli/src/index.tsx

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { getUserCredentials } from './utils/auth'
1818
import { loadAgentDefinitions } from './utils/load-agent-definitions'
1919
import { getLoadedAgentsData } from './utils/local-agent-registry'
2020
import { clearLogFile, logger } from './utils/logger'
21+
import { useValidationState } from './hooks/use-validation-state'
2122

2223
import type { FileTreeNode } from '@codebuff/common/util/file'
2324

@@ -163,9 +164,12 @@ async function bootstrapCli(): Promise<void> {
163164

164165
if (loadedAgentsData) {
165166
const agentDefinitions = loadAgentDefinitions()
166-
const validationResult = await validateAgentsWithNetworkHandling(agentDefinitions, {
167-
remote: true,
168-
})
167+
const validationResult = await validateAgentsWithNetworkHandling(
168+
agentDefinitions,
169+
{
170+
remote: true,
171+
},
172+
)
169173

170174
initialValidationErrors = validationResult.validationErrors
171175
initialValidationNetworkError = validationResult.networkError
@@ -178,39 +182,12 @@ async function bootstrapCli(): Promise<void> {
178182
const [hasInvalidCredentials, setHasInvalidCredentials] =
179183
React.useState(false)
180184
const [fileTree, setFileTree] = React.useState<FileTreeNode[]>([])
181-
const [validationErrors, setValidationErrors] = React.useState(
185+
const { validationErrors, validationNetworkError } = useValidationState({
186+
loadedAgentsData,
182187
initialValidationErrors,
183-
)
184-
const [validationNetworkError, setValidationNetworkError] =
185-
React.useState(initialValidationNetworkError)
186-
const isValidationInFlight = React.useRef(false)
187-
188-
const refreshValidationState = React.useCallback(async () => {
189-
if (!loadedAgentsData || isValidationInFlight.current) {
190-
return
191-
}
192-
193-
isValidationInFlight.current = true
194-
try {
195-
const agentDefinitions = loadAgentDefinitions()
196-
const validationResult = await validateAgentsWithNetworkHandling(
197-
agentDefinitions,
198-
{ remote: true },
199-
)
200-
201-
setValidationErrors(validationResult.validationErrors)
202-
setValidationNetworkError(validationResult.networkError)
203-
} catch (error) {
204-
logger.warn(
205-
{
206-
error: error instanceof Error ? error.message : String(error),
207-
},
208-
'Agent validation retry failed',
209-
)
210-
} finally {
211-
isValidationInFlight.current = false
212-
}
213-
}, [loadedAgentsData])
188+
initialValidationNetworkError,
189+
retryIntervalMs: VALIDATION_RETRY_INTERVAL_MS,
190+
})
214191

215192
React.useEffect(() => {
216193
const userCredentials = getUserCredentials()
@@ -247,20 +224,7 @@ async function bootstrapCli(): Promise<void> {
247224
loadFileTree()
248225
}, [])
249226

250-
React.useEffect(() => {
251-
if (!loadedAgentsData || !validationNetworkError) {
252-
return
253-
}
254-
255-
const interval = setInterval(() => {
256-
void refreshValidationState()
257-
}, VALIDATION_RETRY_INTERVAL_MS)
258-
259-
return () => clearInterval(interval)
260-
}, [loadedAgentsData, validationNetworkError, refreshValidationState])
261-
262227
return (
263-
// Hi!
264228
<App
265229
initialPrompt={initialPrompt}
266230
agentId={agent}

0 commit comments

Comments
 (0)