onbuzz 3.6.1 → 3.6.2
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__/fileTreeTool.test.js +274 -0
- package/src/tools/__tests__/filesystemTool.test.js +717 -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
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageProcessor - Comprehensive unit tests (target: 80%+ line coverage)
|
|
3
|
+
* Tests message processing, tool command extraction, tool execution,
|
|
4
|
+
* parameter unwrapping, async tools, and error handling.
|
|
5
|
+
*/
|
|
6
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
7
|
+
import { createMockLogger, createMockConfig, createMockAiService } from '../../__test-utils__/mockFactories.js';
|
|
8
|
+
|
|
9
|
+
// ── Mock dependencies ────────────────────────────────────────────────────────
|
|
10
|
+
jest.unstable_mockModule('../../services/visualEditorBridge.js', () => ({
|
|
11
|
+
getVisualEditorBridge: jest.fn(() => ({
|
|
12
|
+
isEnabled: () => false,
|
|
13
|
+
hasInstance: () => false
|
|
14
|
+
})),
|
|
15
|
+
InstanceStatus: { IDLE: 'idle', RUNNING: 'running', ERROR: 'error' }
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const mockExtractToolCommands = jest.fn().mockReturnValue([]);
|
|
19
|
+
const mockNormalizeToolCommand = jest.fn((cmd) => ({
|
|
20
|
+
toolId: cmd.toolId,
|
|
21
|
+
parameters: cmd.parameters || {},
|
|
22
|
+
type: cmd.type || 'json',
|
|
23
|
+
rawContent: cmd.rawContent || ''
|
|
24
|
+
}));
|
|
25
|
+
const mockExtractAgentRedirects = jest.fn().mockReturnValue([]);
|
|
26
|
+
const mockParseXMLParameters = jest.fn().mockReturnValue({});
|
|
27
|
+
const mockDecodeHtmlEntities = jest.fn((s) => s);
|
|
28
|
+
|
|
29
|
+
jest.unstable_mockModule('../../utilities/tagParser.js', () => ({
|
|
30
|
+
default: jest.fn().mockImplementation(() => ({
|
|
31
|
+
extractToolCommands: mockExtractToolCommands,
|
|
32
|
+
normalizeToolCommand: mockNormalizeToolCommand,
|
|
33
|
+
extractAgentRedirects: mockExtractAgentRedirects,
|
|
34
|
+
parseXMLParameters: mockParseXMLParameters,
|
|
35
|
+
decodeHtmlEntities: mockDecodeHtmlEntities
|
|
36
|
+
}))
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.unstable_mockModule('../../tools/visualEditorTool.js', () => ({
|
|
40
|
+
VisualEditorTool: {
|
|
41
|
+
injectContextIntoMessage: jest.fn((msg) => msg)
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const { default: MessageProcessor } = await import('../messageProcessor.js');
|
|
46
|
+
|
|
47
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
48
|
+
function makeMP(overrides = {}) {
|
|
49
|
+
const config = createMockConfig(overrides.config);
|
|
50
|
+
const logger = createMockLogger();
|
|
51
|
+
const toolsRegistry = overrides.toolsRegistry || {
|
|
52
|
+
getTool: jest.fn().mockReturnValue(null)
|
|
53
|
+
};
|
|
54
|
+
const agentPool = overrides.agentPool || {
|
|
55
|
+
getAgent: jest.fn().mockResolvedValue(null),
|
|
56
|
+
addUserMessage: jest.fn().mockResolvedValue(undefined),
|
|
57
|
+
addInterAgentMessage: jest.fn().mockResolvedValue(undefined),
|
|
58
|
+
addToolResult: jest.fn().mockResolvedValue(undefined),
|
|
59
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
60
|
+
};
|
|
61
|
+
const contextManager = { getContext: jest.fn() };
|
|
62
|
+
const aiService = createMockAiService();
|
|
63
|
+
|
|
64
|
+
const mp = new MessageProcessor(
|
|
65
|
+
config, logger, toolsRegistry, agentPool, contextManager, aiService
|
|
66
|
+
);
|
|
67
|
+
return { mp, config, logger, toolsRegistry, agentPool, contextManager, aiService };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeAgent(overrides = {}) {
|
|
71
|
+
return {
|
|
72
|
+
id: overrides.id || 'agent-test',
|
|
73
|
+
name: 'TestAgent',
|
|
74
|
+
mode: 'chat',
|
|
75
|
+
conversations: {
|
|
76
|
+
full: { messages: [], lastUpdated: new Date().toISOString() }
|
|
77
|
+
},
|
|
78
|
+
currentModel: 'test-model',
|
|
79
|
+
messageQueues: { userMessages: [], interAgentMessages: [], toolResults: [] },
|
|
80
|
+
directoryAccess: { workingDirectory: '/tmp' },
|
|
81
|
+
projectDir: '/tmp',
|
|
82
|
+
...overrides
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
87
|
+
describe('MessageProcessor', () => {
|
|
88
|
+
let mp, logger, agentPool, toolsRegistry;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
jest.clearAllMocks();
|
|
92
|
+
({ mp, logger, agentPool, toolsRegistry } = makeMP());
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── processMessage ───────────────────────────────────────────────────
|
|
96
|
+
describe('processMessage', () => {
|
|
97
|
+
test('queues user message for existing agent', async () => {
|
|
98
|
+
const agent = makeAgent();
|
|
99
|
+
agentPool.getAgent.mockResolvedValue(agent);
|
|
100
|
+
|
|
101
|
+
const result = await mp.processMessage('agent-test', 'Hello', { sessionId: 'sess-1' });
|
|
102
|
+
expect(result.success).toBe(true);
|
|
103
|
+
expect(agentPool.addUserMessage).toHaveBeenCalledWith('agent-test', expect.objectContaining({
|
|
104
|
+
content: 'Hello',
|
|
105
|
+
role: 'user'
|
|
106
|
+
}));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('throws for non-existent agent', async () => {
|
|
110
|
+
agentPool.getAgent.mockResolvedValue(null);
|
|
111
|
+
await expect(mp.processMessage('nonexistent', 'hi')).rejects.toThrow('Agent not found');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('routes inter-agent messages to addInterAgentMessage', async () => {
|
|
115
|
+
const agent = makeAgent();
|
|
116
|
+
agentPool.getAgent.mockResolvedValue(agent);
|
|
117
|
+
|
|
118
|
+
await mp.processMessage('agent-test', 'inter-msg', {
|
|
119
|
+
isInterAgentMessage: true,
|
|
120
|
+
originalSender: 'agent-sender',
|
|
121
|
+
senderName: 'SenderAgent'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(agentPool.addInterAgentMessage).toHaveBeenCalledWith('agent-test', expect.objectContaining({
|
|
125
|
+
content: 'inter-msg',
|
|
126
|
+
sender: 'agent-sender',
|
|
127
|
+
senderName: 'SenderAgent'
|
|
128
|
+
}));
|
|
129
|
+
expect(agentPool.addUserMessage).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('handles non-string message by JSON.stringify', async () => {
|
|
133
|
+
const agent = makeAgent();
|
|
134
|
+
agentPool.getAgent.mockResolvedValue(agent);
|
|
135
|
+
|
|
136
|
+
await mp.processMessage('agent-test', { key: 'value' }, {});
|
|
137
|
+
expect(logger.info).toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('handles null message gracefully', async () => {
|
|
141
|
+
const agent = makeAgent();
|
|
142
|
+
agentPool.getAgent.mockResolvedValue(agent);
|
|
143
|
+
|
|
144
|
+
const result = await mp.processMessage('agent-test', null, {});
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('registers session with scheduler if available', async () => {
|
|
149
|
+
const agent = makeAgent();
|
|
150
|
+
agentPool.getAgent.mockResolvedValue(agent);
|
|
151
|
+
const mockScheduler = { addAgent: jest.fn().mockResolvedValue(undefined) };
|
|
152
|
+
mp.setScheduler(mockScheduler);
|
|
153
|
+
|
|
154
|
+
await mp.processMessage('agent-test', 'test', { sessionId: 'sess-1' });
|
|
155
|
+
expect(mockScheduler.addAgent).toHaveBeenCalledWith('agent-test', expect.objectContaining({
|
|
156
|
+
triggeredBy: 'user-message',
|
|
157
|
+
sessionId: 'sess-1'
|
|
158
|
+
}));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('sets triggeredBy to inter-agent-message for inter-agent context', async () => {
|
|
162
|
+
const agent = makeAgent();
|
|
163
|
+
agentPool.getAgent.mockResolvedValue(agent);
|
|
164
|
+
const mockScheduler = { addAgent: jest.fn().mockResolvedValue(undefined) };
|
|
165
|
+
mp.setScheduler(mockScheduler);
|
|
166
|
+
|
|
167
|
+
await mp.processMessage('agent-test', 'msg', {
|
|
168
|
+
isInterAgentMessage: true,
|
|
169
|
+
originalSender: 'x',
|
|
170
|
+
sessionId: 'sess-1'
|
|
171
|
+
});
|
|
172
|
+
expect(mockScheduler.addAgent).toHaveBeenCalledWith('agent-test', expect.objectContaining({
|
|
173
|
+
triggeredBy: 'inter-agent-message'
|
|
174
|
+
}));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('includes flow execution context in user message', async () => {
|
|
178
|
+
const agent = makeAgent();
|
|
179
|
+
agentPool.getAgent.mockResolvedValue(agent);
|
|
180
|
+
|
|
181
|
+
await mp.processMessage('agent-test', 'do flow', {
|
|
182
|
+
isFlowExecution: true,
|
|
183
|
+
flowRunId: 'run-1',
|
|
184
|
+
flowNodeId: 'node-1'
|
|
185
|
+
});
|
|
186
|
+
expect(agentPool.addUserMessage).toHaveBeenCalledWith('agent-test', expect.objectContaining({
|
|
187
|
+
isFlowExecution: true,
|
|
188
|
+
flowRunId: 'run-1'
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ─── unwrapParameters ─────────────────────────────────────────────────
|
|
194
|
+
describe('unwrapParameters', () => {
|
|
195
|
+
test('returns null/undefined as-is', () => {
|
|
196
|
+
expect(mp.unwrapParameters(null)).toBeNull();
|
|
197
|
+
expect(mp.unwrapParameters(undefined)).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('returns primitives as-is', () => {
|
|
201
|
+
expect(mp.unwrapParameters('hello')).toBe('hello');
|
|
202
|
+
expect(mp.unwrapParameters(42)).toBe(42);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('unwraps {value, attributes} wrapped object', () => {
|
|
206
|
+
const wrapped = { value: 'test-value', attributes: {} };
|
|
207
|
+
expect(mp.unwrapParameters(wrapped)).toBe('test-value');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('unwraps nested {value, attributes} in object properties', () => {
|
|
211
|
+
const params = {
|
|
212
|
+
filePath: { value: '/path/to/file', attributes: {} },
|
|
213
|
+
content: { value: 'file content', attributes: {} }
|
|
214
|
+
};
|
|
215
|
+
const result = mp.unwrapParameters(params);
|
|
216
|
+
expect(result.filePath).toBe('/path/to/file');
|
|
217
|
+
expect(result.content).toBe('file content');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('preserves attributes when present', () => {
|
|
221
|
+
const params = {
|
|
222
|
+
action: { value: 'write', attributes: { type: 'file' } }
|
|
223
|
+
};
|
|
224
|
+
const result = mp.unwrapParameters(params);
|
|
225
|
+
expect(result.action).toBe('write');
|
|
226
|
+
expect(result.action_attributes).toEqual({ type: 'file' });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('handles arrays by unwrapping each element', () => {
|
|
230
|
+
const arr = [
|
|
231
|
+
{ value: 'a', attributes: {} },
|
|
232
|
+
{ value: 'b', attributes: {} }
|
|
233
|
+
];
|
|
234
|
+
const result = mp.unwrapParameters(arr);
|
|
235
|
+
expect(result).toEqual(['a', 'b']);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('recursively unwraps nested objects', () => {
|
|
239
|
+
const params = {
|
|
240
|
+
outer: {
|
|
241
|
+
inner: { value: 'deep', attributes: {} }
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const result = mp.unwrapParameters(params);
|
|
245
|
+
expect(result.outer.inner).toBe('deep');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('keeps plain values unchanged', () => {
|
|
249
|
+
const params = { name: 'test', count: 5 };
|
|
250
|
+
const result = mp.unwrapParameters(params);
|
|
251
|
+
expect(result).toEqual({ name: 'test', count: 5 });
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ─── extractToolCommands ──────────────────────────────────────────────
|
|
256
|
+
describe('extractToolCommands', () => {
|
|
257
|
+
test('returns empty array for message with no commands', async () => {
|
|
258
|
+
mockExtractToolCommands.mockReturnValue([]);
|
|
259
|
+
mockExtractAgentRedirects.mockReturnValue([]);
|
|
260
|
+
const commands = await mp.extractToolCommands('Just a regular message');
|
|
261
|
+
expect(commands).toEqual([]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('extracts commands from TagParser results', async () => {
|
|
265
|
+
mockExtractToolCommands.mockReturnValue([
|
|
266
|
+
{ toolId: 'terminal', parameters: { command: 'ls' }, type: 'json', rawContent: '{}', position: {} }
|
|
267
|
+
]);
|
|
268
|
+
mockNormalizeToolCommand.mockReturnValue({
|
|
269
|
+
toolId: 'terminal',
|
|
270
|
+
parameters: { command: 'ls' },
|
|
271
|
+
type: 'json',
|
|
272
|
+
rawContent: '{}'
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const commands = await mp.extractToolCommands('Some message with tool');
|
|
276
|
+
expect(commands).toHaveLength(1);
|
|
277
|
+
expect(commands[0].toolId).toBe('terminal');
|
|
278
|
+
expect(commands[0].parameters.command).toBe('ls');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('extracts bracket notation commands', async () => {
|
|
282
|
+
mockExtractToolCommands.mockReturnValue([]);
|
|
283
|
+
const msg = '[tool id="filesystem"]{"action":"read"}[/tool]';
|
|
284
|
+
const commands = await mp.extractToolCommands(msg);
|
|
285
|
+
expect(commands.length).toBeGreaterThanOrEqual(1);
|
|
286
|
+
expect(commands[0].toolId).toBe('filesystem');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('deduplicates bracket commands already found by TagParser', async () => {
|
|
290
|
+
const rawMatch = '[tool id="terminal"]ls[/tool]';
|
|
291
|
+
mockExtractToolCommands.mockReturnValue([
|
|
292
|
+
{ toolId: 'terminal', parameters: {}, type: 'bracket', rawContent: rawMatch, position: { start: 0, end: rawMatch.length } }
|
|
293
|
+
]);
|
|
294
|
+
mockNormalizeToolCommand.mockReturnValue({
|
|
295
|
+
toolId: 'terminal',
|
|
296
|
+
parameters: {},
|
|
297
|
+
type: 'bracket',
|
|
298
|
+
rawContent: rawMatch
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const commands = await mp.extractToolCommands(rawMatch);
|
|
302
|
+
// Should not have duplicates
|
|
303
|
+
const terminalCmds = commands.filter(c => c.toolId === 'terminal');
|
|
304
|
+
expect(terminalCmds.length).toBe(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('extracts agent redirects', async () => {
|
|
308
|
+
mockExtractToolCommands.mockReturnValue([]);
|
|
309
|
+
mockExtractAgentRedirects.mockReturnValue([
|
|
310
|
+
{ to: 'agent-other', content: 'help', urgent: false, requiresResponse: false, context: {}, rawMatch: '<redirect>' }
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
const commands = await mp.extractToolCommands('redirect message');
|
|
314
|
+
expect(commands).toHaveLength(1);
|
|
315
|
+
expect(commands[0].toolId).toBe('agentcommunication');
|
|
316
|
+
expect(commands[0].type).toBe('redirect');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('handles bracket commands with XML content inside', async () => {
|
|
320
|
+
mockExtractToolCommands.mockReturnValue([]);
|
|
321
|
+
mockParseXMLParameters.mockReturnValue({ action: 'write', filePath: '/test.txt' });
|
|
322
|
+
mockNormalizeToolCommand.mockReturnValue({
|
|
323
|
+
toolId: 'filesystem',
|
|
324
|
+
parameters: { actions: [{ action: 'write' }] },
|
|
325
|
+
type: 'xml'
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const msg = '[tool id="filesystem"]<action>write</action><filePath>/test.txt</filePath>[/tool]';
|
|
329
|
+
const commands = await mp.extractToolCommands(msg);
|
|
330
|
+
expect(commands.length).toBeGreaterThanOrEqual(1);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('falls back to bracket format when XML parsing fails', async () => {
|
|
334
|
+
mockExtractToolCommands.mockReturnValue([]);
|
|
335
|
+
mockParseXMLParameters.mockImplementation(() => { throw new Error('bad xml'); });
|
|
336
|
+
|
|
337
|
+
const msg = '[tool id="filesystem"]<broken>xml[/tool]';
|
|
338
|
+
const commands = await mp.extractToolCommands(msg);
|
|
339
|
+
expect(commands.length).toBeGreaterThanOrEqual(1);
|
|
340
|
+
expect(commands[0].type).toBe('bracket');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('handles async attribute in bracket notation', async () => {
|
|
344
|
+
mockExtractToolCommands.mockReturnValue([]);
|
|
345
|
+
const msg = '[tool id="terminal" async="true"]long running[/tool]';
|
|
346
|
+
const commands = await mp.extractToolCommands(msg);
|
|
347
|
+
expect(commands.length).toBeGreaterThanOrEqual(1);
|
|
348
|
+
expect(commands[0].isAsync).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ─── executeTools ─────────────────────────────────────────────────────
|
|
353
|
+
describe('executeTools', () => {
|
|
354
|
+
test('returns failed result for unknown tool', async () => {
|
|
355
|
+
toolsRegistry.getTool.mockReturnValue(null);
|
|
356
|
+
const commands = [{ toolId: 'unknown', parameters: {}, isAsync: false }];
|
|
357
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
358
|
+
expect(results).toHaveLength(1);
|
|
359
|
+
expect(results[0].status).toBe('failed');
|
|
360
|
+
expect(results[0].error).toContain('Tool not found');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('executes synchronous tool successfully', async () => {
|
|
364
|
+
const mockTool = { execute: jest.fn().mockResolvedValue({ success: true }) };
|
|
365
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
366
|
+
const commands = [{ toolId: 'terminal', parameters: { cmd: 'ls' }, isAsync: false }];
|
|
367
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
368
|
+
expect(results).toHaveLength(1);
|
|
369
|
+
expect(results[0].status).toBe('completed');
|
|
370
|
+
expect(results[0].result).toEqual({ success: true });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('marks result as partial when command was truncated', async () => {
|
|
374
|
+
const mockTool = { execute: jest.fn().mockResolvedValue({ success: true }) };
|
|
375
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
376
|
+
const commands = [{ toolId: 'terminal', parameters: {}, isAsync: false, wasTruncated: true }];
|
|
377
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
378
|
+
expect(results[0].status).toBe('partial');
|
|
379
|
+
expect(results[0].wasTruncated).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('catches tool execution error', async () => {
|
|
383
|
+
const mockTool = { execute: jest.fn().mockRejectedValue(new Error('tool crashed')) };
|
|
384
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
385
|
+
const commands = [{ toolId: 'terminal', parameters: {}, isAsync: false }];
|
|
386
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
387
|
+
expect(results[0].status).toBe('failed');
|
|
388
|
+
expect(results[0].error).toBe('tool crashed');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('parses content string when no parameters object', async () => {
|
|
392
|
+
const mockTool = {
|
|
393
|
+
execute: jest.fn().mockResolvedValue({ ok: true }),
|
|
394
|
+
parseParameters: jest.fn().mockReturnValue({ parsed: true })
|
|
395
|
+
};
|
|
396
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
397
|
+
const commands = [{ toolId: 'terminal', content: '{"cmd": "ls"}', isAsync: false }];
|
|
398
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
399
|
+
expect(mockTool.parseParameters).toHaveBeenCalledWith('{"cmd": "ls"}');
|
|
400
|
+
expect(results[0].status).toBe('completed');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('uses raw content when tool has no parseParameters', async () => {
|
|
404
|
+
const mockTool = { execute: jest.fn().mockResolvedValue({ ok: true }) };
|
|
405
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
406
|
+
const commands = [{ toolId: 'terminal', content: 'raw content', isAsync: false }];
|
|
407
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
408
|
+
expect(mockTool.execute).toHaveBeenCalledWith('raw content', expect.any(Object));
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('falls back to raw content when parseParameters throws', async () => {
|
|
412
|
+
const mockTool = {
|
|
413
|
+
execute: jest.fn().mockResolvedValue({ ok: true }),
|
|
414
|
+
parseParameters: jest.fn().mockImplementation(() => { throw new Error('parse fail'); })
|
|
415
|
+
};
|
|
416
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
417
|
+
const commands = [{ toolId: 'terminal', content: 'raw', isAsync: false }];
|
|
418
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
419
|
+
expect(mockTool.execute).toHaveBeenCalledWith('raw', expect.any(Object));
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('unwraps TagParser format parameters before execution', async () => {
|
|
423
|
+
const mockTool = { execute: jest.fn().mockResolvedValue({ ok: true }) };
|
|
424
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
425
|
+
const commands = [{
|
|
426
|
+
toolId: 'filesystem',
|
|
427
|
+
parameters: { filePath: { value: '/test.txt', attributes: {} } },
|
|
428
|
+
isAsync: false
|
|
429
|
+
}];
|
|
430
|
+
const results = await mp.executeTools(commands, { agentId: 'a1' });
|
|
431
|
+
// The unwrapped parameter should have filePath as string
|
|
432
|
+
const calledWith = mockTool.execute.mock.calls[0][0];
|
|
433
|
+
expect(calledWith.filePath).toBe('/test.txt');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('stores results in execution history', async () => {
|
|
437
|
+
const mockTool = { execute: jest.fn().mockResolvedValue({ ok: true }) };
|
|
438
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
439
|
+
const commands = [{ toolId: 'terminal', parameters: {}, isAsync: false }];
|
|
440
|
+
await mp.executeTools(commands, { agentId: 'a1', sessionId: 'sess-1' });
|
|
441
|
+
expect(mp.executionHistory.size).toBe(1);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('passes directoryAccess workingDirectory as projectDir', async () => {
|
|
445
|
+
const mockTool = { execute: jest.fn().mockResolvedValue({ ok: true }) };
|
|
446
|
+
toolsRegistry.getTool.mockReturnValue(mockTool);
|
|
447
|
+
const commands = [{ toolId: 'terminal', parameters: {}, isAsync: false }];
|
|
448
|
+
const context = {
|
|
449
|
+
agentId: 'a1',
|
|
450
|
+
directoryAccess: { workingDirectory: '/custom/dir' },
|
|
451
|
+
projectDir: '/original'
|
|
452
|
+
};
|
|
453
|
+
await mp.executeTools(commands, context);
|
|
454
|
+
const toolContext = mockTool.execute.mock.calls[0][1];
|
|
455
|
+
expect(toolContext.projectDir).toBe('/custom/dir');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ─── executeAsyncTool ─────────────────────────────────────────────────
|
|
460
|
+
describe('executeAsyncTool', () => {
|
|
461
|
+
test('returns async-pending status with operationId', async () => {
|
|
462
|
+
const mockTool = { execute: jest.fn().mockResolvedValue({ ok: true }) };
|
|
463
|
+
const command = { toolId: 'terminal', parameters: { cmd: 'long' }, isAsync: true };
|
|
464
|
+
const result = await mp.executeAsyncTool(command, mockTool, { agentId: 'a1' });
|
|
465
|
+
expect(result.status).toBe('async-pending');
|
|
466
|
+
expect(result.operationId).toBeDefined();
|
|
467
|
+
expect(mp.asyncOperations.has(result.operationId)).toBe(true);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ─── getToolStatus ────────────────────────────────────────────────────
|
|
472
|
+
describe('getToolStatus', () => {
|
|
473
|
+
test('returns not-found for unknown operation', async () => {
|
|
474
|
+
const result = await mp.getToolStatus('unknown-op');
|
|
475
|
+
expect(result.status).toBe('not-found');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('returns operation status for known operation', async () => {
|
|
479
|
+
mp.asyncOperations.set('op-1', {
|
|
480
|
+
id: 'op-1', toolId: 'terminal', status: 'completed', result: { ok: true }
|
|
481
|
+
});
|
|
482
|
+
const result = await mp.getToolStatus('op-1');
|
|
483
|
+
expect(result.status).toBe('completed');
|
|
484
|
+
expect(result.toolId).toBe('terminal');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ─── formatToolResultForAgent ─────────────────────────────────────────
|
|
489
|
+
describe('formatToolResultForAgent', () => {
|
|
490
|
+
test('formats completed object result', () => {
|
|
491
|
+
const result = mp.formatToolResultForAgent({
|
|
492
|
+
toolId: 'fs', status: 'completed', result: { data: 'ok' }
|
|
493
|
+
});
|
|
494
|
+
expect(result).toContain('fs');
|
|
495
|
+
expect(result).toContain('successfully');
|
|
496
|
+
expect(result).toContain('"data"');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('formats completed string result', () => {
|
|
500
|
+
const result = mp.formatToolResultForAgent({
|
|
501
|
+
toolId: 'terminal', status: 'completed', result: 'done'
|
|
502
|
+
});
|
|
503
|
+
expect(result).toContain('done');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('formats failed result', () => {
|
|
507
|
+
const result = mp.formatToolResultForAgent({
|
|
508
|
+
toolId: 'x', status: 'failed', error: 'boom'
|
|
509
|
+
});
|
|
510
|
+
expect(result).toContain('failed');
|
|
511
|
+
expect(result).toContain('boom');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test('formats failed without error message', () => {
|
|
515
|
+
const result = mp.formatToolResultForAgent({ toolId: 'x', status: 'failed' });
|
|
516
|
+
expect(result).toContain('Unknown error');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('formats async-pending result', () => {
|
|
520
|
+
const result = mp.formatToolResultForAgent({
|
|
521
|
+
toolId: 'x', status: 'async-pending', operationId: 'op-1'
|
|
522
|
+
});
|
|
523
|
+
expect(result).toContain('asynchronously');
|
|
524
|
+
expect(result).toContain('op-1');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('formats unknown status', () => {
|
|
528
|
+
const result = mp.formatToolResultForAgent({ toolId: 'x', status: 'unknown' });
|
|
529
|
+
expect(result).toContain('status: unknown');
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ─── stopAutonomousExecution ──────────────────────────────────────────
|
|
534
|
+
describe('stopAutonomousExecution', () => {
|
|
535
|
+
test('returns error when no scheduler', async () => {
|
|
536
|
+
mp.scheduler = null;
|
|
537
|
+
const result = await mp.stopAutonomousExecution('agent-1');
|
|
538
|
+
expect(result.success).toBe(false);
|
|
539
|
+
expect(result.error).toContain('Scheduler not available');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('delegates to scheduler.stopAgentExecution', async () => {
|
|
543
|
+
const mockScheduler = { stopAgentExecution: jest.fn().mockResolvedValue({ success: true }) };
|
|
544
|
+
mp.scheduler = mockScheduler;
|
|
545
|
+
const result = await mp.stopAutonomousExecution('agent-1');
|
|
546
|
+
expect(mockScheduler.stopAgentExecution).toHaveBeenCalledWith('agent-1');
|
|
547
|
+
expect(result.success).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ─── setters ──────────────────────────────────────────────────────────
|
|
552
|
+
describe('setters', () => {
|
|
553
|
+
test('setWebSocketManager stores manager', () => {
|
|
554
|
+
const ws = { broadcast: jest.fn() };
|
|
555
|
+
mp.setWebSocketManager(ws);
|
|
556
|
+
expect(mp.webSocketManager).toBe(ws);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test('setScheduler stores scheduler', () => {
|
|
560
|
+
const sched = { addAgent: jest.fn() };
|
|
561
|
+
mp.setScheduler(sched);
|
|
562
|
+
expect(mp.scheduler).toBe(sched);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// ─── notifyAgentOfToolCompletion ──────────────────────────────────────
|
|
567
|
+
describe('notifyAgentOfToolCompletion', () => {
|
|
568
|
+
test('queues tool result for the agent', async () => {
|
|
569
|
+
const operation = {
|
|
570
|
+
agentId: 'a1',
|
|
571
|
+
toolId: 'terminal',
|
|
572
|
+
status: 'completed',
|
|
573
|
+
result: { ok: true },
|
|
574
|
+
startTime: new Date().toISOString(),
|
|
575
|
+
endTime: new Date().toISOString(),
|
|
576
|
+
context: { sessionId: 'sess-1' }
|
|
577
|
+
};
|
|
578
|
+
await mp.notifyAgentOfToolCompletion(operation);
|
|
579
|
+
expect(agentPool.addToolResult).toHaveBeenCalledWith('a1', expect.objectContaining({
|
|
580
|
+
toolId: 'terminal',
|
|
581
|
+
status: 'completed'
|
|
582
|
+
}));
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('does nothing when no agentId', async () => {
|
|
586
|
+
await mp.notifyAgentOfToolCompletion({ toolId: 'x' });
|
|
587
|
+
expect(agentPool.addToolResult).not.toHaveBeenCalled();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test('logs error when addToolResult fails', async () => {
|
|
591
|
+
agentPool.addToolResult.mockRejectedValueOnce(new Error('queue fail'));
|
|
592
|
+
await mp.notifyAgentOfToolCompletion({
|
|
593
|
+
agentId: 'a1', toolId: 'x', status: 'completed', startTime: '', endTime: ''
|
|
594
|
+
});
|
|
595
|
+
expect(logger.error).toHaveBeenCalled();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test('registers with scheduler when available', async () => {
|
|
599
|
+
const mockScheduler = { addAgent: jest.fn().mockResolvedValue(undefined) };
|
|
600
|
+
mp.scheduler = mockScheduler;
|
|
601
|
+
await mp.notifyAgentOfToolCompletion({
|
|
602
|
+
agentId: 'a1', toolId: 'x', status: 'completed',
|
|
603
|
+
startTime: '', endTime: '', context: { sessionId: 'sess-1' }
|
|
604
|
+
});
|
|
605
|
+
expect(mockScheduler.addAgent).toHaveBeenCalledWith('a1', expect.objectContaining({
|
|
606
|
+
triggeredBy: 'tool-completion'
|
|
607
|
+
}));
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// ─── extractAndExecuteTools ───────────────────────────────────────────
|
|
612
|
+
describe('extractAndExecuteTools', () => {
|
|
613
|
+
test('returns empty array when no commands found', async () => {
|
|
614
|
+
mockExtractToolCommands.mockReturnValue([]);
|
|
615
|
+
mockExtractAgentRedirects.mockReturnValue([]);
|
|
616
|
+
const results = await mp.extractAndExecuteTools('no tools here', 'a1', {});
|
|
617
|
+
expect(results).toEqual([]);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('returns empty array on error', async () => {
|
|
621
|
+
mockExtractToolCommands.mockImplementation(() => { throw new Error('parse crash'); });
|
|
622
|
+
const results = await mp.extractAndExecuteTools('bad', 'a1', {});
|
|
623
|
+
expect(results).toEqual([]);
|
|
624
|
+
expect(logger.error).toHaveBeenCalled();
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
});
|