Skip to content

Commit b6c1c4e

Browse files
committed
test: add agent-detector tests (10 tests)
Cover PATH detection, fallback search, mixed results, concurrency limiting, timeout handling, and WSL unavailability. Mocks child_process execFile to test without real WSL. Co-Authored-By: Rooty
1 parent 88a5002 commit b6c1c4e

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
import { execFile } from 'child_process'
3+
import { detectAgents } from '../agent-detector'
4+
import { AGENT_BINARY_MAP } from '../../shared/agents'
5+
6+
vi.mock('child_process', () => ({
7+
execFile: vi.fn(),
8+
}))
9+
10+
const mockExecFile = vi.mocked(execFile)
11+
12+
/** Stub logger that swallows all output */
13+
const stubLog = {
14+
info: vi.fn(),
15+
warn: vi.fn(),
16+
error: vi.fn(),
17+
debug: vi.fn(),
18+
}
19+
20+
type ExecCallback = (err: Error | null, stdout: string, stderr: string) => void
21+
22+
/**
23+
* Configure execFile mock to resolve agent checks.
24+
* @param found - set of binary names that should be "found"
25+
*/
26+
function mockAgentChecks(found: Set<string>): void {
27+
mockExecFile.mockImplementation((_cmd: unknown, args: unknown, _opts: unknown, cb: unknown) => {
28+
const callback = cb as ExecCallback
29+
const argArr = args as string[]
30+
const cmdStr = argArr.join(' ')
31+
32+
// Diagnostics — always succeed
33+
if (
34+
cmdStr.includes('echo $SHELL') ||
35+
cmdStr.includes('--status') ||
36+
cmdStr.includes('--version') ||
37+
cmdStr.includes('echo "$PATH"') ||
38+
cmdStr.includes('npm bin') ||
39+
cmdStr.includes('node --version')
40+
) {
41+
callback(null, '/bin/bash', '')
42+
return undefined as never
43+
}
44+
45+
// PATH check: `command -v <binary>`
46+
if (cmdStr.includes('command -v')) {
47+
const bin = cmdStr.split('command -v ')[1]?.trim()
48+
if (bin && found.has(bin)) {
49+
callback(null, `/usr/local/bin/${bin}`, '')
50+
} else {
51+
callback(new Error('not found'), '', '')
52+
}
53+
return undefined as never
54+
}
55+
56+
// Fallback search script
57+
if (cmdStr.includes('found=""')) {
58+
// Check if any of the found binaries match
59+
const matchedBin = [...found].find((b) => cmdStr.includes(`/${b}`))
60+
if (matchedBin) {
61+
callback(null, `/home/user/.local/bin/${matchedBin}`, '')
62+
} else {
63+
callback(new Error('not found'), '', '')
64+
}
65+
return undefined as never
66+
}
67+
68+
// Default: fail
69+
callback(new Error('unknown command'), '', '')
70+
return undefined as never
71+
})
72+
}
73+
74+
describe('detectAgents', () => {
75+
beforeEach(() => {
76+
vi.clearAllMocks()
77+
})
78+
79+
afterEach(() => {
80+
vi.restoreAllMocks()
81+
})
82+
83+
it('returns a record with all agent IDs as keys', async () => {
84+
mockAgentChecks(new Set())
85+
const result = await detectAgents(stubLog)
86+
const expectedKeys = Object.keys(AGENT_BINARY_MAP)
87+
expect(Object.keys(result).sort()).toEqual(expectedKeys.sort())
88+
})
89+
90+
it('marks agents as true when found via PATH', async () => {
91+
mockAgentChecks(new Set(['claude', 'codex']))
92+
const result = await detectAgents(stubLog)
93+
expect(result['claude-code']).toBe(true)
94+
expect(result['codex']).toBe(true)
95+
})
96+
97+
it('marks agents as false when not found', async () => {
98+
mockAgentChecks(new Set())
99+
const result = await detectAgents(stubLog)
100+
for (const val of Object.values(result)) {
101+
expect(val).toBe(false)
102+
}
103+
})
104+
105+
it('handles mixed found/not-found results', async () => {
106+
mockAgentChecks(new Set(['claude']))
107+
const result = await detectAgents(stubLog)
108+
expect(result['claude-code']).toBe(true)
109+
expect(result['codex']).toBe(false)
110+
expect(result['aider']).toBe(false)
111+
})
112+
113+
it('logs total detection time', async () => {
114+
mockAgentChecks(new Set())
115+
await detectAgents(stubLog)
116+
expect(stubLog.info).toHaveBeenCalledWith(expect.stringContaining('Agent detection total:'))
117+
})
118+
119+
it('logs individual agent check results', async () => {
120+
mockAgentChecks(new Set(['claude']))
121+
await detectAgents(stubLog)
122+
expect(stubLog.info).toHaveBeenCalledWith(expect.stringMatching(/Agent check: claude found/))
123+
})
124+
125+
it('runs diagnostics without blocking agent checks', async () => {
126+
mockAgentChecks(new Set())
127+
await detectAgents(stubLog)
128+
// Diagnostics log via debug
129+
expect(stubLog.debug).toHaveBeenCalledWith(expect.stringContaining('WSL diag'))
130+
})
131+
132+
it('respects MAX_CONCURRENT=3 (does not spawn all at once)', async () => {
133+
// Track concurrent calls
134+
let concurrent = 0
135+
let maxConcurrent = 0
136+
137+
mockExecFile.mockImplementation((_cmd: unknown, args: unknown, _opts: unknown, cb: unknown) => {
138+
const callback = cb as ExecCallback
139+
const argArr = args as string[]
140+
const cmdStr = argArr.join(' ')
141+
142+
// Diagnostics — instant
143+
if (!cmdStr.includes('command -v') && !cmdStr.includes('found=""')) {
144+
callback(null, '', '')
145+
return undefined as never
146+
}
147+
148+
concurrent++
149+
if (concurrent > maxConcurrent) maxConcurrent = concurrent
150+
151+
// Simulate async delay
152+
setTimeout(() => {
153+
concurrent--
154+
callback(new Error('not found'), '', '')
155+
}, 10)
156+
157+
return undefined as never
158+
})
159+
160+
await detectAgents(stubLog)
161+
// Max concurrent should be <= 3 (the MAX_CONCURRENT limit)
162+
// Each agent can trigger up to 2 calls (PATH + fallback), but concurrency
163+
// is limited at the agent level (3 agents checked simultaneously)
164+
expect(maxConcurrent).toBeLessThanOrEqual(6) // 3 agents × 2 calls each
165+
})
166+
167+
it('handles execFile timeout gracefully', async () => {
168+
mockExecFile.mockImplementation(
169+
(_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
170+
const callback = cb as ExecCallback
171+
const err = new Error('ETIMEDOUT') as NodeJS.ErrnoException
172+
err.code = 'ETIMEDOUT'
173+
callback(err, '', '')
174+
return undefined as never
175+
},
176+
)
177+
178+
const result = await detectAgents(stubLog)
179+
// All agents should be false on timeout
180+
for (const val of Object.values(result)) {
181+
expect(val).toBe(false)
182+
}
183+
})
184+
185+
it('returns false for agents when wsl.exe is not available', async () => {
186+
mockExecFile.mockImplementation(
187+
(_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
188+
const callback = cb as ExecCallback
189+
const err = new Error('ENOENT') as NodeJS.ErrnoException
190+
err.code = 'ENOENT'
191+
callback(err, '', '')
192+
return undefined as never
193+
},
194+
)
195+
196+
const result = await detectAgents(stubLog)
197+
for (const val of Object.values(result)) {
198+
expect(val).toBe(false)
199+
}
200+
})
201+
})

0 commit comments

Comments
 (0)