Skip to content

Commit 9c26370

Browse files
committed
test: add comprehensive test coverage
1 parent 592bfbd commit 9c26370

File tree

12 files changed

+3586
-6
lines changed

12 files changed

+3586
-6
lines changed

agents/__tests__/commander.test.ts

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import { describe, test, expect } from 'bun:test'
2+
3+
import commander from '../commander'
4+
5+
import type { AgentState } from '../types/agent-definition'
6+
import type { ToolResultOutput } from '../types/util-types'
7+
8+
describe('commander agent', () => {
9+
const createMockAgentState = (): AgentState => ({
10+
agentId: 'commander-test',
11+
runId: 'test-run',
12+
parentId: undefined,
13+
messageHistory: [],
14+
output: undefined,
15+
systemPrompt: '',
16+
toolDefinitions: {},
17+
contextTokenCount: 0,
18+
})
19+
20+
describe('definition', () => {
21+
test('has correct id', () => {
22+
expect(commander.id).toBe('commander')
23+
})
24+
25+
test('has display name', () => {
26+
expect(commander.displayName).toBe('Commander')
27+
})
28+
29+
test('uses haiku model', () => {
30+
expect(commander.model).toBe('anthropic/claude-haiku-4.5')
31+
})
32+
33+
test('has output mode set to last_message', () => {
34+
expect(commander.outputMode).toBe('last_message')
35+
})
36+
37+
test('does not include message history', () => {
38+
expect(commander.includeMessageHistory).toBe(false)
39+
})
40+
41+
test('has run_terminal_command tool', () => {
42+
expect(commander.toolNames).toContain('run_terminal_command')
43+
expect(commander.toolNames).toHaveLength(1)
44+
})
45+
})
46+
47+
describe('input schema', () => {
48+
test('requires command parameter', () => {
49+
const schema = commander.inputSchema
50+
const commandProp = schema?.params?.properties?.command
51+
expect(commandProp && typeof commandProp === 'object' && 'type' in commandProp && commandProp.type).toBe('string')
52+
expect(schema?.params?.required).toContain('command')
53+
})
54+
55+
test('has optional timeout_seconds parameter', () => {
56+
const schema = commander.inputSchema
57+
const timeoutProp = schema?.params?.properties?.timeout_seconds
58+
expect(timeoutProp && typeof timeoutProp === 'object' && 'type' in timeoutProp && timeoutProp.type).toBe('number')
59+
expect(schema?.params?.required).not.toContain('timeout_seconds')
60+
})
61+
62+
test('has optional rawOutput parameter', () => {
63+
const schema = commander.inputSchema
64+
const rawOutputProp = schema?.params?.properties?.rawOutput
65+
expect(rawOutputProp && typeof rawOutputProp === 'object' && 'type' in rawOutputProp && rawOutputProp.type).toBe('boolean')
66+
expect(schema?.params?.required).not.toContain('rawOutput')
67+
})
68+
69+
test('has prompt parameter', () => {
70+
expect(commander.inputSchema?.prompt?.type).toBe('string')
71+
})
72+
})
73+
74+
describe('handleSteps', () => {
75+
test('returns error when no command provided', () => {
76+
const mockAgentState = createMockAgentState()
77+
const mockLogger = {
78+
debug: () => {},
79+
info: () => {},
80+
warn: () => {},
81+
error: () => {},
82+
}
83+
84+
const generator = commander.handleSteps!({
85+
agentState: mockAgentState,
86+
logger: mockLogger as any,
87+
params: {},
88+
})
89+
90+
const result = generator.next()
91+
92+
const toolCall = result.value as {
93+
toolName: string
94+
input: { output: string }
95+
}
96+
expect(toolCall.toolName).toBe('set_output')
97+
expect(toolCall.input.output).toContain('Error')
98+
expect(toolCall.input.output).toContain('command')
99+
})
100+
101+
test('yields run_terminal_command with basic command', () => {
102+
const mockAgentState = createMockAgentState()
103+
const mockLogger = {
104+
debug: () => {},
105+
info: () => {},
106+
warn: () => {},
107+
error: () => {},
108+
}
109+
110+
const generator = commander.handleSteps!({
111+
agentState: mockAgentState,
112+
logger: mockLogger as any,
113+
params: { command: 'ls -la' },
114+
})
115+
116+
const result = generator.next()
117+
118+
expect(result.value).toEqual({
119+
toolName: 'run_terminal_command',
120+
input: {
121+
command: 'ls -la',
122+
},
123+
})
124+
})
125+
126+
test('yields run_terminal_command with timeout', () => {
127+
const mockAgentState = createMockAgentState()
128+
const mockLogger = {
129+
debug: () => {},
130+
info: () => {},
131+
warn: () => {},
132+
error: () => {},
133+
}
134+
135+
const generator = commander.handleSteps!({
136+
agentState: mockAgentState,
137+
logger: mockLogger as any,
138+
params: { command: 'sleep 10', timeout_seconds: 60 },
139+
})
140+
141+
const result = generator.next()
142+
143+
expect(result.value).toEqual({
144+
toolName: 'run_terminal_command',
145+
input: {
146+
command: 'sleep 10',
147+
timeout_seconds: 60,
148+
},
149+
})
150+
})
151+
152+
test('yields set_output with raw result when rawOutput is true', () => {
153+
const mockAgentState = createMockAgentState()
154+
const mockLogger = {
155+
debug: () => {},
156+
info: () => {},
157+
warn: () => {},
158+
error: () => {},
159+
}
160+
161+
const generator = commander.handleSteps!({
162+
agentState: mockAgentState,
163+
logger: mockLogger as any,
164+
params: { command: 'echo hello', rawOutput: true },
165+
})
166+
167+
// First yield is the command
168+
generator.next()
169+
170+
// Second yield should be set_output with the result
171+
const mockToolResult = {
172+
agentState: createMockAgentState(),
173+
toolResult: [{ type: 'json' as const, value: { stdout: 'hello' } }],
174+
stepsComplete: true,
175+
}
176+
const result = generator.next(mockToolResult)
177+
178+
const toolCall = result.value as {
179+
toolName: string
180+
input: { output: { stdout: string } }
181+
includeToolCall?: boolean
182+
}
183+
expect(toolCall.toolName).toBe('set_output')
184+
expect(toolCall.input.output).toEqual({ stdout: 'hello' })
185+
expect(toolCall.includeToolCall).toBe(false)
186+
expect(result.done).toBe(false)
187+
188+
// Next should be done
189+
const final = generator.next()
190+
expect(final.done).toBe(true)
191+
})
192+
193+
test('yields STEP for model analysis when rawOutput is false', () => {
194+
const mockAgentState = createMockAgentState()
195+
const mockLogger = {
196+
debug: () => {},
197+
info: () => {},
198+
warn: () => {},
199+
error: () => {},
200+
}
201+
202+
const generator = commander.handleSteps!({
203+
agentState: mockAgentState,
204+
logger: mockLogger as any,
205+
params: { command: 'ls -la', rawOutput: false },
206+
})
207+
208+
// First yield is the command
209+
generator.next()
210+
211+
// Second yield should be STEP for model analysis
212+
const mockToolResult = {
213+
agentState: createMockAgentState(),
214+
toolResult: [
215+
{ type: 'json' as const, value: { stdout: 'file1.txt\nfile2.txt' } },
216+
],
217+
stepsComplete: true,
218+
}
219+
const result = generator.next(mockToolResult)
220+
221+
expect(result.value).toBe('STEP')
222+
})
223+
224+
test('handles empty tool result gracefully', () => {
225+
const mockAgentState = createMockAgentState()
226+
const mockLogger = {
227+
debug: () => {},
228+
info: () => {},
229+
warn: () => {},
230+
error: () => {},
231+
}
232+
233+
const generator = commander.handleSteps!({
234+
agentState: mockAgentState,
235+
logger: mockLogger as any,
236+
params: { command: 'echo test', rawOutput: true },
237+
})
238+
239+
// First yield is the command
240+
generator.next()
241+
242+
// Second yield with empty result
243+
const result = generator.next({
244+
agentState: createMockAgentState(),
245+
toolResult: [] as ToolResultOutput[],
246+
stepsComplete: true,
247+
})
248+
249+
const toolCall = result.value as {
250+
toolName: string
251+
input: { output: string }
252+
}
253+
expect(toolCall.toolName).toBe('set_output')
254+
expect(toolCall.input.output).toBe('')
255+
})
256+
257+
test('handles non-json tool result', () => {
258+
const mockAgentState = createMockAgentState()
259+
const mockLogger = {
260+
debug: () => {},
261+
info: () => {},
262+
warn: () => {},
263+
error: () => {},
264+
}
265+
266+
const generator = commander.handleSteps!({
267+
agentState: mockAgentState,
268+
logger: mockLogger as any,
269+
params: { command: 'echo test', rawOutput: true },
270+
})
271+
272+
// First yield is the command
273+
generator.next()
274+
275+
// Second yield with non-json result
276+
const mockToolResult = {
277+
agentState: createMockAgentState(),
278+
toolResult: [{ type: 'json' as const, value: 'plain text output' }],
279+
stepsComplete: true,
280+
}
281+
const result = generator.next(mockToolResult)
282+
283+
const toolCall = result.value as {
284+
toolName: string
285+
input: { output: string }
286+
}
287+
expect(toolCall.toolName).toBe('set_output')
288+
expect(toolCall.input.output).toBe('')
289+
})
290+
291+
test('handleSteps can be serialized for sandbox execution', () => {
292+
const handleStepsString = commander.handleSteps!.toString()
293+
294+
// Verify it's a valid generator function string
295+
expect(handleStepsString).toMatch(/^function\*\s*\(/)
296+
297+
// Should be able to create a new function from it
298+
const isolatedFunction = new Function(`return (${handleStepsString})`)()
299+
expect(typeof isolatedFunction).toBe('function')
300+
})
301+
})
302+
303+
describe('system prompt', () => {
304+
test('contains command analysis instructions', () => {
305+
expect(commander.systemPrompt).toContain('terminal command')
306+
expect(commander.systemPrompt).toContain('output')
307+
})
308+
309+
test('contains concise description requirement', () => {
310+
expect(commander.systemPrompt).toContain('concise')
311+
})
312+
})
313+
314+
describe('instructions prompt', () => {
315+
test('instructs not to use tools', () => {
316+
expect(commander.instructionsPrompt).toContain('Do not use any tools')
317+
})
318+
319+
test('mentions analyzing command output', () => {
320+
expect(commander.instructionsPrompt).toContain('command')
321+
expect(commander.instructionsPrompt).toContain('output')
322+
})
323+
})
324+
})

0 commit comments

Comments
 (0)