onbuzz 3.6.1 → 3.6.3
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.
- package/package.json +1 -1
- package/src/__test-utils__/fixtures/malformedJson.js +31 -0
- package/src/__test-utils__/globalSetup.js +9 -0
- package/src/__test-utils__/globalTeardown.js +12 -0
- package/src/__test-utils__/mockFactories.js +101 -0
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
- package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
- package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
- package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
- package/src/core/__tests__/agentPool.test.js +601 -0
- package/src/core/__tests__/agentScheduler.test.js +576 -0
- package/src/core/__tests__/contextManager.test.js +252 -0
- package/src/core/__tests__/flowExecutor.test.js +262 -0
- package/src/core/__tests__/messageProcessor.test.js +627 -0
- package/src/core/__tests__/orchestrator.test.js +257 -0
- package/src/core/__tests__/stateManager.test.js +375 -0
- package/src/core/agentPool.js +11 -1
- package/src/index.js +25 -9
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
- package/src/services/__tests__/agentActivityService.test.js +319 -0
- package/src/services/__tests__/apiKeyManager.test.js +206 -0
- package/src/services/__tests__/benchmarkService.test.js +184 -0
- package/src/services/__tests__/budgetService.test.js +211 -0
- package/src/services/__tests__/contextInjectionService.test.js +205 -0
- package/src/services/__tests__/conversationCompactionService.test.js +280 -0
- package/src/services/__tests__/credentialVault.test.js +469 -0
- package/src/services/__tests__/errorHandler.test.js +314 -0
- package/src/services/__tests__/fileAttachmentService.test.js +278 -0
- package/src/services/__tests__/flowContextService.test.js +199 -0
- package/src/services/__tests__/memoryService.test.js +450 -0
- package/src/services/__tests__/modelRouterService.test.js +388 -0
- package/src/services/__tests__/modelsService.test.js +261 -0
- package/src/services/__tests__/portRegistry.test.js +123 -0
- package/src/services/__tests__/projectDetector.test.js +34 -0
- package/src/services/__tests__/promptService.test.js +242 -0
- package/src/services/__tests__/qualityInspector.test.js +97 -0
- package/src/services/__tests__/scheduleService.test.js +308 -0
- package/src/services/__tests__/serviceRegistry.test.js +74 -0
- package/src/services/__tests__/skillsService.test.js +402 -0
- package/src/services/__tests__/tokenCountingService.test.js +48 -0
- package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
- package/src/tools/__tests__/agentDelayTool.test.js +342 -0
- package/src/tools/__tests__/asyncToolManager.test.js +344 -0
- package/src/tools/__tests__/baseTool.test.js +420 -0
- package/src/tools/__tests__/codeMapTool.test.js +348 -0
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
- package/src/tools/__tests__/fileSystemTool.test.js +717 -0
- package/src/tools/__tests__/fileTreeTool.test.js +274 -0
- package/src/tools/__tests__/helpTool.test.js +204 -0
- package/src/tools/__tests__/jobDoneTool.test.js +296 -0
- package/src/tools/__tests__/memoryTool.test.js +297 -0
- package/src/tools/__tests__/seekTool.test.js +282 -0
- package/src/tools/__tests__/skillsTool.test.js +226 -0
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
- package/src/tools/__tests__/taskManagerTool.test.js +725 -0
- package/src/tools/__tests__/terminalTool.test.js +384 -0
- package/src/tools/__tests__/userPromptTool.test.js +297 -0
- package/src/tools/__tests__/webTool.e2e.test.js +25 -11
- package/src/tools/webTool.js +6 -12
- package/src/types/__tests__/agent.test.js +499 -0
- package/src/types/__tests__/contextReference.test.js +606 -0
- package/src/types/__tests__/conversation.test.js +555 -0
- package/src/types/__tests__/toolCommand.test.js +584 -0
- package/src/types/contextReference.js +1 -1
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
- package/src/utilities/__tests__/configManager.test.js +397 -0
- package/src/utilities/__tests__/constants.test.js +49 -0
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
- package/src/utilities/__tests__/fileProcessor.test.js +104 -0
- package/src/utilities/__tests__/jsonRepair.test.js +104 -0
- package/src/utilities/__tests__/logger.test.js +129 -0
- package/src/utilities/__tests__/platformUtils.test.js +87 -0
- package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
- package/src/utilities/__tests__/tagParser.test.js +887 -0
- package/src/utilities/__tests__/toolConstants.test.js +94 -0
- package/src/utilities/tagParser.js +2 -2
- package/src/tools/browserTool.js +0 -897
- package/src/utilities/platformUtils.test.js +0 -98
- /package/src/tools/{filesystemTool.js → fileSystemTool.js} +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createMockLogger } from '../../__test-utils__/mockFactories.js';
|
|
3
|
+
|
|
4
|
+
// Mock TagParser before importing
|
|
5
|
+
jest.unstable_mockModule('../../utilities/tagParser.js', () => ({
|
|
6
|
+
default: {
|
|
7
|
+
extractContent: jest.fn((content, tag) => {
|
|
8
|
+
const regex = new RegExp(`<${tag}>(.*?)</${tag}>`, 'gs');
|
|
9
|
+
const matches = [];
|
|
10
|
+
let match;
|
|
11
|
+
while ((match = regex.exec(content)) !== null) {
|
|
12
|
+
matches.push(match[1]);
|
|
13
|
+
}
|
|
14
|
+
return matches;
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const { default: JobDoneTool } = await import('../jobDoneTool.js');
|
|
20
|
+
|
|
21
|
+
describe('JobDoneTool', () => {
|
|
22
|
+
let tool;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tool = new JobDoneTool();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('constructor', () => {
|
|
29
|
+
test('should set correct id and metadata', () => {
|
|
30
|
+
expect(tool.id).toBe('jobdone');
|
|
31
|
+
expect(tool.name).toBe('Job Done');
|
|
32
|
+
expect(tool.version).toBe('1.0.0');
|
|
33
|
+
expect(tool.requiresProject).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getDescription', () => {
|
|
38
|
+
test('should return description containing usage info', () => {
|
|
39
|
+
const desc = tool.getDescription();
|
|
40
|
+
expect(desc).toContain('Job Done Tool');
|
|
41
|
+
expect(desc).toContain('complete');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('getSchema', () => {
|
|
46
|
+
test('should return valid schema with actions property', () => {
|
|
47
|
+
const schema = tool.getSchema();
|
|
48
|
+
expect(schema.type).toBe('object');
|
|
49
|
+
expect(schema.properties.actions).toBeDefined();
|
|
50
|
+
expect(schema.required).toContain('actions');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('getCapabilities', () => {
|
|
55
|
+
test('should return capabilities with schema', () => {
|
|
56
|
+
const caps = tool.getCapabilities();
|
|
57
|
+
expect(caps.id).toBe('jobdone');
|
|
58
|
+
expect(caps.schema).toBeDefined();
|
|
59
|
+
expect(caps.enabled).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('parseParameters', () => {
|
|
64
|
+
test('should parse structured tags with summary', () => {
|
|
65
|
+
const result = tool.parseParameters('<summary>Task done</summary><success>true</success>');
|
|
66
|
+
expect(result.actions[0].summary).toBe('Task done');
|
|
67
|
+
expect(result.actions[0].success).toBe(true);
|
|
68
|
+
expect(result.actions[0].action).toBe('complete');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should parse with success=false', () => {
|
|
72
|
+
const result = tool.parseParameters('<summary>Failed</summary><success>false</success>');
|
|
73
|
+
expect(result.actions[0].success).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should parse details tag', () => {
|
|
77
|
+
const result = tool.parseParameters('<summary>Done</summary><details>Extra info</details>');
|
|
78
|
+
expect(result.actions[0].details).toBe('Extra info');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should fallback to raw content as summary', () => {
|
|
82
|
+
const result = tool.parseParameters('Simple completion message');
|
|
83
|
+
expect(result.actions[0].summary).toBe('Simple completion message');
|
|
84
|
+
expect(result.actions[0].success).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should default summary to "Task completed" for empty content', () => {
|
|
88
|
+
const result = tool.parseParameters('');
|
|
89
|
+
expect(result.actions[0].summary).toBe('Task completed');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('setAgentPool', () => {
|
|
94
|
+
test('should store the agent pool reference', () => {
|
|
95
|
+
const mockPool = { getAgent: jest.fn() };
|
|
96
|
+
tool.setAgentPool(mockPool);
|
|
97
|
+
expect(tool.agentPool).toBe(mockPool);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('setWebSocketManager', () => {
|
|
102
|
+
test('should store the websocket manager', () => {
|
|
103
|
+
const mockWs = { broadcastToSession: jest.fn() };
|
|
104
|
+
tool.setWebSocketManager(mockWs);
|
|
105
|
+
expect(tool.webSocketManager).toBe(mockWs);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('setFlowExecutor', () => {
|
|
110
|
+
test('should store the flow executor', () => {
|
|
111
|
+
const mockFlow = { notifyAgentCompletion: jest.fn() };
|
|
112
|
+
tool.setFlowExecutor(mockFlow);
|
|
113
|
+
expect(tool.flowExecutor).toBe(mockFlow);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('execute', () => {
|
|
118
|
+
test('should reject when actions array is missing', async () => {
|
|
119
|
+
const result = await tool.execute({}, {});
|
|
120
|
+
expect(result.success).toBe(false);
|
|
121
|
+
expect(result.error).toContain('Actions array is required');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should reject when actions array is empty', async () => {
|
|
125
|
+
const result = await tool.execute({ actions: [] }, {});
|
|
126
|
+
expect(result.success).toBe(false);
|
|
127
|
+
expect(result.error).toContain('Actions array is required');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should reject invalid action type', async () => {
|
|
131
|
+
const result = await tool.execute({
|
|
132
|
+
actions: [{ action: 'invalid', summary: 'test' }]
|
|
133
|
+
}, {});
|
|
134
|
+
expect(result.success).toBe(false);
|
|
135
|
+
expect(result.error).toContain('Invalid action');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should reject when summary is missing', async () => {
|
|
139
|
+
const result = await tool.execute({
|
|
140
|
+
actions: [{ action: 'complete' }]
|
|
141
|
+
}, {});
|
|
142
|
+
expect(result.success).toBe(false);
|
|
143
|
+
expect(result.error).toContain('summary is required');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should successfully complete with minimal params', async () => {
|
|
147
|
+
const result = await tool.execute({
|
|
148
|
+
actions: [{ action: 'complete', summary: 'All done' }]
|
|
149
|
+
}, {});
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
expect(result.taskComplete).toBe(true);
|
|
152
|
+
expect(result.exitAutonomousMode).toBe(true);
|
|
153
|
+
expect(result.summary).toBe('All done');
|
|
154
|
+
expect(result.successfulCompletion).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should complete with success=false', async () => {
|
|
158
|
+
const result = await tool.execute({
|
|
159
|
+
actions: [{ action: 'complete', summary: 'Stuck', success: false }]
|
|
160
|
+
}, {});
|
|
161
|
+
expect(result.success).toBe(true);
|
|
162
|
+
expect(result.successfulCompletion).toBe(false);
|
|
163
|
+
expect(result.output).toContain('with issues');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('should include details in output when provided', async () => {
|
|
167
|
+
const result = await tool.execute({
|
|
168
|
+
actions: [{ action: 'complete', summary: 'Done', details: 'Created 3 files' }]
|
|
169
|
+
}, {});
|
|
170
|
+
expect(result.details).toBe('Created 3 files');
|
|
171
|
+
expect(result.output).toContain('Created 3 files');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('should reject when agent has pending tasks and success=true', async () => {
|
|
175
|
+
const mockAgent = {
|
|
176
|
+
name: 'test-agent',
|
|
177
|
+
sessionId: 'sess-1',
|
|
178
|
+
taskList: {
|
|
179
|
+
tasks: [
|
|
180
|
+
{ status: 'pending', title: 'Unfinished task' },
|
|
181
|
+
{ status: 'in_progress', title: 'Working task' }
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const mockPool = {
|
|
186
|
+
getAgent: jest.fn().mockResolvedValue(mockAgent),
|
|
187
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
188
|
+
};
|
|
189
|
+
tool.setAgentPool(mockPool);
|
|
190
|
+
|
|
191
|
+
const result = await tool.execute(
|
|
192
|
+
{ actions: [{ action: 'complete', summary: 'Done', success: true }] },
|
|
193
|
+
{ agentId: 'agent-1' }
|
|
194
|
+
);
|
|
195
|
+
expect(result.success).toBe(false);
|
|
196
|
+
expect(result.output).toContain('pending task(s)');
|
|
197
|
+
expect(result.pendingTasks).toBe(2);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('should allow completion with success=false even with pending tasks', async () => {
|
|
201
|
+
const mockAgent = {
|
|
202
|
+
name: 'test-agent',
|
|
203
|
+
sessionId: 'sess-1',
|
|
204
|
+
taskList: {
|
|
205
|
+
tasks: [{ status: 'pending', title: 'Stuck task' }]
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const mockPool = {
|
|
209
|
+
getAgent: jest.fn().mockResolvedValue(mockAgent),
|
|
210
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
211
|
+
};
|
|
212
|
+
tool.setAgentPool(mockPool);
|
|
213
|
+
|
|
214
|
+
const result = await tool.execute(
|
|
215
|
+
{ actions: [{ action: 'complete', summary: 'Giving up', success: false }] },
|
|
216
|
+
{ agentId: 'agent-1' }
|
|
217
|
+
);
|
|
218
|
+
expect(result.success).toBe(true);
|
|
219
|
+
expect(result.successfulCompletion).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('should clear agent tasks and persist state on completion', async () => {
|
|
223
|
+
const mockAgent = {
|
|
224
|
+
id: 'agent-1',
|
|
225
|
+
name: 'test-agent',
|
|
226
|
+
sessionId: 'sess-1',
|
|
227
|
+
taskList: {
|
|
228
|
+
tasks: [{ status: 'completed', title: 'Done task' }]
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const mockPool = {
|
|
232
|
+
getAgent: jest.fn().mockResolvedValue(mockAgent),
|
|
233
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
234
|
+
};
|
|
235
|
+
tool.setAgentPool(mockPool);
|
|
236
|
+
|
|
237
|
+
await tool.execute(
|
|
238
|
+
{ actions: [{ action: 'complete', summary: 'All done' }] },
|
|
239
|
+
{ agentId: 'agent-1' }
|
|
240
|
+
);
|
|
241
|
+
expect(mockAgent.taskList.tasks).toEqual([]);
|
|
242
|
+
expect(mockAgent.autonomousWorkComplete).toBe(true);
|
|
243
|
+
expect(mockPool.persistAgentState).toHaveBeenCalledWith('agent-1');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('should broadcast via websocket when available', async () => {
|
|
247
|
+
const mockAgent = {
|
|
248
|
+
id: 'agent-1',
|
|
249
|
+
name: 'test-agent',
|
|
250
|
+
sessionId: 'sess-1',
|
|
251
|
+
mode: 'agent',
|
|
252
|
+
taskList: { tasks: [] }
|
|
253
|
+
};
|
|
254
|
+
const mockPool = {
|
|
255
|
+
getAgent: jest.fn().mockResolvedValue(mockAgent),
|
|
256
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
257
|
+
};
|
|
258
|
+
const mockWs = { broadcastToSession: jest.fn() };
|
|
259
|
+
tool.setAgentPool(mockPool);
|
|
260
|
+
tool.setWebSocketManager(mockWs);
|
|
261
|
+
|
|
262
|
+
await tool.execute(
|
|
263
|
+
{ actions: [{ action: 'complete', summary: 'Done' }] },
|
|
264
|
+
{ agentId: 'agent-1' }
|
|
265
|
+
);
|
|
266
|
+
expect(mockWs.broadcastToSession).toHaveBeenCalledWith('sess-1', expect.objectContaining({
|
|
267
|
+
type: 'agent_job_done'
|
|
268
|
+
}));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('should notify flow executor when set', async () => {
|
|
272
|
+
const mockAgent = {
|
|
273
|
+
id: 'agent-1',
|
|
274
|
+
name: 'test-agent',
|
|
275
|
+
sessionId: 'sess-1',
|
|
276
|
+
taskList: { tasks: [] }
|
|
277
|
+
};
|
|
278
|
+
const mockPool = {
|
|
279
|
+
getAgent: jest.fn().mockResolvedValue(mockAgent),
|
|
280
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
281
|
+
};
|
|
282
|
+
const mockFlow = { notifyAgentCompletion: jest.fn() };
|
|
283
|
+
tool.setAgentPool(mockPool);
|
|
284
|
+
tool.setFlowExecutor(mockFlow);
|
|
285
|
+
|
|
286
|
+
await tool.execute(
|
|
287
|
+
{ actions: [{ action: 'complete', summary: 'Done' }] },
|
|
288
|
+
{ agentId: 'agent-1' }
|
|
289
|
+
);
|
|
290
|
+
expect(mockFlow.notifyAgentCompletion).toHaveBeenCalledWith('agent-1', expect.objectContaining({
|
|
291
|
+
summary: 'Done',
|
|
292
|
+
success: true
|
|
293
|
+
}));
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
|
|
3
|
+
|
|
4
|
+
// Mock constants
|
|
5
|
+
jest.unstable_mockModule('../../utilities/constants.js', () => ({
|
|
6
|
+
TOOL_STATUS: { PENDING: 'pending', EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed' },
|
|
7
|
+
OPERATION_STATUS: { NOT_FOUND: 'not_found' },
|
|
8
|
+
ERROR_TYPES: {},
|
|
9
|
+
SYSTEM_DEFAULTS: { MAX_TOOL_EXECUTION_TIME: 300000 }
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Create mock memory service
|
|
13
|
+
const mockMemoryService = {
|
|
14
|
+
initialize: jest.fn().mockResolvedValue(undefined),
|
|
15
|
+
addMemory: jest.fn(),
|
|
16
|
+
updateMemory: jest.fn(),
|
|
17
|
+
deleteMemory: jest.fn(),
|
|
18
|
+
listMemories: jest.fn(),
|
|
19
|
+
readMemory: jest.fn(),
|
|
20
|
+
searchMemories: jest.fn(),
|
|
21
|
+
getMemoryStats: jest.fn()
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
jest.unstable_mockModule('../../services/memoryService.js', () => ({
|
|
25
|
+
getMemoryService: jest.fn(() => mockMemoryService)
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const { default: MemoryTool } = await import('../memoryTool.js');
|
|
29
|
+
|
|
30
|
+
describe('MemoryTool', () => {
|
|
31
|
+
let tool;
|
|
32
|
+
let logger;
|
|
33
|
+
const context = { agentId: 'agent-1' };
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
logger = createMockLogger();
|
|
38
|
+
tool = new MemoryTool({}, logger);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('constructor sets metadata correctly', () => {
|
|
42
|
+
expect(tool.id).toBe('memory');
|
|
43
|
+
expect(tool.requiresProject).toBe(false);
|
|
44
|
+
expect(tool.timeout).toBe(30000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('getDescription contains all actions', () => {
|
|
48
|
+
const desc = tool.getDescription();
|
|
49
|
+
expect(desc).toContain('Memory Tool');
|
|
50
|
+
expect(desc).toContain('add');
|
|
51
|
+
expect(desc).toContain('update');
|
|
52
|
+
expect(desc).toContain('delete');
|
|
53
|
+
expect(desc).toContain('list');
|
|
54
|
+
expect(desc).toContain('read');
|
|
55
|
+
expect(desc).toContain('search');
|
|
56
|
+
expect(desc).toContain('stats');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('getSupportedActions returns all 7 actions', () => {
|
|
60
|
+
expect(tool.getSupportedActions()).toEqual(
|
|
61
|
+
['add', 'update', 'delete', 'list', 'read', 'search', 'stats']
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('getRequiredParameters returns action', () => {
|
|
66
|
+
expect(tool.getRequiredParameters()).toEqual(['action']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('getCapabilities includes persistent flag', () => {
|
|
70
|
+
const caps = tool.getCapabilities();
|
|
71
|
+
expect(caps.persistent).toBe(true);
|
|
72
|
+
expect(caps.actions).toEqual(tool.getSupportedActions());
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('getParameterSchema has action enum', () => {
|
|
76
|
+
const schema = tool.getParameterSchema();
|
|
77
|
+
expect(schema.properties.action.enum).toEqual(tool.getSupportedActions());
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('parseParameters returns content as-is', () => {
|
|
81
|
+
const input = { action: 'add', title: 'test' };
|
|
82
|
+
expect(tool.parseParameters(input)).toBe(input);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('validateParameterTypes catches invalid action type', () => {
|
|
86
|
+
const result = tool.validateParameterTypes({ action: 123 });
|
|
87
|
+
expect(result.valid).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('validateParameterTypes catches invalid level', () => {
|
|
91
|
+
const result = tool.validateParameterTypes({ level: 'invalid' });
|
|
92
|
+
expect(result.valid).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('validateParameterTypes accepts valid params', () => {
|
|
96
|
+
const result = tool.validateParameterTypes({ action: 'add', title: 'test', level: 'titles' });
|
|
97
|
+
expect(result.valid).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('customValidateParameters rejects unknown action', () => {
|
|
101
|
+
const result = tool.customValidateParameters({ action: 'unknown' });
|
|
102
|
+
expect(result.valid).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('customValidateParameters requires title and content for add', () => {
|
|
106
|
+
const result = tool.customValidateParameters({ action: 'add' });
|
|
107
|
+
expect(result.valid).toBe(false);
|
|
108
|
+
expect(result.errors.some(e => e.includes('title'))).toBe(true);
|
|
109
|
+
expect(result.errors.some(e => e.includes('content'))).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('customValidateParameters requires id for update', () => {
|
|
113
|
+
const result = tool.customValidateParameters({ action: 'update' });
|
|
114
|
+
expect(result.valid).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('customValidateParameters requires id for delete', () => {
|
|
118
|
+
const result = tool.customValidateParameters({ action: 'delete' });
|
|
119
|
+
expect(result.valid).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('customValidateParameters requires id for read', () => {
|
|
123
|
+
const result = tool.customValidateParameters({ action: 'read' });
|
|
124
|
+
expect(result.valid).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('customValidateParameters requires query for search', () => {
|
|
128
|
+
const result = tool.customValidateParameters({ action: 'search' });
|
|
129
|
+
expect(result.valid).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('customValidateParameters enforces length limits', () => {
|
|
133
|
+
const result = tool.customValidateParameters({
|
|
134
|
+
action: 'add',
|
|
135
|
+
title: 'a'.repeat(201),
|
|
136
|
+
content: 'c'.repeat(10001),
|
|
137
|
+
description: 'd'.repeat(501)
|
|
138
|
+
});
|
|
139
|
+
expect(result.valid).toBe(false);
|
|
140
|
+
expect(result.errors.length).toBe(3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('execute throws without agentId', async () => {
|
|
144
|
+
await expect(tool.execute({ action: 'stats' }, {}))
|
|
145
|
+
.rejects.toThrow('Agent ID is required');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('execute add action creates memory', async () => {
|
|
149
|
+
mockMemoryService.addMemory.mockResolvedValue({
|
|
150
|
+
id: 'mem-1', title: 'Test', createdAt: '2025-01-01', expiration: null
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const result = await tool.execute(
|
|
154
|
+
{ action: 'add', title: 'Test', content: 'content', description: 'desc' },
|
|
155
|
+
context
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.success).toBe(true);
|
|
159
|
+
expect(result.action).toBe('add');
|
|
160
|
+
expect(result.memory.id).toBe('mem-1');
|
|
161
|
+
expect(mockMemoryService.addMemory).toHaveBeenCalledWith('agent-1', expect.objectContaining({ title: 'Test' }));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('execute update action modifies memory', async () => {
|
|
165
|
+
mockMemoryService.updateMemory.mockResolvedValue({
|
|
166
|
+
id: 'mem-1', title: 'Updated', updatedAt: '2025-01-02'
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await tool.execute(
|
|
170
|
+
{ action: 'update', id: 'mem-1', title: 'Updated' },
|
|
171
|
+
context
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
expect(result.action).toBe('update');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('execute update returns failure when memory not found', async () => {
|
|
179
|
+
mockMemoryService.updateMemory.mockResolvedValue(null);
|
|
180
|
+
|
|
181
|
+
const result = await tool.execute(
|
|
182
|
+
{ action: 'update', id: 'nonexistent', title: 'x' },
|
|
183
|
+
context
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(result.success).toBe(false);
|
|
187
|
+
expect(result.message).toContain('not found');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('execute delete action removes memory', async () => {
|
|
191
|
+
mockMemoryService.deleteMemory.mockResolvedValue(true);
|
|
192
|
+
|
|
193
|
+
const result = await tool.execute({ action: 'delete', id: 'mem-1' }, context);
|
|
194
|
+
|
|
195
|
+
expect(result.success).toBe(true);
|
|
196
|
+
expect(result.action).toBe('delete');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('execute delete returns failure when memory not found', async () => {
|
|
200
|
+
mockMemoryService.deleteMemory.mockResolvedValue(false);
|
|
201
|
+
|
|
202
|
+
const result = await tool.execute({ action: 'delete', id: 'nonexistent' }, context);
|
|
203
|
+
|
|
204
|
+
expect(result.success).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('execute list action returns memories', async () => {
|
|
208
|
+
mockMemoryService.listMemories.mockResolvedValue({
|
|
209
|
+
count: 2,
|
|
210
|
+
grouped: { '2025-01-01': [{ id: 'mem-1' }, { id: 'mem-2' }] }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await tool.execute({ action: 'list', level: 'titles' }, context);
|
|
214
|
+
|
|
215
|
+
expect(result.success).toBe(true);
|
|
216
|
+
expect(result.totalMemories).toBe(2);
|
|
217
|
+
expect(result.level).toBe('titles');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('execute list with default level', async () => {
|
|
221
|
+
mockMemoryService.listMemories.mockResolvedValue({ count: 0, grouped: {} });
|
|
222
|
+
|
|
223
|
+
const result = await tool.execute({ action: 'list' }, context);
|
|
224
|
+
|
|
225
|
+
expect(result.level).toBe('titles');
|
|
226
|
+
expect(result.message).toContain('No memories');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('execute read action loads memory', async () => {
|
|
230
|
+
mockMemoryService.readMemory.mockResolvedValue({
|
|
231
|
+
id: 'mem-1', title: 'Test', description: 'desc', content: 'full content',
|
|
232
|
+
createdAt: '2025-01-01', updatedAt: null, expiration: null, accessCount: 3
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const result = await tool.execute({ action: 'read', id: 'mem-1' }, context);
|
|
236
|
+
|
|
237
|
+
expect(result.success).toBe(true);
|
|
238
|
+
expect(result.memory.content).toBe('full content');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('execute read returns failure when not found', async () => {
|
|
242
|
+
mockMemoryService.readMemory.mockResolvedValue(null);
|
|
243
|
+
|
|
244
|
+
const result = await tool.execute({ action: 'read', id: 'nonexistent' }, context);
|
|
245
|
+
|
|
246
|
+
expect(result.success).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('execute search action returns results', async () => {
|
|
250
|
+
mockMemoryService.searchMemories.mockResolvedValue([
|
|
251
|
+
{ id: 'mem-1', title: 'Match' }
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
const result = await tool.execute({ action: 'search', query: 'Match' }, context);
|
|
255
|
+
|
|
256
|
+
expect(result.success).toBe(true);
|
|
257
|
+
expect(result.results.length).toBe(1);
|
|
258
|
+
expect(result.query).toBe('Match');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('execute search with no results', async () => {
|
|
262
|
+
mockMemoryService.searchMemories.mockResolvedValue([]);
|
|
263
|
+
|
|
264
|
+
const result = await tool.execute({ action: 'search', query: 'nothing' }, context);
|
|
265
|
+
|
|
266
|
+
expect(result.success).toBe(true);
|
|
267
|
+
expect(result.results.length).toBe(0);
|
|
268
|
+
expect(result.message).toContain('No memories found');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('execute stats action returns statistics', async () => {
|
|
272
|
+
mockMemoryService.getMemoryStats.mockResolvedValue({
|
|
273
|
+
totalMemories: 5, totalAccessCount: 20
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const result = await tool.execute({ action: 'stats' }, context);
|
|
277
|
+
|
|
278
|
+
expect(result.success).toBe(true);
|
|
279
|
+
expect(result.stats.totalMemories).toBe(5);
|
|
280
|
+
expect(result.message).toContain('5 total memories');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('execute throws on unknown action', async () => {
|
|
284
|
+
await expect(tool.execute({ action: 'unknown' }, context))
|
|
285
|
+
.rejects.toThrow('Unknown action');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('execute logs and re-throws service errors', async () => {
|
|
289
|
+
mockMemoryService.addMemory.mockRejectedValue(new Error('DB error'));
|
|
290
|
+
|
|
291
|
+
await expect(
|
|
292
|
+
tool.execute({ action: 'add', title: 'x', content: 'y' }, context)
|
|
293
|
+
).rejects.toThrow('DB error');
|
|
294
|
+
|
|
295
|
+
expect(logger.error).toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
});
|