master-session.test.js
18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
/**
* Master-Innovator 集成测试
*
* 验证 Master Session 的完整工具调用链路:
* 1. Master 收到用户消息 → 解析为 orchestrate 工具调用
* 2. orchestrate 返回编排计划
* 3. Master 按计划调用 call_sub_agent
* 4. 最终汇总结果回复用户
*
* 通过 mock AgentManager.callAgent 模拟 Master 的 Claude CLI 输出。
*/
const { MasterSession } = require('../../core/master-session');
const { ToolRegistry } = require('../../tools/tool-registry');
const { createOrchestrateTool } = require('../../tools/orchestrate-tool');
const { createCallAgentTool, createListAgentsTool } = require('../../tools/call-agent-tool');
// Mock logging
vi.mock('../../core/logging', () => ({
getGlobalLogManager: () => ({
initialized: false,
getOrCreateSession: () => null,
}),
waitForGlobalLogManager: () => Promise.resolve(),
}));
// ─── 测试配置 ───
const testConfig = {
agents: {
master: { type: 'master-cli', client: 'claude-local', description: 'Master Agent' },
coder: { type: 'executor', client: 'claude-local', description: '编写代码' },
writer: { type: 'executor', client: 'claude-local', description: '创意写作' },
verifier: { type: 'validator', client: 'claude-local', description: '代码验证' },
'frontend-tester': { type: 'validator', client: 'claude-local', description: '前端测试' },
},
agentGroups: {
development: {
name: '软件开发',
description: '编写和验证代码',
default: true,
agents: ['coder', 'verifier', 'frontend-tester'],
},
writing: {
name: '创意写作',
description: '写作和编辑',
agents: ['writer'],
},
},
clients: {
'claude-local': { command: 'claude', args: [] },
},
};
// ─── Helper ───
function createMockSession(callResponses) {
let callIndex = 0;
const session = new MasterSession(testConfig, '/tmp/test-master');
// 直接 mock init 跳过真实 AgentManager 创建
session._initialized = true;
// 手动注册工具
session.toolRegistry = new ToolRegistry();
session.toolRegistry.registerAll([
createOrchestrateTool(),
createCallAgentTool(),
createListAgentsTool(),
]);
// Mock AgentManager
const mockCallAgent = vi.fn().mockImplementation(async (agentName, prompt) => {
if (agentName === 'master') {
// Master 的模拟回复 — 按序返回预设响应
const response = callResponses[callIndex++] || { output: '任务完成!' };
return response;
}
// Sub Agent 的模拟回复
return { output: `[${agentName}] 执行完成: ${prompt.substring(0, 50)}...` };
});
session.manager = {
callAgent: mockCallAgent,
closeAllSessions: vi.fn(),
getAgentConfig: vi.fn().mockImplementation((name) => {
const cfg = testConfig.agents[name];
if (!cfg) throw new Error(`Agent "${name}" not found`);
return cfg;
}),
listAgents: vi.fn().mockReturnValue(
Object.entries(testConfig.agents).map(([name, cfg]) => ({ name, ...cfg }))
),
};
return { session, mockCallAgent };
}
// ─── 测试 ───
describe('Master Session — 工具调用集成测试', () => {
afterEach(() => {
vi.restoreAllMocks();
});
// ── 1. 直接回复(无工具调用) ──
describe('直接回复', () => {
it('should reply directly for simple questions without tool calls', async () => {
const { session } = createMockSession([
{ output: 'Docker 是一个容器化平台,用于打包和运行应用。' },
]);
const result = await session.handleMessage('Docker 是什么?');
expect(result.type).toBe('reply');
expect(result.output).toContain('Docker');
expect(result.toolCalls).toHaveLength(0);
expect(result.rounds).toBe(1);
});
});
// ── 2. 单 Agent 调用(call_sub_agent) ──
describe('单 Agent 调用', () => {
it('should call sub agent directly and return result', async () => {
const { session, mockCallAgent } = createMockSession([
// Round 1: Master 调用 call_sub_agent
{
output: `好的,我来帮你写一首诗。
[TOOL_CALL: call_sub_agent]
{
"agent": "writer",
"task": "写一首关于秋天的五言绝句"
}
[/TOOL_CALL]`,
},
// Round 2: Master 收到结果后汇总回复
{
output: '这是 writer 创作的秋天诗:\n\n秋风起天末,万叶落纷纷。\n孤雁向南去,寒山映夕云。',
},
]);
const result = await session.handleMessage('帮我写一首关于秋天的诗');
expect(result.type).toBe('reply');
expect(result.rounds).toBe(2);
expect(result.toolCalls).toHaveLength(1);
expect(result.toolCalls[0].tool).toBe('call_sub_agent');
expect(result.toolCalls[0].params.agent).toBe('writer');
// 验证 Master 被调了 2 次(初始 + 工具结果反馈)
expect(mockCallAgent).toHaveBeenCalledTimes(3); // 2 master + 1 writer
// 验证 writer 确实被调用了
const writerCall = mockCallAgent.mock.calls.find(c => c[0] === 'writer');
expect(writerCall).toBeTruthy();
expect(writerCall[1]).toContain('秋天');
});
});
// ── 3. orchestrate + 多步骤 call_sub_agent ──
describe('多 Agent 编排', () => {
it('should orchestrate plan then call sub agents step by step', async () => {
const { session, mockCallAgent } = createMockSession([
// Round 1: Master 调用 orchestrate 获取编排计划
{
output: `这是一个需要多 Agent 协作的任务,让我先获取编排计划。
[TOOL_CALL: orchestrate]
{
"task": "开发一个 Python 命令行计算器",
"agentGroup": "development"
}
[/TOOL_CALL]`,
},
// Round 2: Master 收到 plan,调用 coder
{
output: `收到编排计划。按步骤执行,先调用 coder。
[TOOL_CALL: call_sub_agent]
{
"agent": "coder",
"task": "开发一个 Python 命令行计算器,支持加减乘除,处理除零和非法输入"
}
[/TOOL_CALL]`,
},
// Round 3: coder 完成,调用 verifier
{
output: `coder 已完成代码编写。现在调用 verifier 验证。
[TOOL_CALL: call_sub_agent]
{
"agent": "verifier",
"task": "验证 Python 计算器:运行测试,检查除零处理和错误输入处理"
}
[/TOOL_CALL]`,
},
// Round 4: 全部通过,汇总回复
{
output: `✅ 任务完成!
**计算器已开发并验证通过:**
- coder 创建了 calc.py,支持加减乘除
- verifier 验证通过,除零和非法输入都有正确处理
所有步骤已完成。`,
},
]);
const toolCalls = [];
const result = await session.handleMessage('开发一个 Python 命令行计算器', {
onToolCall: (tc) => toolCalls.push(tc),
});
// 验证最终结果
expect(result.type).toBe('reply');
expect(result.rounds).toBe(4);
expect(result.output).toContain('任务完成');
// 验证工具调用历史
expect(result.toolCalls).toHaveLength(3); // orchestrate + coder + verifier
expect(result.toolCalls[0].tool).toBe('orchestrate');
expect(result.toolCalls[1].tool).toBe('call_sub_agent');
expect(result.toolCalls[1].params.agent).toBe('coder');
expect(result.toolCalls[2].tool).toBe('call_sub_agent');
expect(result.toolCalls[2].params.agent).toBe('verifier');
// 验证 orchestrate 返回了有效 plan
const orchResult = result.toolCalls[0].result;
expect(orchResult.status).toBe('success');
expect(orchResult.result.status).toBe('success');
expect(orchResult.result.plan.agentGroup).toBe('development');
expect(orchResult.result.plan.steps.length).toBeGreaterThan(0);
// 验证 onToolCall 回调被触发
expect(toolCalls).toHaveLength(3);
// 验证 Sub Agent 被实际调用
const agentCalls = mockCallAgent.mock.calls.filter(c => c[0] !== 'master');
expect(agentCalls.map(c => c[0])).toEqual(['coder', 'verifier']);
});
});
// ── 4. 修复循环 ──
describe('修复循环', () => {
it('should handle validator failure → executor fix → re-validate', async () => {
const { session, mockCallAgent } = createMockSession([
// Round 1: 直接调 coder
{
output: `[TOOL_CALL: call_sub_agent]
{
"agent": "coder",
"task": "写一个 sort 函数"
}
[/TOOL_CALL]`,
},
// Round 2: 调 verifier
{
output: `[TOOL_CALL: call_sub_agent]
{
"agent": "verifier",
"task": "验证 sort 函数"
}
[/TOOL_CALL]`,
},
// Round 3: verifier "失败"(通过模拟 Sub Agent 返回失败结果),Master 让 coder 修复
{
output: `verifier 报告了问题,让 coder 修复。
[TOOL_CALL: call_sub_agent]
{
"agent": "coder",
"task": "修复 sort 函数:空数组未处理"
}
[/TOOL_CALL]`,
},
// Round 4: 再次验证
{
output: `[TOOL_CALL: call_sub_agent]
{
"agent": "verifier",
"task": "重新验证 sort 函数"
}
[/TOOL_CALL]`,
},
// Round 5: 完成
{
output: '✅ sort 函数已修复并验证通过。',
},
]);
const result = await session.handleMessage('写一个排序函数');
expect(result.type).toBe('reply');
expect(result.rounds).toBe(5);
// 验证工具调用:coder → verifier → coder(修复) → verifier(重验)
const agents = result.toolCalls.map(tc => tc.params.agent);
expect(agents).toEqual(['coder', 'verifier', 'coder', 'verifier']);
});
});
// ── 5. list_agents 工具 ──
describe('list_agents 工具', () => {
it('should return available agents when Master calls list_agents', async () => {
const { session } = createMockSession([
{
output: `[TOOL_CALL: list_agents]
{}
[/TOOL_CALL]`,
},
{
output: '目前可用的 Agent 有:coder(编写代码)、writer(创意写作)、verifier(代码验证)。',
},
]);
const result = await session.handleMessage('有哪些 Agent 可用?');
expect(result.type).toBe('reply');
expect(result.toolCalls).toHaveLength(1);
expect(result.toolCalls[0].tool).toBe('list_agents');
const listResult = result.toolCalls[0].result.result;
expect(listResult.agents.length).toBeGreaterThan(0);
expect(listResult.agents.find(a => a.name === 'coder')).toBeTruthy();
expect(listResult.groups.length).toBeGreaterThan(0);
});
});
// ── 6. 未知工具处理 ──
describe('错误处理', () => {
it('should handle unknown tool gracefully', async () => {
const { session } = createMockSession([
{
output: `[TOOL_CALL: nonexistent_tool]
{"task": "test"}
[/TOOL_CALL]`,
},
{
output: '工具不存在,我直接回答你的问题。',
},
]);
const result = await session.handleMessage('测试');
expect(result.type).toBe('reply');
expect(result.toolCalls).toHaveLength(1);
expect(result.toolCalls[0].result.status).toBe('error');
expect(result.toolCalls[0].result.error).toContain('Unknown tool');
});
it('should handle nonexistent agent in call_sub_agent', async () => {
const { session } = createMockSession([
{
output: `[TOOL_CALL: call_sub_agent]
{
"agent": "nonexistent_agent",
"task": "test"
}
[/TOOL_CALL]`,
},
{
output: '该 Agent 不存在。',
},
]);
const result = await session.handleMessage('调用不存在的 agent');
expect(result.type).toBe('reply');
expect(result.toolCalls).toHaveLength(1);
expect(result.toolCalls[0].result.status).toBe('error');
expect(result.toolCalls[0].result.error).toContain('不存在');
});
it('should stop at max tool rounds', async () => {
// 无限循环调用工具
const infiniteResponses = Array(15).fill({
output: `[TOOL_CALL: list_agents]
{}
[/TOOL_CALL]`,
});
const { session } = createMockSession(infiniteResponses);
const result = await session.handleMessage('无限循环', {
maxToolRounds: 3,
});
expect(result.type).toBe('max_rounds');
expect(result.rounds).toBe(3);
});
});
// ── 7. ToolRegistry 单元测试 ──
describe('ToolRegistry', () => {
it('should parse multiple tool calls from output', () => {
const output = `
先做第一步:
[TOOL_CALL: orchestrate]
{"task": "test task"}
[/TOOL_CALL]
还有第二步:
[TOOL_CALL: call_sub_agent]
{"agent": "coder", "task": "write code"}
[/TOOL_CALL]
`;
const calls = ToolRegistry.parseToolCalls(output);
expect(calls).toHaveLength(2);
expect(calls[0].tool).toBe('orchestrate');
expect(calls[0].params.task).toBe('test task');
expect(calls[1].tool).toBe('call_sub_agent');
expect(calls[1].params.agent).toBe('coder');
});
it('should handle non-JSON params as raw task', () => {
const output = `[TOOL_CALL: call_sub_agent]
just a plain text task
[/TOOL_CALL]`;
const calls = ToolRegistry.parseToolCalls(output);
expect(calls).toHaveLength(1);
expect(calls[0].params.task).toBe('just a plain text task');
});
it('should return empty array for no tool calls', () => {
const calls = ToolRegistry.parseToolCalls('Just a normal reply with no tools.');
expect(calls).toHaveLength(0);
});
it('should generate tool prompt with all registered tools', () => {
const registry = new ToolRegistry();
registry.registerAll([
createOrchestrateTool(),
createCallAgentTool(),
createListAgentsTool(),
]);
const prompt = registry.generateToolPrompt();
expect(prompt).toContain('orchestrate');
expect(prompt).toContain('call_sub_agent');
expect(prompt).toContain('list_agents');
expect(prompt).toContain('[TOOL_CALL:');
});
});
// ── 8. orchestrate planning 验证 ──
describe('orchestrate planning', () => {
it('should generate valid plan for development group', async () => {
const tool = createOrchestrateTool();
const result = await tool.execute(
{ task: '开发计算器', agentGroup: 'development' },
{ config: testConfig }
);
expect(result.status).toBe('success');
expect(result.plan.agentGroup).toBe('development');
expect(result.plan.steps.length).toBeGreaterThan(0);
// executor 步骤
const executorSteps = result.plan.steps.filter(s => s.type === 'executor');
expect(executorSteps.length).toBe(1); // coder
expect(executorSteps[0].agent).toBe('coder');
// validator 步骤
const validatorSteps = result.plan.steps.filter(s => s.type === 'validator');
expect(validatorSteps.length).toBe(2); // verifier + frontend-tester
// conditional 修复步骤
const conditionalSteps = result.plan.steps.filter(s => s.action === 'conditional');
expect(conditionalSteps.length).toBe(1);
});
it('should return error for nonexistent group', async () => {
const tool = createOrchestrateTool();
const result = await tool.execute(
{ task: 'test', agentGroup: 'nonexistent' },
{ config: testConfig }
);
expect(result.status).toBe('error');
expect(result.message).toContain('不存在');
});
it('should use default group when not specified', async () => {
const tool = createOrchestrateTool();
const result = await tool.execute(
{ task: '开发功能' },
{ config: testConfig }
);
expect(result.status).toBe('success');
expect(result.plan.agentGroup).toBe('development'); // default: true
});
});
// ── 9. 事件发射验证 ──
describe('事件发射', () => {
it('should emit round and tool_call events', async () => {
const { session } = createMockSession([
{
output: `[TOOL_CALL: list_agents]
{}
[/TOOL_CALL]`,
},
{ output: '完成' },
]);
const events = { round: [], tool_call: [], tool_result: [] };
session.on('round', (e) => events.round.push(e));
session.on('tool_call', (e) => events.tool_call.push(e));
session.on('tool_result', (e) => events.tool_result.push(e));
await session.handleMessage('test');
expect(events.round.length).toBe(2); // 2 rounds
expect(events.tool_call.length).toBe(1);
expect(events.tool_call[0].tool).toBe('list_agents');
expect(events.tool_result.length).toBe(1);
});
});
});