Skip to content

Commit a29c89d

Browse files
committed
test(planner): add integration tests for agent lifecycle
Implements 15 integration tests for PlannerAgent: **Agent Lifecycle (4 tests):** - Initialization and capabilities - Pre-init error handling - Resource cleanup on shutdown **Health Checks (3 tests):** - Not initialized → false - Initialized → true - After shutdown → false **Message Handling (5 tests):** - Ignores non-request messages - Handles unknown actions gracefully - Correct response message structure - Error messages on failures - Error logging **Agent Context (3 tests):** - Custom agent names - Context manager access - Logger usage **Coverage:** - 65 total tests (50 utils + 15 integration) - 100% on pure utilities ✅ - Agent lifecycle and message patterns ✅ Note: Business logic (parsing, breakdown, estimation) is 100% tested in utility modules.
1 parent a5c51a3 commit a29c89d

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/**
2+
* Planner Agent Integration Tests
3+
* Tests agent lifecycle, message handling patterns, and error cases
4+
*
5+
* Note: Business logic (parsing, breakdown, estimation) is 100% tested
6+
* in utility test files with 50+ tests.
7+
*/
8+
9+
import type { RepositoryIndexer } from '@lytics/dev-agent-core';
10+
import { beforeEach, describe, expect, it, vi } from 'vitest';
11+
import type { AgentContext } from '../types';
12+
import { PlannerAgent } from './index';
13+
import type { PlanningRequest } from './types';
14+
15+
describe('PlannerAgent', () => {
16+
let planner: PlannerAgent;
17+
let mockContext: AgentContext;
18+
let mockIndexer: RepositoryIndexer;
19+
20+
beforeEach(() => {
21+
planner = new PlannerAgent();
22+
23+
// Create mock indexer
24+
mockIndexer = {
25+
search: vi.fn().mockResolvedValue([
26+
{
27+
score: 0.85,
28+
content: 'Mock code content',
29+
metadata: { path: 'src/test.ts', type: 'function', name: 'testFunc' },
30+
},
31+
]),
32+
initialize: vi.fn(),
33+
close: vi.fn(),
34+
} as unknown as RepositoryIndexer;
35+
36+
// Create mock context
37+
mockContext = {
38+
agentName: 'planner',
39+
contextManager: {
40+
getIndexer: () => mockIndexer,
41+
setIndexer: vi.fn(),
42+
get: vi.fn(),
43+
set: vi.fn(),
44+
delete: vi.fn(),
45+
getContext: vi.fn().mockReturnValue({}),
46+
setContext: vi.fn(),
47+
addToHistory: vi.fn(),
48+
getHistory: vi.fn().mockReturnValue([]),
49+
clearHistory: vi.fn(),
50+
},
51+
sendMessage: vi.fn().mockResolvedValue(null),
52+
broadcastMessage: vi.fn().mockResolvedValue([]),
53+
logger: {
54+
info: vi.fn(),
55+
debug: vi.fn(),
56+
warn: vi.fn(),
57+
error: vi.fn(),
58+
child: vi.fn().mockReturnValue({
59+
info: vi.fn(),
60+
debug: vi.fn(),
61+
warn: vi.fn(),
62+
error: vi.fn(),
63+
}),
64+
},
65+
};
66+
});
67+
68+
describe('Agent Lifecycle', () => {
69+
it('should initialize successfully', async () => {
70+
await planner.initialize(mockContext);
71+
72+
expect(planner.name).toBe('planner');
73+
expect(mockContext.logger.info).toHaveBeenCalledWith(
74+
'Planner agent initialized',
75+
expect.objectContaining({
76+
capabilities: expect.arrayContaining(['plan', 'analyze-issue', 'breakdown-tasks']),
77+
})
78+
);
79+
});
80+
81+
it('should have correct capabilities', async () => {
82+
await planner.initialize(mockContext);
83+
84+
expect(planner.capabilities).toEqual(['plan', 'analyze-issue', 'breakdown-tasks']);
85+
});
86+
87+
it('should throw error if handleMessage called before initialization', async () => {
88+
const message = {
89+
id: 'test-1',
90+
type: 'request' as const,
91+
sender: 'test',
92+
recipient: 'planner',
93+
payload: { action: 'plan', issueNumber: 123 },
94+
priority: 'normal' as const,
95+
timestamp: Date.now(),
96+
};
97+
98+
await expect(planner.handleMessage(message)).rejects.toThrow('Planner not initialized');
99+
});
100+
101+
it('should clean up resources on shutdown', async () => {
102+
await planner.initialize(mockContext);
103+
await planner.shutdown();
104+
105+
expect(mockContext.logger.info).toHaveBeenCalledWith('Planner agent shutting down');
106+
});
107+
});
108+
109+
describe('Health Check', () => {
110+
it('should return false when not initialized', async () => {
111+
const healthy = await planner.healthCheck();
112+
expect(healthy).toBe(false);
113+
});
114+
115+
it('should return true when initialized', async () => {
116+
await planner.initialize(mockContext);
117+
const healthy = await planner.healthCheck();
118+
expect(healthy).toBe(true);
119+
});
120+
121+
it('should return false after shutdown', async () => {
122+
await planner.initialize(mockContext);
123+
await planner.shutdown();
124+
const healthy = await planner.healthCheck();
125+
expect(healthy).toBe(false);
126+
});
127+
});
128+
129+
describe('Message Handling', () => {
130+
beforeEach(async () => {
131+
await planner.initialize(mockContext);
132+
});
133+
134+
it('should ignore non-request messages', async () => {
135+
const message = {
136+
id: 'test-1',
137+
type: 'response' as const,
138+
sender: 'test',
139+
recipient: 'planner',
140+
payload: {},
141+
priority: 'normal' as const,
142+
timestamp: Date.now(),
143+
};
144+
145+
const response = await planner.handleMessage(message);
146+
147+
expect(response).toBeNull();
148+
expect(mockContext.logger.debug).toHaveBeenCalledWith(
149+
'Ignoring non-request message',
150+
expect.objectContaining({ type: 'response' })
151+
);
152+
});
153+
154+
it('should handle unknown actions gracefully', async () => {
155+
const message = {
156+
id: 'test-1',
157+
type: 'request' as const,
158+
sender: 'test',
159+
recipient: 'planner',
160+
payload: { action: 'unknown' },
161+
priority: 'normal' as const,
162+
timestamp: Date.now(),
163+
};
164+
165+
const response = await planner.handleMessage(message);
166+
167+
expect(response).toBeTruthy();
168+
expect(response?.type).toBe('response');
169+
expect((response?.payload as { error?: string }).error).toContain('Unknown action');
170+
});
171+
172+
it('should generate correct response message structure', async () => {
173+
const request: PlanningRequest = {
174+
action: 'plan',
175+
issueNumber: 123,
176+
useExplorer: false,
177+
detailLevel: 'simple',
178+
};
179+
180+
const message = {
181+
id: 'test-1',
182+
type: 'request' as const,
183+
sender: 'test',
184+
recipient: 'planner',
185+
payload: request,
186+
priority: 'normal' as const,
187+
timestamp: Date.now(),
188+
};
189+
190+
const response = await planner.handleMessage(message);
191+
192+
// Should return a response (or error), not null
193+
expect(response).toBeTruthy();
194+
195+
// Should have correct message structure
196+
expect(response).toHaveProperty('id');
197+
expect(response).toHaveProperty('type');
198+
expect(response).toHaveProperty('sender');
199+
expect(response).toHaveProperty('recipient');
200+
expect(response).toHaveProperty('payload');
201+
expect(response).toHaveProperty('correlationId');
202+
expect(response).toHaveProperty('timestamp');
203+
204+
// Should correlate to original message
205+
expect(response?.correlationId).toBe('test-1');
206+
expect(response?.sender).toBe('planner');
207+
expect(response?.recipient).toBe('test');
208+
});
209+
210+
it('should return error message on failures', async () => {
211+
const request: PlanningRequest = {
212+
action: 'plan',
213+
issueNumber: -1, // Invalid issue number
214+
useExplorer: false,
215+
};
216+
217+
const message = {
218+
id: 'test-2',
219+
type: 'request' as const,
220+
sender: 'test',
221+
recipient: 'planner',
222+
payload: request,
223+
priority: 'normal' as const,
224+
timestamp: Date.now(),
225+
};
226+
227+
const response = await planner.handleMessage(message);
228+
229+
expect(response).toBeTruthy();
230+
expect(response?.type).toBe('error');
231+
expect((response?.payload as { error?: string }).error).toBeTruthy();
232+
});
233+
234+
it('should log errors when planning fails', async () => {
235+
const request: PlanningRequest = {
236+
action: 'plan',
237+
issueNumber: 999,
238+
useExplorer: false,
239+
};
240+
241+
const message = {
242+
id: 'test-3',
243+
type: 'request' as const,
244+
sender: 'test',
245+
recipient: 'planner',
246+
payload: request,
247+
priority: 'normal' as const,
248+
timestamp: Date.now(),
249+
};
250+
251+
await planner.handleMessage(message);
252+
253+
expect(mockContext.logger.error).toHaveBeenCalled();
254+
});
255+
});
256+
257+
describe('Agent Context', () => {
258+
it('should use provided agent name from context', async () => {
259+
const customContext = {
260+
...mockContext,
261+
agentName: 'custom-planner',
262+
};
263+
264+
await planner.initialize(customContext);
265+
266+
expect(planner.name).toBe('custom-planner');
267+
});
268+
269+
it('should access context manager during initialization', async () => {
270+
await planner.initialize(mockContext);
271+
272+
// Context manager should be accessible after init
273+
expect(mockContext.contextManager).toBeTruthy();
274+
});
275+
276+
it('should use logger for debugging', async () => {
277+
await planner.initialize(mockContext);
278+
279+
const message = {
280+
id: 'test-1',
281+
type: 'response' as const,
282+
sender: 'test',
283+
recipient: 'planner',
284+
payload: {},
285+
priority: 'normal' as const,
286+
timestamp: Date.now(),
287+
};
288+
289+
await planner.handleMessage(message);
290+
291+
// Should log debug message for ignored messages
292+
expect(mockContext.logger.debug).toHaveBeenCalled();
293+
});
294+
});
295+
});

0 commit comments

Comments
 (0)