@zhin.js/ai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,484 @@
1
+ /**
2
+ * Agent 集成测试
3
+ *
4
+ * 测试 AI Agent 的完整流程,包括:
5
+ * - 工具调用流程
6
+ * - 对话历史管理
7
+ * - 错误处理
8
+ * - 重复调用检测
9
+ */
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
11
+ import { createMockTool } from './setup.js';
12
+
13
+ // Mock Logger
14
+ vi.mock('@zhin.js/core', async (importOriginal) => {
15
+ const original = await importOriginal() as any;
16
+ return {
17
+ ...original,
18
+ Logger: class {
19
+ debug = vi.fn();
20
+ info = vi.fn();
21
+ warn = vi.fn();
22
+ error = vi.fn();
23
+ },
24
+ };
25
+ });
26
+
27
+ import { createAgent, Agent } from '../src/agent.js';
28
+
29
+ // 创建完整的 mock provider
30
+ const createMockProvider = () => ({
31
+ name: 'mock',
32
+ models: ['mock-model'],
33
+ chat: vi.fn(),
34
+ healthCheck: vi.fn().mockResolvedValue(true),
35
+ });
36
+
37
+ // 创建标准 chat 响应
38
+ const createChatResponse = (content: string, toolCalls?: any[]) => ({
39
+ id: 'test-id',
40
+ object: 'chat.completion',
41
+ created: Date.now(),
42
+ model: 'mock-model',
43
+ choices: [{
44
+ index: 0,
45
+ message: {
46
+ role: 'assistant',
47
+ content,
48
+ tool_calls: toolCalls,
49
+ },
50
+ finish_reason: toolCalls ? 'tool_calls' : 'stop',
51
+ }],
52
+ usage: {
53
+ prompt_tokens: 10,
54
+ completion_tokens: 10,
55
+ total_tokens: 20,
56
+ },
57
+ });
58
+
59
+ describe('Agent 完整流程测试', () => {
60
+ let mockProvider: ReturnType<typeof createMockProvider>;
61
+
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ mockProvider = createMockProvider();
65
+ });
66
+
67
+ describe('基本对话', () => {
68
+ it('应该处理简单的文本响应', async () => {
69
+ mockProvider.chat.mockResolvedValue(createChatResponse('你好!'));
70
+
71
+ const agent = createAgent(mockProvider as any, {
72
+ systemPrompt: '你是一个助手',
73
+ tools: [],
74
+ });
75
+
76
+ const result = await agent.run('你好');
77
+
78
+ expect(result.content).toBe('你好!');
79
+ expect(result.iterations).toBe(1);
80
+ });
81
+
82
+ it('应该正确统计 token 用量', async () => {
83
+ mockProvider.chat.mockResolvedValue(createChatResponse('回复'));
84
+
85
+ const agent = createAgent(mockProvider as any, {
86
+ tools: [],
87
+ });
88
+
89
+ const result = await agent.run('测试');
90
+
91
+ expect(result.usage.total_tokens).toBe(20);
92
+ });
93
+ });
94
+
95
+ describe('工具调用', () => {
96
+ it('应该正确执行单个工具调用', async () => {
97
+ const calculatorTool = createMockTool({
98
+ name: 'calculator',
99
+ description: '计算器',
100
+ parameters: {
101
+ expression: { type: 'string', description: '表达式' },
102
+ },
103
+ required: ['expression'],
104
+ executeResult: JSON.stringify({ result: 42 }),
105
+ });
106
+
107
+ // 第一次调用返回工具调用
108
+ mockProvider.chat
109
+ .mockResolvedValueOnce(createChatResponse('', [{
110
+ id: 'call-1',
111
+ type: 'function',
112
+ function: {
113
+ name: 'calculator',
114
+ arguments: JSON.stringify({ expression: '6 * 7' }),
115
+ },
116
+ }]))
117
+ .mockResolvedValueOnce(createChatResponse('计算结果是 42'));
118
+
119
+ const agent = createAgent(mockProvider as any, {
120
+ tools: [calculatorTool],
121
+ });
122
+
123
+ const result = await agent.run('计算 6 * 7');
124
+
125
+ // 工具应该被调用
126
+ expect(calculatorTool.execute).toHaveBeenCalledWith(
127
+ { expression: '6 * 7' }
128
+ );
129
+
130
+ // 应该返回最终结果
131
+ expect(result.content).toBe('计算结果是 42');
132
+ expect(result.toolCalls).toHaveLength(1);
133
+ expect(result.toolCalls[0].tool).toBe('calculator');
134
+ });
135
+
136
+ it('应该处理多个工具调用', async () => {
137
+ const tool1 = createMockTool({
138
+ name: 'tool1',
139
+ executeResult: 'result1',
140
+ });
141
+
142
+ const tool2 = createMockTool({
143
+ name: 'tool2',
144
+ executeResult: 'result2',
145
+ });
146
+
147
+ mockProvider.chat
148
+ .mockResolvedValueOnce(createChatResponse('', [
149
+ {
150
+ id: 'call-1',
151
+ type: 'function',
152
+ function: { name: 'tool1', arguments: '{}' },
153
+ },
154
+ {
155
+ id: 'call-2',
156
+ type: 'function',
157
+ function: { name: 'tool2', arguments: '{}' },
158
+ },
159
+ ]))
160
+ .mockResolvedValueOnce(createChatResponse('完成'));
161
+
162
+ const agent = createAgent(mockProvider as any, {
163
+ tools: [tool1, tool2],
164
+ });
165
+
166
+ const result = await agent.run('执行两个工具');
167
+
168
+ expect(tool1.execute).toHaveBeenCalled();
169
+ expect(tool2.execute).toHaveBeenCalled();
170
+ expect(result.toolCalls).toHaveLength(2);
171
+ });
172
+
173
+ it('应该检测并阻止重复工具调用', async () => {
174
+ const tool = createMockTool({
175
+ name: 'repeatable',
176
+ parameters: { query: { type: 'string' } },
177
+ executeResult: 'result',
178
+ });
179
+
180
+ // 模拟重复调用同一工具
181
+ mockProvider.chat
182
+ .mockResolvedValueOnce(createChatResponse('', [{
183
+ id: 'call-1',
184
+ type: 'function',
185
+ function: { name: 'repeatable', arguments: JSON.stringify({ query: 'same' }) },
186
+ }]))
187
+ .mockResolvedValueOnce(createChatResponse('', [{
188
+ id: 'call-2',
189
+ type: 'function',
190
+ function: { name: 'repeatable', arguments: JSON.stringify({ query: 'same' }) },
191
+ }]))
192
+ .mockResolvedValueOnce(createChatResponse('完成'));
193
+
194
+ const agent = createAgent(mockProvider as any, {
195
+ tools: [tool],
196
+ });
197
+
198
+ const result = await agent.run('测试重复');
199
+
200
+ // 工具应该只被实际执行一次(第一次)
201
+ expect(tool.execute).toHaveBeenCalledTimes(1);
202
+ });
203
+ });
204
+
205
+ describe('工具执行错误处理', () => {
206
+ it('应该处理工具执行错误', async () => {
207
+ const errorTool = createMockTool({
208
+ name: 'error_tool',
209
+ executeError: new Error('工具执行失败'),
210
+ });
211
+
212
+ mockProvider.chat
213
+ .mockResolvedValueOnce(createChatResponse('', [{
214
+ id: 'call-1',
215
+ type: 'function',
216
+ function: { name: 'error_tool', arguments: '{}' },
217
+ }]))
218
+ .mockResolvedValueOnce(createChatResponse('工具出错了'));
219
+
220
+ const agent = createAgent(mockProvider as any, {
221
+ tools: [errorTool],
222
+ });
223
+
224
+ const result = await agent.run('执行会出错的工具');
225
+
226
+ // 应该继续执行,不会崩溃
227
+ expect(result.content).toBeDefined();
228
+ });
229
+
230
+ it('应该处理不存在的工具调用', async () => {
231
+ mockProvider.chat
232
+ .mockResolvedValueOnce(createChatResponse('', [{
233
+ id: 'call-1',
234
+ type: 'function',
235
+ function: { name: 'nonexistent', arguments: '{}' },
236
+ }]))
237
+ .mockResolvedValueOnce(createChatResponse('工具不存在'));
238
+
239
+ const agent = createAgent(mockProvider as any, {
240
+ tools: [],
241
+ });
242
+
243
+ const result = await agent.run('调用不存在的工具');
244
+
245
+ // 应该继续执行,包含错误信息
246
+ expect(result).toBeDefined();
247
+ });
248
+ });
249
+
250
+ describe('事件监听', () => {
251
+ it('应该触发 tool_call 事件', async () => {
252
+ const tool = createMockTool({
253
+ name: 'event_tool',
254
+ executeResult: 'done',
255
+ });
256
+
257
+ mockProvider.chat
258
+ .mockResolvedValueOnce(createChatResponse('', [{
259
+ id: 'call-1',
260
+ type: 'function',
261
+ function: { name: 'event_tool', arguments: JSON.stringify({ test: true }) },
262
+ }]))
263
+ .mockResolvedValueOnce(createChatResponse('完成'));
264
+
265
+ const agent = createAgent(mockProvider as any, {
266
+ tools: [tool],
267
+ });
268
+
269
+ const toolCallHandler = vi.fn();
270
+ agent.on('tool_call', toolCallHandler);
271
+
272
+ await agent.run('测试事件');
273
+
274
+ expect(toolCallHandler).toHaveBeenCalledWith('event_tool', { test: true });
275
+ });
276
+
277
+ it('应该触发 tool_result 事件', async () => {
278
+ const tool = createMockTool({
279
+ name: 'result_tool',
280
+ executeResult: JSON.stringify({ data: 'test' }),
281
+ });
282
+
283
+ mockProvider.chat
284
+ .mockResolvedValueOnce(createChatResponse('', [{
285
+ id: 'call-1',
286
+ type: 'function',
287
+ function: { name: 'result_tool', arguments: '{}' },
288
+ }]))
289
+ .mockResolvedValueOnce(createChatResponse('完成'));
290
+
291
+ const agent = createAgent(mockProvider as any, {
292
+ tools: [tool],
293
+ });
294
+
295
+ const toolResultHandler = vi.fn();
296
+ agent.on('tool_result', toolResultHandler);
297
+
298
+ await agent.run('测试结果事件');
299
+
300
+ // Result is the raw string from tool execution
301
+ expect(toolResultHandler).toHaveBeenCalledWith(
302
+ 'result_tool',
303
+ expect.stringContaining('data')
304
+ );
305
+ });
306
+
307
+ it('应该触发 complete 事件', async () => {
308
+ mockProvider.chat.mockResolvedValue(createChatResponse('完成'));
309
+
310
+ const agent = createAgent(mockProvider as any, {
311
+ tools: [],
312
+ });
313
+
314
+ const completeHandler = vi.fn();
315
+ agent.on('complete', completeHandler);
316
+
317
+ await agent.run('测试');
318
+
319
+ expect(completeHandler).toHaveBeenCalledWith(
320
+ expect.objectContaining({
321
+ content: '完成',
322
+ })
323
+ );
324
+ });
325
+
326
+ it('应该返回取消订阅函数', async () => {
327
+ mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
328
+
329
+ const agent = createAgent(mockProvider as any, {
330
+ tools: [],
331
+ });
332
+
333
+ const handler = vi.fn();
334
+ const unsubscribe = agent.on('complete', handler);
335
+
336
+ // 取消订阅
337
+ unsubscribe();
338
+
339
+ await agent.run('测试');
340
+
341
+ // handler 不应该被调用
342
+ expect(handler).not.toHaveBeenCalled();
343
+ });
344
+ });
345
+ });
346
+
347
+ describe('Agent 配置测试', () => {
348
+ it('应该使用自定义系统提示', async () => {
349
+ const mockProvider = createMockProvider();
350
+ mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
351
+
352
+ const agent = createAgent(mockProvider as any, {
353
+ systemPrompt: '你是一个代码助手',
354
+ tools: [],
355
+ });
356
+
357
+ await agent.run('你好');
358
+
359
+ // 检查传递给 provider 的消息包含系统提示
360
+ expect(mockProvider.chat).toHaveBeenCalledWith(
361
+ expect.objectContaining({
362
+ messages: expect.arrayContaining([
363
+ expect.objectContaining({
364
+ role: 'system',
365
+ content: expect.stringContaining('代码助手'),
366
+ }),
367
+ ]),
368
+ })
369
+ );
370
+ });
371
+
372
+ it('应该传递工具定义给 provider', async () => {
373
+ const mockProvider = createMockProvider();
374
+ mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
375
+
376
+ const tool = createMockTool({
377
+ name: 'test_tool',
378
+ description: '测试工具',
379
+ });
380
+
381
+ const agent = createAgent(mockProvider as any, {
382
+ tools: [tool],
383
+ });
384
+
385
+ await agent.run('测试');
386
+
387
+ expect(mockProvider.chat).toHaveBeenCalledWith(
388
+ expect.objectContaining({
389
+ tools: expect.arrayContaining([
390
+ expect.objectContaining({
391
+ type: 'function',
392
+ function: expect.objectContaining({
393
+ name: 'test_tool',
394
+ }),
395
+ }),
396
+ ]),
397
+ })
398
+ );
399
+ });
400
+
401
+ it('应该限制最大迭代次数', async () => {
402
+ const mockProvider = createMockProvider();
403
+
404
+ // 模拟一直返回工具调用
405
+ mockProvider.chat.mockResolvedValue(createChatResponse('', [{
406
+ id: 'call-1',
407
+ type: 'function',
408
+ function: { name: 'infinite', arguments: '{}' },
409
+ }]));
410
+
411
+ const tool = createMockTool({
412
+ name: 'infinite',
413
+ executeResult: 'loop',
414
+ });
415
+
416
+ const agent = createAgent(mockProvider as any, {
417
+ tools: [tool],
418
+ maxIterations: 3,
419
+ });
420
+
421
+ const result = await agent.run('无限循环');
422
+
423
+ // 应该在达到最大迭代次数后停止
424
+ expect(result.iterations).toBeLessThanOrEqual(3);
425
+ });
426
+ });
427
+
428
+ describe('Agent 类', () => {
429
+ it('应该正确创建实例', () => {
430
+ const mockProvider = createMockProvider();
431
+ const agent = new Agent(mockProvider as any, {
432
+ tools: [],
433
+ });
434
+
435
+ expect(agent).toBeInstanceOf(Agent);
436
+ });
437
+
438
+ it('应该支持动态添加工具', async () => {
439
+ const mockProvider = createMockProvider();
440
+ mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
441
+
442
+ const agent = new Agent(mockProvider as any, {
443
+ tools: [],
444
+ });
445
+
446
+ const tool = createMockTool({ name: 'dynamic_tool' });
447
+ agent.addTool(tool);
448
+
449
+ await agent.run('测试');
450
+
451
+ expect(mockProvider.chat).toHaveBeenCalledWith(
452
+ expect.objectContaining({
453
+ tools: expect.arrayContaining([
454
+ expect.objectContaining({
455
+ function: expect.objectContaining({
456
+ name: 'dynamic_tool',
457
+ }),
458
+ }),
459
+ ]),
460
+ })
461
+ );
462
+ });
463
+
464
+ it('应该支持动态移除工具', async () => {
465
+ const mockProvider = createMockProvider();
466
+ mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
467
+
468
+ const tool = createMockTool({ name: 'removable' });
469
+ const agent = new Agent(mockProvider as any, {
470
+ tools: [tool],
471
+ });
472
+
473
+ agent.removeTool('removable');
474
+
475
+ await agent.run('测试');
476
+
477
+ // 应该没有工具
478
+ expect(mockProvider.chat).toHaveBeenCalledWith(
479
+ expect.objectContaining({
480
+ tools: undefined,
481
+ })
482
+ );
483
+ });
484
+ });