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,384 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
|
|
3
|
+
|
|
4
|
+
// Mock child_process BEFORE importing TerminalTool
|
|
5
|
+
const mockExec = jest.fn();
|
|
6
|
+
const mockSpawn = jest.fn();
|
|
7
|
+
|
|
8
|
+
jest.unstable_mockModule('child_process', () => ({
|
|
9
|
+
exec: mockExec,
|
|
10
|
+
spawn: mockSpawn,
|
|
11
|
+
execSync: jest.fn(() => ''),
|
|
12
|
+
default: { exec: mockExec, spawn: mockSpawn, execSync: jest.fn(() => '') }
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock fs/promises
|
|
16
|
+
const mockFs = {
|
|
17
|
+
stat: jest.fn(),
|
|
18
|
+
readdir: jest.fn(),
|
|
19
|
+
mkdir: jest.fn()
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
jest.unstable_mockModule('fs/promises', () => ({
|
|
23
|
+
default: mockFs,
|
|
24
|
+
...mockFs
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Mock constants
|
|
28
|
+
const TERMINAL_CONFIG = {
|
|
29
|
+
STATES: {
|
|
30
|
+
RUNNING: 'running',
|
|
31
|
+
COMPLETED: 'completed',
|
|
32
|
+
FAILED: 'failed',
|
|
33
|
+
KILLED: 'killed'
|
|
34
|
+
},
|
|
35
|
+
MAX_OUTPUT_SIZE: 100000
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
jest.unstable_mockModule('../../utilities/constants.js', () => ({
|
|
39
|
+
TOOL_STATUS: { PENDING: 'pending', EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed' },
|
|
40
|
+
OPERATION_STATUS: { NOT_FOUND: 'not_found' },
|
|
41
|
+
ERROR_TYPES: {},
|
|
42
|
+
SYSTEM_DEFAULTS: { MAX_TOOL_EXECUTION_TIME: 300000 },
|
|
43
|
+
TERMINAL_CONFIG
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Mock tagParser
|
|
47
|
+
jest.unstable_mockModule('../../utilities/tagParser.js', () => ({
|
|
48
|
+
default: {
|
|
49
|
+
extractContent: jest.fn((content, tag) => {
|
|
50
|
+
const regex = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'gi');
|
|
51
|
+
const matches = [];
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = regex.exec(content)) !== null) {
|
|
54
|
+
matches.push(match[1]);
|
|
55
|
+
}
|
|
56
|
+
return matches;
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Mock DirectoryAccessManager
|
|
62
|
+
jest.unstable_mockModule('../../utilities/directoryAccessManager.js', () => ({
|
|
63
|
+
default: class MockDirectoryAccessManager {
|
|
64
|
+
constructor() {}
|
|
65
|
+
createDirectoryAccess(config) { return config; }
|
|
66
|
+
getWorkingDirectory(config) { return config.workingDirectory || '/project'; }
|
|
67
|
+
validateReadAccess() { return { allowed: true }; }
|
|
68
|
+
validateWriteAccess() { return { allowed: true }; }
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const { default: TerminalTool } = await import('../terminalTool.js');
|
|
73
|
+
|
|
74
|
+
// Helper: configure mockExec to call callback with success
|
|
75
|
+
function setupExecSuccess(stdout = 'output', stderr = '') {
|
|
76
|
+
mockExec.mockImplementation((cmd, opts, cb) => {
|
|
77
|
+
if (typeof opts === 'function') { cb = opts; opts = {}; }
|
|
78
|
+
process.nextTick(() => cb(null, stdout, stderr));
|
|
79
|
+
return { pid: 12345, on: jest.fn(), kill: jest.fn(), killed: false };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Helper: configure mockExec to call callback with error
|
|
84
|
+
function setupExecError(message = 'command failed', code = 1, stdout = '', stderr = 'error output') {
|
|
85
|
+
mockExec.mockImplementation((cmd, opts, cb) => {
|
|
86
|
+
if (typeof opts === 'function') { cb = opts; opts = {}; }
|
|
87
|
+
const error = new Error(message);
|
|
88
|
+
error.code = code;
|
|
89
|
+
process.nextTick(() => cb(error, stdout, stderr));
|
|
90
|
+
return { pid: 12345, on: jest.fn(), kill: jest.fn(), killed: false };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('TerminalTool', () => {
|
|
95
|
+
let tool;
|
|
96
|
+
let logger;
|
|
97
|
+
const context = { agentId: 'agent-1', projectDir: '/project' };
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
jest.clearAllMocks();
|
|
101
|
+
logger = createMockLogger();
|
|
102
|
+
tool = new TerminalTool({}, logger);
|
|
103
|
+
// Override translateCommand to return command as-is for simpler testing
|
|
104
|
+
tool.translateCommand = jest.fn(async (cmd) => cmd);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('constructor sets metadata correctly', () => {
|
|
108
|
+
expect(tool.id).toBe('terminal');
|
|
109
|
+
expect(tool.requiresProject).toBe(false);
|
|
110
|
+
expect(tool.timeout).toBe(120000);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('getDescription mentions terminal', () => {
|
|
114
|
+
const desc = tool.getDescription();
|
|
115
|
+
expect(desc).toContain('Terminal Tool');
|
|
116
|
+
expect(desc).toContain('run-command');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('getSupportedActions returns all action types', () => {
|
|
120
|
+
const actions = tool.getSupportedActions();
|
|
121
|
+
expect(actions).toContain('run-command');
|
|
122
|
+
expect(actions).toContain('change-directory');
|
|
123
|
+
expect(actions).toContain('list-directory');
|
|
124
|
+
expect(actions).toContain('create-directory');
|
|
125
|
+
expect(actions).toContain('get-working-directory');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('getRequiredParameters returns actions', () => {
|
|
129
|
+
expect(tool.getRequiredParameters()).toEqual(['actions']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('parseParameters extracts run-command from XML', () => {
|
|
133
|
+
const content = '<run-command>npm install</run-command>';
|
|
134
|
+
const result = tool.parseParameters(content);
|
|
135
|
+
expect(result.actions).toEqual([{ type: 'run-command', command: 'npm install' }]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('parseParameters extracts change-directory from XML', () => {
|
|
139
|
+
const content = '<change-directory>src/components</change-directory>';
|
|
140
|
+
const result = tool.parseParameters(content);
|
|
141
|
+
expect(result.actions).toEqual([{ type: 'change-directory', directory: 'src/components' }]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('parseParameters extracts get-working-directory from XML', () => {
|
|
145
|
+
const content = '<get-working-directory>true</get-working-directory>';
|
|
146
|
+
const result = tool.parseParameters(content);
|
|
147
|
+
expect(result.actions.some(a => a.type === 'get-working-directory')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('parseParameters extracts timeout and async flags', () => {
|
|
151
|
+
const content = '<run-command>ls</run-command><timeout>5000</timeout><async>true</async>';
|
|
152
|
+
const result = tool.parseParameters(content);
|
|
153
|
+
expect(result.timeout).toBe(5000);
|
|
154
|
+
expect(result.async).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('customValidateParameters rejects empty actions', () => {
|
|
158
|
+
const result = tool.customValidateParameters({ actions: [] });
|
|
159
|
+
expect(result.valid).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('customValidateParameters rejects action without type', () => {
|
|
163
|
+
const result = tool.customValidateParameters({ actions: [{ command: 'ls' }] });
|
|
164
|
+
expect(result.valid).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('customValidateParameters rejects run-command without command', () => {
|
|
168
|
+
const result = tool.customValidateParameters({
|
|
169
|
+
actions: [{ type: 'run-command', command: '' }]
|
|
170
|
+
});
|
|
171
|
+
expect(result.valid).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('customValidateParameters rejects blocked commands', () => {
|
|
175
|
+
const result = tool.customValidateParameters({
|
|
176
|
+
actions: [{ type: 'run-command', command: 'rm -rf /' }]
|
|
177
|
+
});
|
|
178
|
+
expect(result.valid).toBe(false);
|
|
179
|
+
expect(result.errors.some(e => e.includes('blocked'))).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('customValidateParameters rejects change-directory without directory', () => {
|
|
183
|
+
const result = tool.customValidateParameters({
|
|
184
|
+
actions: [{ type: 'change-directory', directory: '' }]
|
|
185
|
+
});
|
|
186
|
+
expect(result.valid).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('customValidateParameters rejects unknown action type', () => {
|
|
190
|
+
const result = tool.customValidateParameters({
|
|
191
|
+
actions: [{ type: 'unknown-action' }]
|
|
192
|
+
});
|
|
193
|
+
expect(result.valid).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('customValidateParameters accepts valid actions', () => {
|
|
197
|
+
const result = tool.customValidateParameters({
|
|
198
|
+
actions: [{ type: 'run-command', command: 'npm install' }]
|
|
199
|
+
});
|
|
200
|
+
expect(result.valid).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('isBlockedCommand detects blocked commands', () => {
|
|
204
|
+
expect(tool.isBlockedCommand('rm -rf /')).toBe(true);
|
|
205
|
+
expect(tool.isBlockedCommand('format C:')).toBe(true);
|
|
206
|
+
expect(tool.isBlockedCommand('shutdown')).toBe(true);
|
|
207
|
+
expect(tool.isBlockedCommand('npm install')).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('isBlockedCommand is case-insensitive', () => {
|
|
211
|
+
expect(tool.isBlockedCommand('SHUTDOWN')).toBe(true);
|
|
212
|
+
expect(tool.isBlockedCommand('Format')).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('execute run-command returns stdout on success', async () => {
|
|
216
|
+
setupExecSuccess('hello world');
|
|
217
|
+
|
|
218
|
+
const result = await tool.execute(
|
|
219
|
+
{ actions: [{ type: 'run-command', command: 'echo hello' }] },
|
|
220
|
+
context
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(result.success).toBe(true);
|
|
224
|
+
expect(result.actions[0].success).toBe(true);
|
|
225
|
+
expect(result.actions[0].stdout).toBe('hello world');
|
|
226
|
+
expect(result.actions[0].exitCode).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('execute run-command handles command error', async () => {
|
|
230
|
+
setupExecError('command not found', 127, '', 'bash: badcmd: command not found');
|
|
231
|
+
|
|
232
|
+
const result = await tool.execute(
|
|
233
|
+
{ actions: [{ type: 'run-command', command: 'badcmd' }] },
|
|
234
|
+
context
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// The tool resolves even on failure, just with success: false on the action
|
|
238
|
+
expect(result.actions[0].success).toBe(false);
|
|
239
|
+
expect(result.actions[0].exitCode).toBe(127);
|
|
240
|
+
expect(result.actions[0].error).toContain('command not found');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('execute run-command captures exit code', async () => {
|
|
244
|
+
setupExecError('exit 2', 2);
|
|
245
|
+
|
|
246
|
+
const result = await tool.execute(
|
|
247
|
+
{ actions: [{ type: 'run-command', command: 'exit 2' }] },
|
|
248
|
+
context
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(result.actions[0].exitCode).toBe(2);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('execute change-directory updates working dir', async () => {
|
|
255
|
+
mockFs.stat.mockResolvedValue({ isDirectory: () => true });
|
|
256
|
+
|
|
257
|
+
const result = await tool.execute(
|
|
258
|
+
{ actions: [{ type: 'change-directory', directory: 'src' }] },
|
|
259
|
+
context
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(result.success).toBe(true);
|
|
263
|
+
expect(result.actions[0].success).toBe(true);
|
|
264
|
+
expect(result.actions[0].action).toBe('change-directory');
|
|
265
|
+
expect(result.workingDirectory).toContain('src');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('execute get-working-directory returns current dir', async () => {
|
|
269
|
+
const result = await tool.execute(
|
|
270
|
+
{ actions: [{ type: 'get-working-directory' }] },
|
|
271
|
+
context
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(result.success).toBe(true);
|
|
275
|
+
expect(result.actions[0].action).toBe('get-working-directory');
|
|
276
|
+
expect(result.actions[0].workingDirectory).toBeTruthy();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('execute list-directory returns directory contents', async () => {
|
|
280
|
+
mockFs.readdir.mockResolvedValue([
|
|
281
|
+
{ name: 'file.js', isDirectory: () => false, isFile: () => true, isSymbolicLink: () => false },
|
|
282
|
+
{ name: 'src', isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false }
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
const result = await tool.execute(
|
|
286
|
+
{ actions: [{ type: 'list-directory', directory: '/project' }] },
|
|
287
|
+
context
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(result.success).toBe(true);
|
|
291
|
+
expect(result.actions[0].totalItems).toBe(2);
|
|
292
|
+
expect(result.actions[0].files).toBe(1);
|
|
293
|
+
expect(result.actions[0].directories).toBe(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('execute create-directory creates directory', async () => {
|
|
297
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
298
|
+
|
|
299
|
+
const result = await tool.execute(
|
|
300
|
+
{ actions: [{ type: 'create-directory', directory: 'new-dir' }] },
|
|
301
|
+
context
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(result.success).toBe(true);
|
|
305
|
+
expect(result.actions[0].success).toBe(true);
|
|
306
|
+
expect(result.actions[0].action).toBe('create-directory');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('execute handles multiple actions', async () => {
|
|
310
|
+
setupExecSuccess('ok');
|
|
311
|
+
|
|
312
|
+
const result = await tool.execute(
|
|
313
|
+
{
|
|
314
|
+
actions: [
|
|
315
|
+
{ type: 'get-working-directory' },
|
|
316
|
+
{ type: 'run-command', command: 'echo ok' }
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
context
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(result.executedActions).toBe(2);
|
|
323
|
+
expect(result.actions.length).toBe(2);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('execute reports overall failure when some actions fail', async () => {
|
|
327
|
+
setupExecError('fail', 1);
|
|
328
|
+
|
|
329
|
+
const result = await tool.execute(
|
|
330
|
+
{ actions: [{ type: 'run-command', command: 'fail-cmd' }] },
|
|
331
|
+
context
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(result.success).toBe(false);
|
|
335
|
+
expect(result.failedActions).toBe(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('execute handles action throwing error gracefully', async () => {
|
|
339
|
+
mockFs.readdir.mockRejectedValue(new Error('permission denied'));
|
|
340
|
+
|
|
341
|
+
const result = await tool.execute(
|
|
342
|
+
{ actions: [{ type: 'list-directory', directory: '/forbidden' }] },
|
|
343
|
+
context
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(result.actions[0].success).toBe(false);
|
|
347
|
+
expect(result.actions[0].error).toContain('permission denied');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('addToHistory records command and trims history', () => {
|
|
351
|
+
for (let i = 0; i < 110; i++) {
|
|
352
|
+
tool.addToHistory(
|
|
353
|
+
{ type: 'run-command', command: `cmd-${i}` },
|
|
354
|
+
{ success: true, executionTime: 10, workingDirectory: '/project' },
|
|
355
|
+
'agent-1'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
expect(tool.commandHistory.length).toBe(100);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('execute uses directoryAccess working directory', async () => {
|
|
362
|
+
setupExecSuccess('ok');
|
|
363
|
+
|
|
364
|
+
const result = await tool.execute(
|
|
365
|
+
{ actions: [{ type: 'run-command', command: 'ls' }] },
|
|
366
|
+
{
|
|
367
|
+
agentId: 'agent-1',
|
|
368
|
+
projectDir: '/project',
|
|
369
|
+
directoryAccess: {
|
|
370
|
+
workingDirectory: '/custom/dir',
|
|
371
|
+
writeEnabledDirectories: ['/custom/dir']
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(result.success).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('getParameterSchema returns valid schema', () => {
|
|
380
|
+
const schema = tool.getParameterSchema();
|
|
381
|
+
expect(schema.type).toBe('object');
|
|
382
|
+
expect(schema.properties).toHaveProperty('actions');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -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 prompt service
|
|
5
|
+
const mockPromptService = {
|
|
6
|
+
createPromptRequest: jest.fn(),
|
|
7
|
+
formatResponseAsMessage: jest.fn().mockReturnValue('Formatted response')
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
jest.unstable_mockModule('../../services/promptService.js', () => ({
|
|
11
|
+
getPromptService: jest.fn().mockReturnValue(mockPromptService)
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const { default: UserPromptTool } = await import('../userPromptTool.js');
|
|
15
|
+
|
|
16
|
+
describe('UserPromptTool', () => {
|
|
17
|
+
let tool;
|
|
18
|
+
let logger;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
logger = createMockLogger();
|
|
22
|
+
tool = new UserPromptTool({}, logger);
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('constructor', () => {
|
|
27
|
+
test('should set correct metadata', () => {
|
|
28
|
+
expect(tool.requiresProject).toBe(false);
|
|
29
|
+
expect(tool.isAsync).toBe(false);
|
|
30
|
+
expect(tool.timeout).toBe(300000); // 5 minutes
|
|
31
|
+
expect(tool.agentPool).toBeNull();
|
|
32
|
+
expect(tool.webSocketManager).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('getDescription', () => {
|
|
37
|
+
test('should return description with usage info', () => {
|
|
38
|
+
const desc = tool.getDescription();
|
|
39
|
+
expect(desc).toContain('User Prompt Tool');
|
|
40
|
+
expect(desc).toContain('questions');
|
|
41
|
+
expect(desc).toContain('options');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('parseParameters', () => {
|
|
46
|
+
test('should return content as-is', () => {
|
|
47
|
+
const result = tool.parseParameters('test');
|
|
48
|
+
expect(result).toBe('test');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('getRequiredParameters', () => {
|
|
53
|
+
test('should require questions', () => {
|
|
54
|
+
expect(tool.getRequiredParameters()).toContain('questions');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('getSupportedActions', () => {
|
|
59
|
+
test('should include prompt and ask actions', () => {
|
|
60
|
+
const actions = tool.getSupportedActions();
|
|
61
|
+
expect(actions).toContain('prompt');
|
|
62
|
+
expect(actions).toContain('ask');
|
|
63
|
+
expect(actions).toContain('question');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('validateParameterTypes', () => {
|
|
68
|
+
test('should accept valid params', () => {
|
|
69
|
+
const result = tool.validateParameterTypes({
|
|
70
|
+
message: 'Context',
|
|
71
|
+
questions: [{ message: 'Question?' }]
|
|
72
|
+
});
|
|
73
|
+
expect(result.valid).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should reject non-string message', () => {
|
|
77
|
+
const result = tool.validateParameterTypes({ message: 123 });
|
|
78
|
+
expect(result.valid).toBe(false);
|
|
79
|
+
expect(result.errors[0]).toContain('message must be a string');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should reject non-array questions', () => {
|
|
83
|
+
const result = tool.validateParameterTypes({ questions: 'not-array' });
|
|
84
|
+
expect(result.valid).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('customValidateParameters', () => {
|
|
89
|
+
test('should reject empty questions', () => {
|
|
90
|
+
const result = tool.customValidateParameters({ questions: [] });
|
|
91
|
+
expect(result.valid).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should reject more than 5 questions', () => {
|
|
95
|
+
const questions = Array(6).fill({ message: 'Q' });
|
|
96
|
+
const result = tool.customValidateParameters({ questions });
|
|
97
|
+
expect(result.valid).toBe(false);
|
|
98
|
+
expect(result.errors[0]).toContain('Maximum 5');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should reject question without message', () => {
|
|
102
|
+
const result = tool.customValidateParameters({ questions: [{ options: ['A'] }] });
|
|
103
|
+
expect(result.valid).toBe(false);
|
|
104
|
+
expect(result.errors[0]).toContain('message is required');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should reject long context message', () => {
|
|
108
|
+
const result = tool.customValidateParameters({
|
|
109
|
+
message: 'x'.repeat(501),
|
|
110
|
+
questions: [{ message: 'Q?' }]
|
|
111
|
+
});
|
|
112
|
+
expect(result.valid).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should accept valid params', () => {
|
|
116
|
+
const result = tool.customValidateParameters({
|
|
117
|
+
questions: [{ message: 'Question?' }]
|
|
118
|
+
});
|
|
119
|
+
expect(result.valid).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should accept question with "question" field instead of "message"', () => {
|
|
123
|
+
const result = tool.customValidateParameters({
|
|
124
|
+
questions: [{ question: 'Question?' }]
|
|
125
|
+
});
|
|
126
|
+
expect(result.valid).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('execute', () => {
|
|
131
|
+
test('should throw when agentId is missing', async () => {
|
|
132
|
+
await expect(tool.execute(
|
|
133
|
+
{ questions: [{ message: 'Q?' }] },
|
|
134
|
+
{}
|
|
135
|
+
)).rejects.toThrow('Agent ID is required');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should throw when webSocketManager is not set', async () => {
|
|
139
|
+
await expect(tool.execute(
|
|
140
|
+
{ questions: [{ message: 'Q?' }] },
|
|
141
|
+
{ agentId: 'agent-1' }
|
|
142
|
+
)).rejects.toThrow('WebSocket manager not available');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should send prompt and return response on success', async () => {
|
|
146
|
+
const mockWs = { broadcastToSession: jest.fn() };
|
|
147
|
+
const mockPool = {
|
|
148
|
+
getAgent: jest.fn().mockResolvedValue({ id: 'agent-1', mode: 'agent', awaitingUserInput: null }),
|
|
149
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
tool.setWebSocketManager(mockWs);
|
|
153
|
+
tool.setAgentPool(mockPool);
|
|
154
|
+
|
|
155
|
+
mockPromptService.createPromptRequest.mockReturnValue({
|
|
156
|
+
requestInfo: {
|
|
157
|
+
requestId: 'req-1',
|
|
158
|
+
message: 'Context',
|
|
159
|
+
questions: [{ message: 'Q?' }],
|
|
160
|
+
timeoutAt: new Date().toISOString()
|
|
161
|
+
},
|
|
162
|
+
promise: Promise.resolve({ response: { q1: 'Answer' } })
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const result = await tool.execute(
|
|
166
|
+
{ message: 'Context', questions: [{ message: 'Q?' }] },
|
|
167
|
+
{ agentId: 'agent-1', sessionId: 'sess-1' }
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(result.success).toBe(true);
|
|
171
|
+
expect(result.action).toBe('prompt');
|
|
172
|
+
expect(result.formattedResponse).toBe('Formatted response');
|
|
173
|
+
expect(mockWs.broadcastToSession).toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should handle timeout error', async () => {
|
|
177
|
+
const mockWs = { broadcastToSession: jest.fn() };
|
|
178
|
+
const mockPool = {
|
|
179
|
+
getAgent: jest.fn().mockResolvedValue({ id: 'agent-1', mode: 'agent' }),
|
|
180
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
tool.setWebSocketManager(mockWs);
|
|
184
|
+
tool.setAgentPool(mockPool);
|
|
185
|
+
|
|
186
|
+
mockPromptService.createPromptRequest.mockReturnValue({
|
|
187
|
+
requestInfo: {
|
|
188
|
+
requestId: 'req-1',
|
|
189
|
+
message: 'Context',
|
|
190
|
+
questions: [{ message: 'Q?' }],
|
|
191
|
+
timeoutAt: new Date().toISOString()
|
|
192
|
+
},
|
|
193
|
+
promise: Promise.reject(new Error('Request timed out'))
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await tool.execute(
|
|
197
|
+
{ questions: [{ message: 'Q?' }] },
|
|
198
|
+
{ agentId: 'agent-1', sessionId: 'sess-1' }
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(result.success).toBe(false);
|
|
202
|
+
expect(result.error.toLowerCase()).toMatch(/timed?\s*out|timeout|not respond/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('should handle cancellation', async () => {
|
|
206
|
+
const mockWs = { broadcastToSession: jest.fn() };
|
|
207
|
+
const mockPool = {
|
|
208
|
+
getAgent: jest.fn().mockResolvedValue({ id: 'agent-1', mode: 'agent' }),
|
|
209
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
tool.setWebSocketManager(mockWs);
|
|
213
|
+
tool.setAgentPool(mockPool);
|
|
214
|
+
|
|
215
|
+
mockPromptService.createPromptRequest.mockReturnValue({
|
|
216
|
+
requestInfo: {
|
|
217
|
+
requestId: 'req-1',
|
|
218
|
+
message: null,
|
|
219
|
+
questions: [{ message: 'Q?' }],
|
|
220
|
+
timeoutAt: new Date().toISOString()
|
|
221
|
+
},
|
|
222
|
+
promise: Promise.reject(new Error('Request cancelled'))
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await tool.execute(
|
|
226
|
+
{ questions: [{ message: 'Q?' }] },
|
|
227
|
+
{ agentId: 'agent-1', sessionId: 'sess-1' }
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(result.success).toBe(false);
|
|
231
|
+
expect(result.error).toContain('cancelled');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('should rethrow unexpected errors', async () => {
|
|
235
|
+
const mockWs = { broadcastToSession: jest.fn() };
|
|
236
|
+
const mockPool = {
|
|
237
|
+
getAgent: jest.fn().mockResolvedValue({ id: 'agent-1', mode: 'agent' }),
|
|
238
|
+
persistAgentState: jest.fn().mockResolvedValue(undefined)
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
tool.setWebSocketManager(mockWs);
|
|
242
|
+
tool.setAgentPool(mockPool);
|
|
243
|
+
|
|
244
|
+
mockPromptService.createPromptRequest.mockReturnValue({
|
|
245
|
+
requestInfo: {
|
|
246
|
+
requestId: 'req-1',
|
|
247
|
+
message: null,
|
|
248
|
+
questions: [],
|
|
249
|
+
timeoutAt: new Date().toISOString()
|
|
250
|
+
},
|
|
251
|
+
promise: Promise.reject(new Error('Unexpected failure'))
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await expect(tool.execute(
|
|
255
|
+
{ questions: [{ message: 'Q?' }] },
|
|
256
|
+
{ agentId: 'agent-1', sessionId: 'sess-1' }
|
|
257
|
+
)).rejects.toThrow('Unexpected failure');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('setAgentPool', () => {
|
|
262
|
+
test('should store agent pool', () => {
|
|
263
|
+
const pool = { getAgent: jest.fn() };
|
|
264
|
+
tool.setAgentPool(pool);
|
|
265
|
+
expect(tool.agentPool).toBe(pool);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('setWebSocketManager', () => {
|
|
270
|
+
test('should enable tool when ws manager is provided', () => {
|
|
271
|
+
tool.setWebSocketManager({ broadcastToSession: jest.fn() });
|
|
272
|
+
expect(tool.isEnabled).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should disable tool when ws manager is null', () => {
|
|
276
|
+
tool.setWebSocketManager(null);
|
|
277
|
+
expect(tool.isEnabled).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('getCapabilities', () => {
|
|
282
|
+
test('should include pausesAgent and requiresUI', () => {
|
|
283
|
+
const caps = tool.getCapabilities();
|
|
284
|
+
expect(caps.pausesAgent).toBe(true);
|
|
285
|
+
expect(caps.requiresUI).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('getParameterSchema', () => {
|
|
290
|
+
test('should return schema with questions', () => {
|
|
291
|
+
const schema = tool.getParameterSchema();
|
|
292
|
+
expect(schema.required).toContain('questions');
|
|
293
|
+
expect(schema.properties.questions).toBeDefined();
|
|
294
|
+
expect(schema.properties.message).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|