Skip to content

Commit 2220f3f

Browse files
committed
tests: added/updated cli tests
1 parent bc4e3e9 commit 2220f3f

File tree

4 files changed

+293
-38
lines changed

4 files changed

+293
-38
lines changed

cli/src/__tests__/integration/local-agents.test.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ describe('Local Agent Integration', () => {
308308
).toContain('structured_output')
309309
})
310310

311-
test('allows running without authentication token', async () => {
311+
test('loads agent definitions without auth', () => {
312312
mkdirSync(agentsDir, { recursive: true })
313313

314314
writeAgentFile(
@@ -324,24 +324,8 @@ describe('Local Agent Integration', () => {
324324
`,
325325
)
326326

327-
const warnMock = mock(() => {})
328-
329-
mock.module('../../utils/auth', () => ({
330-
getAuthTokenDetails: () => ({ token: '', source: 'env' as const }),
331-
}))
332-
mock.module('../../utils/logger', () => ({
333-
logger: {
334-
info: mock(() => {}),
335-
error: mock(() => {}),
336-
warn: warnMock,
337-
debug: mock(() => {}),
338-
},
339-
}))
340-
341-
const { getCodebuffClient } = await import('../../utils/codebuff-client')
342-
const client = getCodebuffClient()
343-
344-
expect(client).toBeNull()
345-
expect(warnMock.mock.calls.length).toBeGreaterThan(0)
327+
const definitions = loadAgentDefinitions()
328+
expect(definitions).toHaveLength(1)
329+
expect(definitions[0].id).toBe('authless-agent')
346330
})
347331
})
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { renderHook, waitFor } from '@testing-library/react'
2+
import { mock } from 'bun:test'
3+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
4+
5+
import { useSendMessage } from '../use-send-message'
6+
import * as codebuffClient from '../../utils/codebuff-client'
7+
import { logger } from '../../utils/logger'
8+
9+
// Mock the codebuff client
10+
const mockRun = mock(async () => ({ credits: 100 }))
11+
const mockGetCodebuffClient = mock(() => ({
12+
run: mockRun,
13+
}))
14+
15+
mock.module('../../utils/codebuff-client', () => ({
16+
getCodebuffClient: mockGetCodebuffClient,
17+
formatToolOutput: mock(() => 'formatted output'),
18+
}))
19+
20+
mock.module('../../utils/load-agent-definitions', () => ({
21+
loadAgentDefinitions: mock(() => []),
22+
}))
23+
24+
const mockLoggerInfo = mock(() => {})
25+
mock.module('../../utils/logger', () => ({
26+
logger: {
27+
info: mockLoggerInfo,
28+
error: mock(() => {}),
29+
warn: mock(() => {}),
30+
debug: mock(() => {}),
31+
},
32+
}))
33+
34+
describe('useSendMessage timer', () => {
35+
let mockSetMessages: ReturnType<typeof mock>
36+
let mockSetFocusedAgentId: ReturnType<typeof mock>
37+
let mockSetInputFocused: ReturnType<typeof mock>
38+
let mockSetStreamingAgents: ReturnType<typeof mock>
39+
let mockSetCollapsedAgents: ReturnType<typeof mock>
40+
let mockSetActiveSubagents: ReturnType<typeof mock>
41+
let mockSetIsChainInProgress: ReturnType<typeof mock>
42+
let mockSetIsWaitingForResponse: ReturnType<typeof mock>
43+
let mockStartStreaming: ReturnType<typeof mock>
44+
let mockStopStreaming: ReturnType<typeof mock>
45+
let mockSetIsStreaming: ReturnType<typeof mock>
46+
let mockSetCanProcessQueue: ReturnType<typeof mock>
47+
let inputRef: React.MutableRefObject<any>
48+
let activeSubagentsRef: React.MutableRefObject<Set<string>>
49+
let isChainInProgressRef: React.MutableRefObject<boolean>
50+
let abortControllerRef: React.MutableRefObject<AbortController | null>
51+
52+
beforeEach(() => {
53+
mockSetMessages = mock((fn: any) => {
54+
if (typeof fn === 'function') {
55+
fn([])
56+
}
57+
})
58+
mockSetFocusedAgentId = mock(() => {})
59+
mockSetInputFocused = mock(() => {})
60+
mockSetStreamingAgents = mock((fn: any) => {
61+
if (typeof fn === 'function') {
62+
return fn(new Set())
63+
}
64+
})
65+
mockSetCollapsedAgents = mock((fn: any) => {
66+
if (typeof fn === 'function') {
67+
return fn(new Set())
68+
}
69+
})
70+
mockSetActiveSubagents = mock((fn: any) => {
71+
if (typeof fn === 'function') {
72+
return fn(new Set())
73+
}
74+
})
75+
mockSetIsChainInProgress = mock(() => {})
76+
mockSetIsWaitingForResponse = mock(() => {})
77+
mockStartStreaming = mock(() => {})
78+
mockStopStreaming = mock(() => {})
79+
mockSetIsStreaming = mock(() => {})
80+
mockSetCanProcessQueue = mock(() => {})
81+
inputRef = { current: { focus: mock(() => {}) } }
82+
activeSubagentsRef = { current: new Set() }
83+
isChainInProgressRef = { current: false }
84+
abortControllerRef = { current: null }
85+
86+
mockLoggerInfo.mockClear()
87+
mockRun.mockClear()
88+
})
89+
90+
afterEach(() => {
91+
mock.restore()
92+
})
93+
94+
test('logs timer start and end when sending a message', async () => {
95+
const { result } = renderHook(() =>
96+
useSendMessage({
97+
setMessages: mockSetMessages,
98+
setFocusedAgentId: mockSetFocusedAgentId,
99+
setInputFocused: mockSetInputFocused,
100+
inputRef,
101+
setStreamingAgents: mockSetStreamingAgents,
102+
setCollapsedAgents: mockSetCollapsedAgents,
103+
activeSubagentsRef,
104+
isChainInProgressRef,
105+
setActiveSubagents: mockSetActiveSubagents,
106+
setIsChainInProgress: mockSetIsChainInProgress,
107+
setIsWaitingForResponse: mockSetIsWaitingForResponse,
108+
startStreaming: mockStartStreaming,
109+
stopStreaming: mockStopStreaming,
110+
setIsStreaming: mockSetIsStreaming,
111+
setCanProcessQueue: mockSetCanProcessQueue,
112+
abortControllerRef,
113+
}),
114+
)
115+
116+
await result.current.sendMessage('test message', { agentMode: 'FAST' })
117+
118+
await waitFor(() => {
119+
// Find timer start log
120+
const timerStartLog = mockLoggerInfo.mock.calls.find(
121+
(call) =>
122+
call[1] && typeof call[1] === 'string' && call[1].includes('[TIMER] Timer START'),
123+
)
124+
expect(timerStartLog).toBeDefined()
125+
expect(timerStartLog?.[0]).toHaveProperty('startTime')
126+
127+
// Find timer end log
128+
const timerEndLog = mockLoggerInfo.mock.calls.find(
129+
(call) =>
130+
call[1] && typeof call[1] === 'string' && call[1].includes('[TIMER] Timer END'),
131+
)
132+
expect(timerEndLog).toBeDefined()
133+
expect(timerEndLog?.[0]).toHaveProperty('startTime')
134+
expect(timerEndLog?.[0]).toHaveProperty('endTime')
135+
expect(timerEndLog?.[0]).toHaveProperty('elapsedMs')
136+
expect(timerEndLog?.[0]).toHaveProperty('elapsedTime')
137+
})
138+
})
139+
140+
test('calculates elapsed time correctly', async () => {
141+
const startTime = Date.now()
142+
143+
const { result } = renderHook(() =>
144+
useSendMessage({
145+
setMessages: mockSetMessages,
146+
setFocusedAgentId: mockSetFocusedAgentId,
147+
setInputFocused: mockSetInputFocused,
148+
inputRef,
149+
setStreamingAgents: mockSetStreamingAgents,
150+
setCollapsedAgents: mockSetCollapsedAgents,
151+
activeSubagentsRef,
152+
isChainInProgressRef,
153+
setActiveSubagents: mockSetActiveSubagents,
154+
setIsChainInProgress: mockSetIsChainInProgress,
155+
setIsWaitingForResponse: mockSetIsWaitingForResponse,
156+
startStreaming: mockStartStreaming,
157+
stopStreaming: mockStopStreaming,
158+
setIsStreaming: mockSetIsStreaming,
159+
setCanProcessQueue: mockSetCanProcessQueue,
160+
abortControllerRef,
161+
}),
162+
)
163+
164+
await result.current.sendMessage('test message', { agentMode: 'FAST' })
165+
166+
await waitFor(() => {
167+
const timerEndLog = mockLoggerInfo.mock.calls.find(
168+
(call) =>
169+
call[1] && typeof call[1] === 'string' && call[1].includes('[TIMER] Timer END'),
170+
)
171+
172+
expect(timerEndLog).toBeDefined()
173+
const logData = timerEndLog?.[0]
174+
expect(logData.elapsedMs).toBeGreaterThanOrEqual(0)
175+
expect(logData.endTime).toBeGreaterThanOrEqual(logData.startTime)
176+
expect(logData.elapsedMs).toBe(logData.endTime - logData.startTime)
177+
178+
// Verify elapsed time string format
179+
const elapsedTimeStr = logData.elapsedTime
180+
expect(typeof elapsedTimeStr).toBe('string')
181+
expect(parseFloat(elapsedTimeStr)).toBeGreaterThanOrEqual(0)
182+
})
183+
})
184+
185+
test('includes completion time in message metadata', async () => {
186+
const { result } = renderHook(() =>
187+
useSendMessage({
188+
setMessages: mockSetMessages,
189+
setFocusedAgentId: mockSetFocusedAgentId,
190+
setInputFocused: mockSetInputFocused,
191+
inputRef,
192+
setStreamingAgents: mockSetStreamingAgents,
193+
setCollapsedAgents: mockSetCollapsedAgents,
194+
activeSubagentsRef,
195+
isChainInProgressRef,
196+
setActiveSubagents: mockSetActiveSubagents,
197+
setIsChainInProgress: mockSetIsChainInProgress,
198+
setIsWaitingForResponse: mockSetIsWaitingForResponse,
199+
startStreaming: mockStartStreaming,
200+
stopStreaming: mockStopStreaming,
201+
setIsStreaming: mockSetIsStreaming,
202+
setCanProcessQueue: mockSetCanProcessQueue,
203+
abortControllerRef,
204+
}),
205+
)
206+
207+
await result.current.sendMessage('test message', { agentMode: 'FAST' })
208+
209+
await waitFor(() => {
210+
// Find the setMessages call that marks completion
211+
const completionCall = mockSetMessages.mock.calls.find((call) => {
212+
const fn = call[0]
213+
if (typeof fn !== 'function') return false
214+
215+
const testMessages = [
216+
{
217+
id: 'ai-123',
218+
variant: 'ai',
219+
content: '',
220+
blocks: [],
221+
},
222+
]
223+
const result = fn(testMessages)
224+
return result.some((msg: any) => msg.isComplete && msg.completionTime)
225+
})
226+
227+
expect(completionCall).toBeDefined()
228+
})
229+
})
230+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import { formatValidationError } from '../validation-error-formatting'
4+
5+
describe('formatValidationError', () => {
6+
test('parses JSON array payloads and extracts field/message', () => {
7+
const raw = `[
8+
{
9+
"code": "custom",
10+
"path": [
11+
"toolNames"
12+
],
13+
"message": "Non-empty spawnableAgents array requires the 'spawn_agents' tool. Add 'spawn_agents' to toolNames or remove spawnableAgents."
14+
}
15+
]`
16+
17+
const result = formatValidationError(raw)
18+
19+
expect(result.fieldName).toBe('toolNames')
20+
expect(result.message).toBe(
21+
"Non-empty spawnableAgents array requires the 'spawn_agents' tool. Add 'spawn_agents' to toolNames or remove spawnableAgents.",
22+
)
23+
})
24+
25+
test('strips agent name prefix', () => {
26+
const result = formatValidationError('Agent "demo" (demo.ts): Invalid input: expected string, received number')
27+
28+
expect(result.fieldName).toBeUndefined()
29+
expect(result.message).toBe('Invalid input: expected string, received number')
30+
})
31+
32+
test('extracts field:message pattern', () => {
33+
const result = formatValidationError('instructions: Required field is missing')
34+
35+
expect(result.fieldName).toBe('instructions')
36+
expect(result.message).toBe('Required field is missing')
37+
})
38+
39+
test('handles messages without field patterns', () => {
40+
const result = formatValidationError('Schema validation failed: Generic error')
41+
42+
expect(result.fieldName).toBeUndefined()
43+
expect(result.message).toBe('Generic error')
44+
})
45+
46+
test('handles nested path from JSON error', () => {
47+
const raw = `[
48+
{
49+
"path": ["outputSchema", "properties", "summary"],
50+
"message": "Required"
51+
}
52+
]`
53+
54+
const result = formatValidationError(raw)
55+
56+
expect(result.fieldName).toBe('outputSchema.properties.summary')
57+
expect(result.message).toBe('Required')
58+
})
59+
})

plans/local-agents-edge-cases.md

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)