Skip to content

Commit e1a7423

Browse files
committed
test(coordinator): add Explorer integration tests
- 17 comprehensive tests for Coordinator → Explorer flow - Tests message routing for all Explorer actions - Tests task execution via task queue - Tests health checks and context sharing - Tests graceful error handling All tests passing ✅
1 parent 830051c commit e1a7423

File tree

1 file changed

+310
-0
lines changed

1 file changed

+310
-0
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/**
2+
* Integration Tests: Coordinator → Explorer
3+
* Tests the full flow from Coordinator to Explorer Agent
4+
*/
5+
6+
import { mkdir, rm } from 'node:fs/promises';
7+
import { tmpdir } from 'node:os';
8+
import { join } from 'node:path';
9+
import { RepositoryIndexer } from '@lytics/dev-agent-core';
10+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11+
import { ExplorerAgent } from '../explorer';
12+
import { SubagentCoordinator } from './coordinator';
13+
14+
describe('Coordinator → Explorer Integration', () => {
15+
let coordinator: SubagentCoordinator;
16+
let explorer: ExplorerAgent;
17+
let indexer: RepositoryIndexer;
18+
let testVectorPath: string;
19+
20+
beforeEach(async () => {
21+
// Create temporary vector store
22+
testVectorPath = join(tmpdir(), `test-vectors-${Date.now()}`);
23+
await mkdir(testVectorPath, { recursive: true });
24+
25+
// Initialize coordinator
26+
coordinator = new SubagentCoordinator({
27+
logLevel: 'error', // Quiet during tests
28+
healthCheckInterval: 0, // Disable periodic checks
29+
});
30+
31+
// Initialize indexer (without indexing - tests will mock/stub as needed)
32+
indexer = new RepositoryIndexer({
33+
repositoryPath: process.cwd(),
34+
vectorStorePath: testVectorPath,
35+
});
36+
await indexer.initialize();
37+
38+
// Note: NOT indexing the full repo to avoid OOM in tests
39+
// Tests will use the indexer API without real data
40+
41+
// Set indexer in coordinator context
42+
coordinator.getContextManager().setIndexer(indexer);
43+
44+
// Create and register Explorer
45+
explorer = new ExplorerAgent();
46+
await coordinator.registerAgent(explorer);
47+
48+
coordinator.start();
49+
});
50+
51+
afterEach(async () => {
52+
await coordinator.stop();
53+
await indexer.close();
54+
await rm(testVectorPath, { recursive: true, force: true });
55+
});
56+
57+
describe('Agent Registration', () => {
58+
it('should register Explorer successfully', () => {
59+
const agents = coordinator.getAgents();
60+
expect(agents).toContain('explorer');
61+
});
62+
63+
it('should initialize Explorer with context', async () => {
64+
// Explorer is initialized but reports unhealthy without indexed data
65+
const healthCheck = await explorer.healthCheck();
66+
expect(healthCheck).toBe(false); // No vectors stored yet
67+
68+
// But it's still registered and can receive messages
69+
const response = await coordinator.sendMessage({
70+
type: 'request',
71+
sender: 'test',
72+
recipient: 'explorer',
73+
payload: { action: 'pattern', query: 'test' },
74+
});
75+
expect(response).toBeDefined();
76+
});
77+
78+
it('should prevent duplicate registration', async () => {
79+
const duplicate = new ExplorerAgent();
80+
await expect(coordinator.registerAgent(duplicate)).rejects.toThrow('already registered');
81+
});
82+
});
83+
84+
describe('Message Routing', () => {
85+
it('should route pattern search request to Explorer', async () => {
86+
const response = await coordinator.sendMessage({
87+
type: 'request',
88+
sender: 'test',
89+
recipient: 'explorer',
90+
payload: {
91+
action: 'pattern',
92+
query: 'RepositoryIndexer',
93+
limit: 5,
94+
threshold: 0.7,
95+
},
96+
});
97+
98+
expect(response).toBeDefined();
99+
expect(response?.type).toBe('response');
100+
expect(response?.sender).toBe('explorer');
101+
102+
const result = response?.payload as { action: string; results?: unknown[] };
103+
expect(result.action).toBe('pattern');
104+
// Results array exists (may be empty without indexed data)
105+
expect(Array.isArray(result.results)).toBe(true);
106+
});
107+
108+
it('should route similar code request to Explorer', async () => {
109+
const response = await coordinator.sendMessage({
110+
type: 'request',
111+
sender: 'test',
112+
recipient: 'explorer',
113+
payload: {
114+
action: 'similar',
115+
content: 'export class RepositoryIndexer { constructor() { } }',
116+
limit: 3,
117+
threshold: 0.5,
118+
},
119+
});
120+
121+
expect(response).toBeDefined();
122+
// May be error or response depending on indexer state
123+
expect(['response', 'error']).toContain(response?.type);
124+
125+
if (response?.type === 'response') {
126+
const result = response.payload as { action: string; results?: unknown[] };
127+
expect(result.action).toBe('similar');
128+
expect(Array.isArray(result.results)).toBe(true);
129+
}
130+
});
131+
132+
it('should route relationships request to Explorer', async () => {
133+
const response = await coordinator.sendMessage({
134+
type: 'request',
135+
sender: 'test',
136+
recipient: 'explorer',
137+
payload: {
138+
action: 'relationships',
139+
component: 'RepositoryIndexer',
140+
depth: 1,
141+
},
142+
});
143+
144+
expect(response).toBeDefined();
145+
expect(response?.type).toBe('response');
146+
147+
const result = response?.payload as { action: string; relationships?: unknown[] };
148+
expect(result.action).toBe('relationships');
149+
expect(Array.isArray(result.relationships)).toBe(true);
150+
});
151+
152+
it('should route insights request to Explorer', async () => {
153+
const response = await coordinator.sendMessage({
154+
type: 'request',
155+
sender: 'test',
156+
recipient: 'explorer',
157+
payload: {
158+
action: 'insights',
159+
scope: 'repository',
160+
includePatterns: true,
161+
},
162+
});
163+
164+
expect(response).toBeDefined();
165+
expect(response?.type).toBe('response');
166+
167+
const result = response?.payload as { action: string; insights?: unknown };
168+
expect(result.action).toBe('insights');
169+
expect(result.insights).toBeDefined();
170+
});
171+
172+
it('should handle unknown actions gracefully', async () => {
173+
const response = await coordinator.sendMessage({
174+
type: 'request',
175+
sender: 'test',
176+
recipient: 'explorer',
177+
payload: {
178+
action: 'unknown-action',
179+
},
180+
});
181+
182+
expect(response).toBeDefined();
183+
expect(response?.type).toBe('response');
184+
185+
const result = response?.payload as { error?: string };
186+
expect(result.error).toBeDefined();
187+
expect(result.error).toContain('Unknown action');
188+
});
189+
190+
it('should handle non-existent agent gracefully', async () => {
191+
const response = await coordinator.sendMessage({
192+
type: 'request',
193+
sender: 'test',
194+
recipient: 'non-existent-agent',
195+
payload: {},
196+
});
197+
198+
expect(response).toBeDefined();
199+
expect(response?.type).toBe('error');
200+
201+
const error = response?.payload as { error: string };
202+
expect(error.error).toContain('not found');
203+
});
204+
});
205+
206+
describe('Task Execution', () => {
207+
it('should execute pattern search task via task queue', async () => {
208+
const taskId = coordinator.submitTask({
209+
type: 'pattern-search',
210+
agentName: 'explorer',
211+
payload: {
212+
action: 'pattern',
213+
query: 'SubagentCoordinator',
214+
limit: 5,
215+
},
216+
priority: 10,
217+
});
218+
219+
expect(taskId).toBeDefined();
220+
221+
// Wait for task to complete
222+
await new Promise((resolve) => setTimeout(resolve, 100));
223+
224+
const task = coordinator.getTask(taskId);
225+
expect(task).toBeDefined();
226+
expect(task?.status).toBe('completed');
227+
});
228+
229+
it('should track task statistics', async () => {
230+
coordinator.submitTask({
231+
type: 'insights',
232+
agentName: 'explorer',
233+
payload: {
234+
action: 'insights',
235+
scope: 'repository',
236+
},
237+
});
238+
239+
await new Promise((resolve) => setTimeout(resolve, 100));
240+
241+
const stats = coordinator.getStats();
242+
expect(stats.tasksCompleted).toBeGreaterThan(0);
243+
});
244+
});
245+
246+
describe('Health Checks', () => {
247+
it('should report Explorer health status based on indexed data', async () => {
248+
// Without indexed data, health check returns false
249+
const isHealthy = await explorer.healthCheck();
250+
expect(isHealthy).toBe(false);
251+
252+
// Note: In production with indexed data, this would return true
253+
});
254+
255+
it('should track message statistics', async () => {
256+
await coordinator.sendMessage({
257+
type: 'request',
258+
sender: 'test',
259+
recipient: 'explorer',
260+
payload: {
261+
action: 'pattern',
262+
query: 'test',
263+
},
264+
});
265+
266+
const stats = coordinator.getStats();
267+
expect(stats.messagesSent).toBeGreaterThan(0);
268+
expect(stats.messagesReceived).toBeGreaterThan(0);
269+
});
270+
});
271+
272+
describe('Context Management', () => {
273+
it('should share indexer context between Coordinator and Explorer', async () => {
274+
const contextIndexer = coordinator.getContextManager().getIndexer();
275+
expect(contextIndexer).toBeDefined();
276+
expect(contextIndexer).toBe(indexer);
277+
});
278+
279+
it('should allow Explorer to access shared context', async () => {
280+
const response = await coordinator.sendMessage({
281+
type: 'request',
282+
sender: 'test',
283+
recipient: 'explorer',
284+
payload: {
285+
action: 'pattern',
286+
query: 'test',
287+
},
288+
});
289+
290+
// Should succeed because indexer is in shared context
291+
expect(response?.type).toBe('response');
292+
});
293+
});
294+
295+
describe('Shutdown', () => {
296+
it('should gracefully unregister Explorer', async () => {
297+
await coordinator.unregisterAgent('explorer');
298+
299+
const agents = coordinator.getAgents();
300+
expect(agents).not.toContain('explorer');
301+
});
302+
303+
it('should stop coordinator and all agents', async () => {
304+
await coordinator.stop();
305+
306+
const agents = coordinator.getAgents();
307+
expect(agents).toHaveLength(0);
308+
});
309+
});
310+
});

0 commit comments

Comments
 (0)