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,509 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
|
|
3
|
+
|
|
4
|
+
// ── Mock fs/promises ──────────────────────────────────────────────
|
|
5
|
+
const fsMock = {
|
|
6
|
+
readFile: jest.fn(),
|
|
7
|
+
writeFile: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
stat: jest.fn().mockResolvedValue({ size: 500, mtime: new Date() }),
|
|
9
|
+
access: jest.fn().mockResolvedValue(undefined),
|
|
10
|
+
readdir: jest.fn().mockResolvedValue([]),
|
|
11
|
+
mkdir: jest.fn().mockResolvedValue(undefined)
|
|
12
|
+
};
|
|
13
|
+
jest.unstable_mockModule('fs/promises', () => ({ default: fsMock, ...fsMock }));
|
|
14
|
+
|
|
15
|
+
// ── Mock crypto ───────────────────────────────────────────────────
|
|
16
|
+
jest.unstable_mockModule('crypto', () => ({
|
|
17
|
+
default: {
|
|
18
|
+
createHash: jest.fn(() => ({
|
|
19
|
+
update: jest.fn().mockReturnThis(),
|
|
20
|
+
digest: jest.fn().mockReturnValue('abcdef0123456789abcdef')
|
|
21
|
+
}))
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// ── Mock TagParser ────────────────────────────────────────────────
|
|
26
|
+
jest.unstable_mockModule('../../utilities/tagParser.js', () => ({
|
|
27
|
+
default: class MockTagParser {
|
|
28
|
+
static extractTagsWithAttributes() { return []; }
|
|
29
|
+
parseAttributes(str) {
|
|
30
|
+
const attrs = {};
|
|
31
|
+
const matches = str.matchAll(/([\w-]+)=["']([^"']+)["']/g);
|
|
32
|
+
for (const m of matches) attrs[m[1]] = m[2];
|
|
33
|
+
return attrs;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// ── Mock DirectoryAccessManager ───────────────────────────────────
|
|
39
|
+
jest.unstable_mockModule('../../utilities/directoryAccessManager.js', () => ({
|
|
40
|
+
default: class MockDAM {
|
|
41
|
+
constructor() {}
|
|
42
|
+
createDirectoryAccess(cfg) { return cfg; }
|
|
43
|
+
getWorkingDirectory(cfg) { return cfg?.workingDirectory || '/tmp/test'; }
|
|
44
|
+
validateReadAccess() { return { allowed: true }; }
|
|
45
|
+
validateWriteAccess() { return { allowed: true }; }
|
|
46
|
+
createRelativePath(p) { return p.replace(/^\/tmp\/test\/?/, ''); }
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// ── Mock structuredFileValidator ──────────────────────────────────
|
|
51
|
+
jest.unstable_mockModule('../../utilities/structuredFileValidator.js', () => ({
|
|
52
|
+
validateContent: jest.fn().mockReturnValue({ valid: true, errors: [] }),
|
|
53
|
+
validateStructuredFile: jest.fn().mockReturnValue({ valid: true, errors: [] }),
|
|
54
|
+
detectFormat: jest.fn().mockReturnValue('json'),
|
|
55
|
+
getSupportedFormats: jest.fn().mockReturnValue(['json', 'yaml', 'xml'])
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// ── Mock constants ────────────────────────────────────────────────
|
|
59
|
+
jest.unstable_mockModule('../../utilities/constants.js', () => ({
|
|
60
|
+
STATIC_ANALYSIS: {
|
|
61
|
+
ANALYSIS_TIMEOUT: 30000,
|
|
62
|
+
MAX_FILE_SIZE_FOR_ANALYSIS: 5 * 1024 * 1024,
|
|
63
|
+
MAX_FILES_PER_BATCH: 50,
|
|
64
|
+
ENABLE_CACHE: true,
|
|
65
|
+
CACHE_DURATION: 300000,
|
|
66
|
+
SEVERITY: { CRITICAL: 'critical', ERROR: 'error', WARNING: 'warning', INFO: 'info', SUGGESTION: 'suggestion' },
|
|
67
|
+
EXTENSION_TO_LANGUAGE: {
|
|
68
|
+
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
69
|
+
'.ts': 'typescript', '.tsx': 'typescript',
|
|
70
|
+
'.py': 'python',
|
|
71
|
+
'.css': 'css', '.scss': 'scss', '.less': 'less'
|
|
72
|
+
},
|
|
73
|
+
LANGUAGE: {
|
|
74
|
+
JAVASCRIPT: 'javascript', TYPESCRIPT: 'typescript', PYTHON: 'python',
|
|
75
|
+
CSS: 'css', SCSS: 'scss', LESS: 'less'
|
|
76
|
+
},
|
|
77
|
+
FRAMEWORK_MANIFESTS: {
|
|
78
|
+
JAVASCRIPT: 'package.json',
|
|
79
|
+
PYTHON: 'requirements.txt',
|
|
80
|
+
PYTHON_POETRY: 'pyproject.toml'
|
|
81
|
+
},
|
|
82
|
+
JS_FRAMEWORKS: { REACT: 'react', VUE: 'vue', ANGULAR: '@angular/core' },
|
|
83
|
+
PYTHON_FRAMEWORKS: { DJANGO: 'django', FLASK: 'flask', FASTAPI: 'fastapi' }
|
|
84
|
+
},
|
|
85
|
+
TOOL_STATUS: { SUCCESS: 'success', ERROR: 'error' },
|
|
86
|
+
SYSTEM_DEFAULTS: { MAX_TOOL_EXECUTION_TIME: 30000 }
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// ── Mock BaseTool ─────────────────────────────────────────────────
|
|
90
|
+
jest.unstable_mockModule('../baseTool.js', () => ({
|
|
91
|
+
BaseTool: class {
|
|
92
|
+
constructor() {
|
|
93
|
+
this.id = 'staticanalysis';
|
|
94
|
+
this.config = {};
|
|
95
|
+
this.logger = null;
|
|
96
|
+
this.requiresProject = false;
|
|
97
|
+
this.isAsync = false;
|
|
98
|
+
this.timeout = 30000;
|
|
99
|
+
this.maxConcurrentOperations = 1;
|
|
100
|
+
this.builtinDelay = 0;
|
|
101
|
+
this.activeOperations = new Map();
|
|
102
|
+
this.operationHistory = [];
|
|
103
|
+
this.isEnabled = true;
|
|
104
|
+
this.lastUsed = null;
|
|
105
|
+
this.usageCount = 0;
|
|
106
|
+
}
|
|
107
|
+
getDescription() { return ''; }
|
|
108
|
+
getSummary() { return ''; }
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const { default: StaticAnalysisTool } = await import('../staticAnalysisTool.js');
|
|
113
|
+
|
|
114
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
115
|
+
function createTestSetup() {
|
|
116
|
+
const logger = createMockLogger();
|
|
117
|
+
const tool = new StaticAnalysisTool({}, logger);
|
|
118
|
+
tool.logger = logger;
|
|
119
|
+
|
|
120
|
+
const context = {
|
|
121
|
+
projectDir: '/tmp/test',
|
|
122
|
+
agentId: 'test-agent',
|
|
123
|
+
directoryAccess: {
|
|
124
|
+
workingDirectory: '/tmp/test',
|
|
125
|
+
writeEnabledDirectories: ['/tmp/test'],
|
|
126
|
+
restrictToProject: true
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return { tool, context, logger };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
jest.clearAllMocks();
|
|
135
|
+
fsMock.stat.mockResolvedValue({ size: 500, mtime: new Date() });
|
|
136
|
+
fsMock.readFile.mockResolvedValue('const x = 1;');
|
|
137
|
+
fsMock.readdir.mockResolvedValue([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('StaticAnalysisTool', () => {
|
|
141
|
+
// ── constructor ─────────────────────────────────────────────────
|
|
142
|
+
describe('constructor', () => {
|
|
143
|
+
test('initializes with defaults', () => {
|
|
144
|
+
const tool = new StaticAnalysisTool({});
|
|
145
|
+
expect(tool.requiresProject).toBe(true);
|
|
146
|
+
expect(tool.analysisCache).toBeInstanceOf(Map);
|
|
147
|
+
expect(tool.metrics.totalAnalyses).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('accepts custom config', () => {
|
|
151
|
+
const tool = new StaticAnalysisTool({ maxFilesPerBatch: 10 });
|
|
152
|
+
expect(tool.maxFilesPerBatch).toBe(10);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── getDescription ──────────────────────────────────────────────
|
|
157
|
+
describe('getDescription', () => {
|
|
158
|
+
test('returns description mentioning supported languages', () => {
|
|
159
|
+
const { tool } = createTestSetup();
|
|
160
|
+
const desc = tool.getDescription();
|
|
161
|
+
expect(desc).toContain('JavaScript');
|
|
162
|
+
expect(desc).toContain('TypeScript');
|
|
163
|
+
expect(desc).toContain('Python');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── parseParameters ─────────────────────────────────────────────
|
|
168
|
+
describe('parseParameters', () => {
|
|
169
|
+
test('returns params object with actions array', () => {
|
|
170
|
+
const { tool } = createTestSetup();
|
|
171
|
+
const result = tool.parseParameters('<analyze file-path="src/index.js" />');
|
|
172
|
+
expect(result.actions).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── customValidateParameters ────────────────────────────────────
|
|
177
|
+
describe('customValidateParameters', () => {
|
|
178
|
+
test('valid with correct analyze action', () => {
|
|
179
|
+
const { tool } = createTestSetup();
|
|
180
|
+
const result = tool.customValidateParameters({
|
|
181
|
+
actions: [{ type: 'analyze', filePath: 'src/index.js' }]
|
|
182
|
+
});
|
|
183
|
+
expect(result.valid).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('invalid when actions is empty', () => {
|
|
187
|
+
const { tool } = createTestSetup();
|
|
188
|
+
const result = tool.customValidateParameters({ actions: [] });
|
|
189
|
+
expect(result.valid).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('invalid when filePath missing for analyze', () => {
|
|
193
|
+
const { tool } = createTestSetup();
|
|
194
|
+
const result = tool.customValidateParameters({
|
|
195
|
+
actions: [{ type: 'analyze' }]
|
|
196
|
+
});
|
|
197
|
+
expect(result.valid).toBe(false);
|
|
198
|
+
expect(result.errors[0]).toContain('file-path is required');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('invalid when directory missing for analyze-project', () => {
|
|
202
|
+
const { tool } = createTestSetup();
|
|
203
|
+
const result = tool.customValidateParameters({
|
|
204
|
+
actions: [{ type: 'analyze-project' }]
|
|
205
|
+
});
|
|
206
|
+
expect(result.valid).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('invalid for unknown action type', () => {
|
|
210
|
+
const { tool } = createTestSetup();
|
|
211
|
+
const result = tool.customValidateParameters({
|
|
212
|
+
actions: [{ type: 'fly-to-moon' }]
|
|
213
|
+
});
|
|
214
|
+
expect(result.valid).toBe(false);
|
|
215
|
+
expect(result.errors[0]).toContain('unknown action type');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('validates security-scan requires filePath', () => {
|
|
219
|
+
const { tool } = createTestSetup();
|
|
220
|
+
const result = tool.customValidateParameters({
|
|
221
|
+
actions: [{ type: 'security-scan' }]
|
|
222
|
+
});
|
|
223
|
+
expect(result.valid).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('validates security-scan-project requires directory', () => {
|
|
227
|
+
const { tool } = createTestSetup();
|
|
228
|
+
const result = tool.customValidateParameters({
|
|
229
|
+
actions: [{ type: 'security-scan-project' }]
|
|
230
|
+
});
|
|
231
|
+
expect(result.valid).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('validates validate-config requires filePath', () => {
|
|
235
|
+
const { tool } = createTestSetup();
|
|
236
|
+
const result = tool.customValidateParameters({
|
|
237
|
+
actions: [{ type: 'validate-config' }]
|
|
238
|
+
});
|
|
239
|
+
expect(result.valid).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('validates validate-structured accepts content without filePath', () => {
|
|
243
|
+
const { tool } = createTestSetup();
|
|
244
|
+
const result = tool.customValidateParameters({
|
|
245
|
+
actions: [{ type: 'validate-structured', content: '{"a":1}' }]
|
|
246
|
+
});
|
|
247
|
+
expect(result.valid).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('validates validate-structured rejects both missing', () => {
|
|
251
|
+
const { tool } = createTestSetup();
|
|
252
|
+
const result = tool.customValidateParameters({
|
|
253
|
+
actions: [{ type: 'validate-structured' }]
|
|
254
|
+
});
|
|
255
|
+
expect(result.valid).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('validates batch size limit', () => {
|
|
259
|
+
const { tool } = createTestSetup();
|
|
260
|
+
tool.maxFilesPerBatch = 2;
|
|
261
|
+
const result = tool.customValidateParameters({
|
|
262
|
+
actions: [
|
|
263
|
+
{ type: 'analyze', filePath: 'a.js' },
|
|
264
|
+
{ type: 'analyze', filePath: 'b.js' },
|
|
265
|
+
{ type: 'analyze', filePath: 'c.js' }
|
|
266
|
+
]
|
|
267
|
+
});
|
|
268
|
+
expect(result.valid).toBe(false);
|
|
269
|
+
expect(result.errors.some(e => e.includes('Too many actions'))).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('validates fix requires filePath', () => {
|
|
273
|
+
const { tool } = createTestSetup();
|
|
274
|
+
const result = tool.customValidateParameters({
|
|
275
|
+
actions: [{ type: 'fix' }]
|
|
276
|
+
});
|
|
277
|
+
expect(result.valid).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('validates format requires filePath', () => {
|
|
281
|
+
const { tool } = createTestSetup();
|
|
282
|
+
const result = tool.customValidateParameters({
|
|
283
|
+
actions: [{ type: 'format' }]
|
|
284
|
+
});
|
|
285
|
+
expect(result.valid).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ── execute - analyze ───────────────────────────────────────────
|
|
290
|
+
describe('execute - analyze', () => {
|
|
291
|
+
test('analyzes a JavaScript file with mock analyzer', async () => {
|
|
292
|
+
const { tool, context } = createTestSetup();
|
|
293
|
+
const mockAnalyzer = {
|
|
294
|
+
analyze: jest.fn().mockResolvedValue([
|
|
295
|
+
{ severity: 'error', message: 'unused var', line: 1, column: 5 }
|
|
296
|
+
])
|
|
297
|
+
};
|
|
298
|
+
tool.getAnalyzer = jest.fn().mockResolvedValue(mockAnalyzer);
|
|
299
|
+
tool.detectFramework = jest.fn().mockResolvedValue(null);
|
|
300
|
+
|
|
301
|
+
const result = await tool.execute({
|
|
302
|
+
actions: [{ type: 'analyze', filePath: 'src/index.js' }]
|
|
303
|
+
}, context);
|
|
304
|
+
|
|
305
|
+
expect(result.success).toBe(true);
|
|
306
|
+
expect(result.results.files).toHaveLength(1);
|
|
307
|
+
expect(result.results.files[0].errors).toHaveLength(1);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('returns skipped for unsupported file type', async () => {
|
|
311
|
+
const { tool, context } = createTestSetup();
|
|
312
|
+
// .xyz has no language mapping
|
|
313
|
+
const result = await tool.execute({
|
|
314
|
+
actions: [{ type: 'analyze', filePath: 'file.xyz' }]
|
|
315
|
+
}, context);
|
|
316
|
+
|
|
317
|
+
expect(result.success).toBe(true);
|
|
318
|
+
expect(result.results.files[0].skipped).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('returns skipped when no analyzer available', async () => {
|
|
322
|
+
const { tool, context } = createTestSetup();
|
|
323
|
+
tool.getAnalyzer = jest.fn().mockResolvedValue(null);
|
|
324
|
+
tool.detectFramework = jest.fn().mockResolvedValue(null);
|
|
325
|
+
|
|
326
|
+
const result = await tool.execute({
|
|
327
|
+
actions: [{ type: 'analyze', filePath: 'src/app.js' }]
|
|
328
|
+
}, context);
|
|
329
|
+
|
|
330
|
+
expect(result.results.files[0].skipped).toBe(true);
|
|
331
|
+
expect(result.results.files[0].skipReason).toContain('No analyzer');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('handles file too large error', async () => {
|
|
335
|
+
const { tool, context } = createTestSetup();
|
|
336
|
+
fsMock.stat.mockResolvedValueOnce({ size: 999999999, mtime: new Date() });
|
|
337
|
+
|
|
338
|
+
const result = await tool.execute({
|
|
339
|
+
actions: [{ type: 'analyze', filePath: 'huge.js' }]
|
|
340
|
+
}, context);
|
|
341
|
+
|
|
342
|
+
expect(result.results.files[0].error).toContain('too large');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ── execute - analyze-project ───────────────────────────────────
|
|
347
|
+
describe('execute - analyze-project', () => {
|
|
348
|
+
test('analyzes project directory', async () => {
|
|
349
|
+
const { tool, context } = createTestSetup();
|
|
350
|
+
// findFiles returns file list
|
|
351
|
+
tool.findFiles = jest.fn().mockResolvedValue(['/tmp/test/src/a.js']);
|
|
352
|
+
tool.analyzeFile = jest.fn().mockResolvedValue({
|
|
353
|
+
file: 'src/a.js',
|
|
354
|
+
language: 'javascript',
|
|
355
|
+
errors: [], warnings: [], info: [],
|
|
356
|
+
analyzed: true, totalIssues: 0
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const result = await tool.execute({
|
|
360
|
+
actions: [{ type: 'analyze-project', directory: 'src', pattern: '**/*.js' }]
|
|
361
|
+
}, context);
|
|
362
|
+
|
|
363
|
+
expect(result.success).toBe(true);
|
|
364
|
+
expect(result.results.files.length).toBeGreaterThanOrEqual(0);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ── execute - unknown action ────────────────────────────────────
|
|
369
|
+
describe('execute - unknown action', () => {
|
|
370
|
+
test('adds error result for unknown action type', async () => {
|
|
371
|
+
const { tool, context } = createTestSetup();
|
|
372
|
+
const result = await tool.execute({
|
|
373
|
+
actions: [{ type: 'teleport', filePath: 'x.js' }]
|
|
374
|
+
}, context);
|
|
375
|
+
|
|
376
|
+
expect(result.success).toBe(true); // overall success is always true
|
|
377
|
+
expect(result.results.files[0].error).toContain('Unknown action type');
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ── detectLanguage ──────────────────────────────────────────────
|
|
382
|
+
describe('detectLanguage', () => {
|
|
383
|
+
test('detects JavaScript from .js extension', () => {
|
|
384
|
+
const { tool } = createTestSetup();
|
|
385
|
+
expect(tool.detectLanguage('/path/to/file.js')).toBe('javascript');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('detects TypeScript from .ts extension', () => {
|
|
389
|
+
const { tool } = createTestSetup();
|
|
390
|
+
expect(tool.detectLanguage('/path/to/file.ts')).toBe('typescript');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('detects Python from .py extension', () => {
|
|
394
|
+
const { tool } = createTestSetup();
|
|
395
|
+
expect(tool.detectLanguage('/path/to/file.py')).toBe('python');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('detects CSS from .css extension', () => {
|
|
399
|
+
const { tool } = createTestSetup();
|
|
400
|
+
expect(tool.detectLanguage('/path/to/style.css')).toBe('css');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('returns null for unsupported extension', () => {
|
|
404
|
+
const { tool } = createTestSetup();
|
|
405
|
+
expect(tool.detectLanguage('/path/to/file.xyz')).toBeNull();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ── updateSummary ───────────────────────────────────────────────
|
|
410
|
+
describe('updateSummary', () => {
|
|
411
|
+
test('updates summary counts from file result', () => {
|
|
412
|
+
const { tool } = createTestSetup();
|
|
413
|
+
const summary = {
|
|
414
|
+
totalFiles: 0, totalErrors: 0, totalWarnings: 0, totalInfo: 0,
|
|
415
|
+
errorsByCategory: {}, filesByLanguage: {}, filesWithErrors: 0
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
tool.updateSummary(summary, {
|
|
419
|
+
analyzed: true,
|
|
420
|
+
language: 'javascript',
|
|
421
|
+
errors: [{ severity: 'error', category: 'syntax' }],
|
|
422
|
+
warnings: [{ severity: 'warning' }],
|
|
423
|
+
info: []
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
expect(summary.totalFiles).toBe(1);
|
|
427
|
+
expect(summary.totalErrors).toBe(1);
|
|
428
|
+
expect(summary.totalWarnings).toBe(1);
|
|
429
|
+
expect(summary.filesWithErrors).toBe(1);
|
|
430
|
+
expect(summary.filesByLanguage.javascript).toBe(1);
|
|
431
|
+
expect(summary.errorsByCategory.syntax).toBe(1);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('skips non-analyzed results', () => {
|
|
435
|
+
const { tool } = createTestSetup();
|
|
436
|
+
const summary = {
|
|
437
|
+
totalFiles: 0, totalErrors: 0, totalWarnings: 0, totalInfo: 0,
|
|
438
|
+
errorsByCategory: {}, filesByLanguage: {}, filesWithErrors: 0
|
|
439
|
+
};
|
|
440
|
+
tool.updateSummary(summary, { analyzed: false });
|
|
441
|
+
expect(summary.totalFiles).toBe(0);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ── getPerformanceMetrics ───────────────────────────────────────
|
|
446
|
+
describe('getPerformanceMetrics', () => {
|
|
447
|
+
test('returns metrics with cache hit rate', () => {
|
|
448
|
+
const { tool } = createTestSetup();
|
|
449
|
+
tool.metrics.totalAnalyses = 10;
|
|
450
|
+
tool.metrics.cacheHits = 3;
|
|
451
|
+
tool.metrics.filesAnalyzed = 7;
|
|
452
|
+
tool.metrics.totalAnalysisTime = 1400;
|
|
453
|
+
|
|
454
|
+
const metrics = tool.getPerformanceMetrics();
|
|
455
|
+
expect(metrics.cacheHitRate).toBe(30);
|
|
456
|
+
expect(metrics.averageAnalysisTime).toBe(200);
|
|
457
|
+
expect(metrics.cacheSize).toBe(0);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('handles zero analyses', () => {
|
|
461
|
+
const { tool } = createTestSetup();
|
|
462
|
+
const metrics = tool.getPerformanceMetrics();
|
|
463
|
+
expect(metrics.cacheHitRate).toBe(0);
|
|
464
|
+
expect(metrics.averageAnalysisTime).toBe(0);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// ── computeContentHash ──────────────────────────────────────────
|
|
469
|
+
describe('computeContentHash', () => {
|
|
470
|
+
test('returns truncated hash string', () => {
|
|
471
|
+
const { tool } = createTestSetup();
|
|
472
|
+
const hash = tool.computeContentHash('some content');
|
|
473
|
+
expect(typeof hash).toBe('string');
|
|
474
|
+
expect(hash.length).toBe(16);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ── execute returns performance metrics ─────────────────────────
|
|
479
|
+
describe('execute - result structure', () => {
|
|
480
|
+
test('result includes performance and toolUsed', async () => {
|
|
481
|
+
const { tool, context } = createTestSetup();
|
|
482
|
+
const result = await tool.execute({
|
|
483
|
+
actions: [{ type: 'analyze', filePath: 'test.xyz' }]
|
|
484
|
+
}, context);
|
|
485
|
+
expect(result.toolUsed).toBe('staticanalysis');
|
|
486
|
+
expect(result.performance).toBeDefined();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// ── cache behavior ──────────────────────────────────────────────
|
|
491
|
+
describe('caching', () => {
|
|
492
|
+
test('uses cached result on second analysis of same file', async () => {
|
|
493
|
+
const { tool, context } = createTestSetup();
|
|
494
|
+
const mockAnalyzer = {
|
|
495
|
+
analyze: jest.fn().mockResolvedValue([])
|
|
496
|
+
};
|
|
497
|
+
tool.getAnalyzer = jest.fn().mockResolvedValue(mockAnalyzer);
|
|
498
|
+
tool.detectFramework = jest.fn().mockResolvedValue(null);
|
|
499
|
+
|
|
500
|
+
// First call - should analyze
|
|
501
|
+
await tool.execute({ actions: [{ type: 'analyze', filePath: 'src/cached.js' }] }, context);
|
|
502
|
+
expect(mockAnalyzer.analyze).toHaveBeenCalledTimes(1);
|
|
503
|
+
|
|
504
|
+
// Second call - same content hash should use cache
|
|
505
|
+
await tool.execute({ actions: [{ type: 'analyze', filePath: 'src/cached.js' }] }, context);
|
|
506
|
+
expect(tool.metrics.cacheHits).toBeGreaterThanOrEqual(1);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|