Skip to content

Commit 4582ef2

Browse files
committed
Add typed mock factory utilities for testing
New testing utilities in common/src/testing/mocks/: - createMockLogger() - typed logger mock with capture support - createMockFetch(), installMockFetch() - typed fetch mock with call tracking - createMockFs() - typed filesystem mock compatible with CodebuffFileSystem - createMockTimers() - typed timer mock (deprecated, prefer Bun builtins) - Tree-sitter mocks: createMockTreeSitterParser/Query/Captures Test fixture improvements: - createTestAgentRuntimeParams() for agent runtime testing - mockFileContext, testClientEnv, testCiEnv exports Bug fixes: - Fix installMockFetch call-capture bug (calls now captured even after mockImplementation) - Fix null guard in toContentString (prevents TypeError on null array items) Updated test files to use typed mock factories: - packages/code-map tests use tree-sitter mock utilities - web/agents-transform.test.ts uses explicit type annotations - Various agent-runtime tests use typed fixtures Also trimmed verbose JSDoc comments across all testing utility files.
1 parent 5e7fbbf commit 4582ef2

File tree

30 files changed

+3385
-613
lines changed

30 files changed

+3385
-613
lines changed

agents/e2e/context-pruner.e2e.test.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@ import {
1010
type ToolMessage,
1111
type JSONValue,
1212
} from '@codebuff/sdk'
13+
14+
import type { ToolCallPart } from '@codebuff/common/types/messages/content-part'
15+
16+
/**
17+
* Type guard to check if a content part is a tool-call part with toolCallId.
18+
*/
19+
function isToolCallPart(part: unknown): part is ToolCallPart {
20+
return (
21+
typeof part === 'object' &&
22+
part !== null &&
23+
'type' in part &&
24+
part.type === 'tool-call' &&
25+
'toolCallId' in part &&
26+
typeof (part as ToolCallPart).toolCallId === 'string'
27+
)
28+
}
29+
30+
/**
31+
* Type guard to check if a message is a tool message with toolCallId.
32+
*/
33+
function isToolMessageWithId(msg: Message): msg is ToolMessage & { toolCallId: string } {
34+
return msg.role === 'tool' && 'toolCallId' in msg && typeof msg.toolCallId === 'string'
35+
}
1336
/**
1437
* Integration tests for the context-pruner agent.
1538
* These tests verify that context-pruner correctly prunes message history
@@ -154,8 +177,8 @@ Do not do anything else. Just spawn context-pruner and then report the result.`,
154177
for (const msg of finalMessages) {
155178
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
156179
for (const part of msg.content) {
157-
if (part.type === 'tool-call' && (part as any).toolCallId) {
158-
toolCallIds.add((part as any).toolCallId)
180+
if (isToolCallPart(part)) {
181+
toolCallIds.add(part.toolCallId)
159182
}
160183
}
161184
}
@@ -164,8 +187,8 @@ Do not do anything else. Just spawn context-pruner and then report the result.`,
164187
// Extract all tool result IDs
165188
const toolResultIds = new Set<string>()
166189
for (const msg of finalMessages) {
167-
if (msg.role === 'tool' && (msg as any).toolCallId) {
168-
toolResultIds.add((msg as any).toolCallId)
190+
if (isToolMessageWithId(msg)) {
191+
toolResultIds.add(msg.toolCallId)
169192
}
170193
}
171194

@@ -280,13 +303,13 @@ Do not do anything else. Just spawn context-pruner and then report the result.`,
280303
for (const msg of finalMessages) {
281304
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
282305
for (const part of msg.content) {
283-
if (part.type === 'tool-call' && (part as any).toolCallId) {
284-
toolCallIds.add((part as any).toolCallId)
306+
if (isToolCallPart(part)) {
307+
toolCallIds.add(part.toolCallId)
285308
}
286309
}
287310
}
288-
if (msg.role === 'tool' && (msg as any).toolCallId) {
289-
toolResultIds.add((msg as any).toolCallId)
311+
if (isToolMessageWithId(msg)) {
312+
toolResultIds.add(msg.toolCallId)
290313
}
291314
}
292315

cli/src/__tests__/unit/copy-button.test.ts

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createMockTimers } from '@codebuff/common/testing/mocks/timers'
12
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
23

34
import {
@@ -10,6 +11,8 @@ import {
1011
} from '../../components/copy-button'
1112
import { initializeThemeStore } from '../../hooks/use-theme'
1213

14+
import type { MockTimers } from '@codebuff/common/testing/mocks/timers'
15+
1316
// Initialize theme before tests
1417
initializeThemeStore()
1518

@@ -101,39 +104,15 @@ describe('CopyButton - exported constants', () => {
101104
})
102105

103106
describe('CopyButton - copied state reset timing', () => {
104-
let originalSetTimeout: typeof setTimeout
105-
let originalClearTimeout: typeof clearTimeout
106-
let timers: { id: number; ms: number; fn: Function; active: boolean }[]
107-
let nextId: number
108-
109-
const runTimers = () => {
110-
for (const t of timers) {
111-
if (t.active) t.fn()
112-
}
113-
timers = []
114-
}
107+
let mockTimers: MockTimers
115108

116109
beforeEach(() => {
117-
timers = []
118-
nextId = 1
119-
originalSetTimeout = globalThis.setTimeout
120-
originalClearTimeout = globalThis.clearTimeout
121-
122-
globalThis.setTimeout = ((fn: Function, ms?: number) => {
123-
const id = nextId++
124-
timers.push({ id, ms: Number(ms ?? 0), fn, active: true })
125-
return id as any
126-
}) as any
127-
128-
globalThis.clearTimeout = ((id?: any) => {
129-
const rec = timers.find((t) => t.id === id)
130-
if (rec) rec.active = false
131-
}) as any
110+
mockTimers = createMockTimers()
111+
mockTimers.install()
132112
})
133113

134114
afterEach(() => {
135-
globalThis.setTimeout = originalSetTimeout
136-
globalThis.clearTimeout = originalClearTimeout
115+
mockTimers.restore()
137116
})
138117

139118
test('uses the exported COPIED_RESET_DELAY_MS constant (2000ms)', () => {
@@ -150,10 +129,11 @@ describe('CopyButton - copied state reset timing', () => {
150129

151130
handleCopy()
152131
expect(isCopied).toBe(true)
153-
expect(timers.length).toBe(1)
154-
expect(timers[0].ms).toBe(COPIED_RESET_DELAY_MS)
132+
expect(mockTimers.getPendingCount()).toBe(1)
133+
const nextTimer = mockTimers.getNext()
134+
expect(nextTimer?.ms).toBe(COPIED_RESET_DELAY_MS)
155135

156-
runTimers()
136+
mockTimers.runAll()
157137
expect(isCopied).toBe(false)
158138
})
159139

@@ -176,8 +156,7 @@ describe('CopyButton - copied state reset timing', () => {
176156
handleCopy()
177157
handleCopy()
178158

179-
const activeTimers = timers.filter((t) => t.active)
180-
expect(activeTimers.length).toBe(1)
159+
expect(mockTimers.getPendingCount()).toBe(1)
181160
})
182161
})
183162

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

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createMockLogger } from '@codebuff/common/testing/mocks/logger'
12
import {
23
describe,
34
test,
@@ -145,47 +146,32 @@ describe('fetchUsageData', () => {
145146
globalThis.fetch = mock(
146147
async () => new Response('Error', { status: 500 }),
147148
) as unknown as typeof fetch
148-
const mockLogger = {
149-
error: mock(() => {}),
150-
warn: mock(() => {}),
151-
info: mock(() => {}),
152-
debug: mock(() => {}),
153-
}
149+
const mockLogger = createMockLogger()
154150

155151
await expect(
156-
fetchUsageData({ authToken: 'test-token', logger: mockLogger as any }),
152+
fetchUsageData({ authToken: 'test-token', logger: mockLogger }),
157153
).rejects.toThrow('Failed to fetch usage: 500')
158154
})
159155

160156
test('should throw error on 401 unauthorized', async () => {
161157
globalThis.fetch = mock(
162158
async () => new Response('Unauthorized', { status: 401 }),
163159
) as unknown as typeof fetch
164-
const mockLogger = {
165-
error: mock(() => {}),
166-
warn: mock(() => {}),
167-
info: mock(() => {}),
168-
debug: mock(() => {}),
169-
}
160+
const mockLogger = createMockLogger()
170161

171162
await expect(
172-
fetchUsageData({ authToken: 'invalid-token', logger: mockLogger as any }),
163+
fetchUsageData({ authToken: 'invalid-token', logger: mockLogger }),
173164
).rejects.toThrow('Failed to fetch usage: 401')
174165
})
175166

176167
test('should throw error on 402 payment required', async () => {
177168
globalThis.fetch = mock(
178169
async () => new Response('Payment Required', { status: 402 }),
179170
) as unknown as typeof fetch
180-
const mockLogger = {
181-
error: mock(() => {}),
182-
warn: mock(() => {}),
183-
info: mock(() => {}),
184-
debug: mock(() => {}),
185-
}
171+
const mockLogger = createMockLogger()
186172

187173
await expect(
188-
fetchUsageData({ authToken: 'test-token', logger: mockLogger as any }),
174+
fetchUsageData({ authToken: 'test-token', logger: mockLogger }),
189175
).rejects.toThrow('Failed to fetch usage: 402')
190176
})
191177

@@ -255,19 +241,13 @@ describe('fetchUsageData', () => {
255241
async () => new Response('Server Error', { status: 503 }),
256242
) as unknown as typeof fetch
257243

258-
const errorMock = mock(() => {})
259-
const mockLogger = {
260-
error: errorMock,
261-
warn: mock(() => {}),
262-
info: mock(() => {}),
263-
debug: mock(() => {}),
264-
}
244+
const mockLogger = createMockLogger()
265245

266246
await expect(
267-
fetchUsageData({ authToken: 'test-token', logger: mockLogger as any }),
247+
fetchUsageData({ authToken: 'test-token', logger: mockLogger }),
268248
).rejects.toThrow()
269249

270-
expect(errorMock).toHaveBeenCalledWith(
250+
expect(mockLogger.error).toHaveBeenCalledWith(
271251
{ status: 503 },
272252
'Failed to fetch usage data from API',
273253
)

0 commit comments

Comments
 (0)