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.
Files changed (84) hide show
  1. package/package.json +1 -1
  2. package/src/__test-utils__/fixtures/malformedJson.js +31 -0
  3. package/src/__test-utils__/globalSetup.js +9 -0
  4. package/src/__test-utils__/globalTeardown.js +12 -0
  5. package/src/__test-utils__/mockFactories.js +101 -0
  6. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
  7. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
  8. package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
  9. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
  10. package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
  11. package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
  12. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
  13. package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
  14. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
  15. package/src/core/__tests__/agentPool.test.js +601 -0
  16. package/src/core/__tests__/agentScheduler.test.js +576 -0
  17. package/src/core/__tests__/contextManager.test.js +252 -0
  18. package/src/core/__tests__/flowExecutor.test.js +262 -0
  19. package/src/core/__tests__/messageProcessor.test.js +627 -0
  20. package/src/core/__tests__/orchestrator.test.js +257 -0
  21. package/src/core/__tests__/stateManager.test.js +375 -0
  22. package/src/core/agentPool.js +11 -1
  23. package/src/index.js +25 -9
  24. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
  25. package/src/services/__tests__/agentActivityService.test.js +319 -0
  26. package/src/services/__tests__/apiKeyManager.test.js +206 -0
  27. package/src/services/__tests__/benchmarkService.test.js +184 -0
  28. package/src/services/__tests__/budgetService.test.js +211 -0
  29. package/src/services/__tests__/contextInjectionService.test.js +205 -0
  30. package/src/services/__tests__/conversationCompactionService.test.js +280 -0
  31. package/src/services/__tests__/credentialVault.test.js +469 -0
  32. package/src/services/__tests__/errorHandler.test.js +314 -0
  33. package/src/services/__tests__/fileAttachmentService.test.js +278 -0
  34. package/src/services/__tests__/flowContextService.test.js +199 -0
  35. package/src/services/__tests__/memoryService.test.js +450 -0
  36. package/src/services/__tests__/modelRouterService.test.js +388 -0
  37. package/src/services/__tests__/modelsService.test.js +261 -0
  38. package/src/services/__tests__/portRegistry.test.js +123 -0
  39. package/src/services/__tests__/projectDetector.test.js +34 -0
  40. package/src/services/__tests__/promptService.test.js +242 -0
  41. package/src/services/__tests__/qualityInspector.test.js +97 -0
  42. package/src/services/__tests__/scheduleService.test.js +308 -0
  43. package/src/services/__tests__/serviceRegistry.test.js +74 -0
  44. package/src/services/__tests__/skillsService.test.js +402 -0
  45. package/src/services/__tests__/tokenCountingService.test.js +48 -0
  46. package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
  47. package/src/tools/__tests__/agentDelayTool.test.js +342 -0
  48. package/src/tools/__tests__/asyncToolManager.test.js +344 -0
  49. package/src/tools/__tests__/baseTool.test.js +420 -0
  50. package/src/tools/__tests__/codeMapTool.test.js +348 -0
  51. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
  52. package/src/tools/__tests__/fileSystemTool.test.js +717 -0
  53. package/src/tools/__tests__/fileTreeTool.test.js +274 -0
  54. package/src/tools/__tests__/helpTool.test.js +204 -0
  55. package/src/tools/__tests__/jobDoneTool.test.js +296 -0
  56. package/src/tools/__tests__/memoryTool.test.js +297 -0
  57. package/src/tools/__tests__/seekTool.test.js +282 -0
  58. package/src/tools/__tests__/skillsTool.test.js +226 -0
  59. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
  60. package/src/tools/__tests__/taskManagerTool.test.js +725 -0
  61. package/src/tools/__tests__/terminalTool.test.js +384 -0
  62. package/src/tools/__tests__/userPromptTool.test.js +297 -0
  63. package/src/tools/__tests__/webTool.e2e.test.js +25 -11
  64. package/src/tools/webTool.js +6 -12
  65. package/src/types/__tests__/agent.test.js +499 -0
  66. package/src/types/__tests__/contextReference.test.js +606 -0
  67. package/src/types/__tests__/conversation.test.js +555 -0
  68. package/src/types/__tests__/toolCommand.test.js +584 -0
  69. package/src/types/contextReference.js +1 -1
  70. package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
  71. package/src/utilities/__tests__/configManager.test.js +397 -0
  72. package/src/utilities/__tests__/constants.test.js +49 -0
  73. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
  74. package/src/utilities/__tests__/fileProcessor.test.js +104 -0
  75. package/src/utilities/__tests__/jsonRepair.test.js +104 -0
  76. package/src/utilities/__tests__/logger.test.js +129 -0
  77. package/src/utilities/__tests__/platformUtils.test.js +87 -0
  78. package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
  79. package/src/utilities/__tests__/tagParser.test.js +887 -0
  80. package/src/utilities/__tests__/toolConstants.test.js +94 -0
  81. package/src/utilities/tagParser.js +2 -2
  82. package/src/tools/browserTool.js +0 -897
  83. package/src/utilities/platformUtils.test.js +0 -98
  84. /package/src/tools/{filesystemTool.js → fileSystemTool.js} +0 -0
@@ -0,0 +1,208 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock child_process spawn
5
+ const mockSpawn = jest.fn();
6
+ jest.unstable_mockModule('child_process', () => ({
7
+ spawn: mockSpawn
8
+ }));
9
+
10
+ // Mock fs/promises
11
+ const mockWriteFile = jest.fn().mockResolvedValue(undefined);
12
+ const mockUnlink = jest.fn().mockResolvedValue(undefined);
13
+ jest.unstable_mockModule('fs/promises', () => ({
14
+ default: { writeFile: mockWriteFile, unlink: mockUnlink },
15
+ writeFile: mockWriteFile,
16
+ unlink: mockUnlink
17
+ }));
18
+
19
+ const { default: PythonAnalyzer } = await import('../PythonAnalyzer.js');
20
+
21
+ // Helper to create a mock process
22
+ function createMockProcess(stdout = '', stderr = '', exitCode = 0, error = null) {
23
+ const stdoutStream = {
24
+ on: jest.fn((event, cb) => {
25
+ if (event === 'data' && stdout) {
26
+ setTimeout(() => cb(Buffer.from(stdout)), 5);
27
+ }
28
+ })
29
+ };
30
+ const stderrStream = {
31
+ on: jest.fn((event, cb) => {
32
+ if (event === 'data' && stderr) {
33
+ setTimeout(() => cb(Buffer.from(stderr)), 5);
34
+ }
35
+ })
36
+ };
37
+
38
+ const proc = {
39
+ stdout: stdoutStream,
40
+ stderr: stderrStream,
41
+ on: jest.fn((event, cb) => {
42
+ if (event === 'close') {
43
+ setTimeout(() => cb(exitCode), 10);
44
+ }
45
+ if (event === 'error' && error) {
46
+ setTimeout(() => cb(error), 5);
47
+ }
48
+ }),
49
+ kill: jest.fn()
50
+ };
51
+
52
+ return proc;
53
+ }
54
+
55
+ describe('PythonAnalyzer', () => {
56
+ let analyzer;
57
+ let logger;
58
+
59
+ beforeEach(() => {
60
+ logger = createMockLogger();
61
+ analyzer = new PythonAnalyzer(logger);
62
+ analyzer.pythonCommand = null;
63
+ jest.clearAllMocks();
64
+ });
65
+
66
+ // ── Constructor ──
67
+ test('constructor initializes with defaults', () => {
68
+ expect(analyzer.logger).toBe(logger);
69
+ expect(analyzer.pythonCommand).toBeNull();
70
+ });
71
+
72
+ test('constructor works without logger', () => {
73
+ const a = new PythonAnalyzer();
74
+ expect(a.logger).toBeNull();
75
+ });
76
+
77
+ // ── getSupportedExtensions ──
78
+ test('getSupportedExtensions returns .py', () => {
79
+ expect(analyzer.getSupportedExtensions()).toEqual(['.py']);
80
+ });
81
+
82
+ // ── supportsAutoFix ──
83
+ test('supportsAutoFix returns false', () => {
84
+ expect(analyzer.supportsAutoFix()).toBe(false);
85
+ });
86
+
87
+ // ── getPythonCommand ──
88
+ test('getPythonCommand returns cached command on second call', async () => {
89
+ analyzer.pythonCommand = 'python3';
90
+ const result = await analyzer.getPythonCommand();
91
+ expect(result).toBe('python3');
92
+ expect(mockSpawn).not.toHaveBeenCalled();
93
+ });
94
+
95
+ test('getPythonCommand finds python3 via spawn', async () => {
96
+ mockSpawn.mockImplementation((cmd, args) => {
97
+ if (args.includes('--version')) {
98
+ return createMockProcess('Python 3.11.0', '', 0);
99
+ }
100
+ return createMockProcess('', '', 1);
101
+ });
102
+
103
+ const result = await analyzer.getPythonCommand();
104
+ // Should find one of the python commands
105
+ if (result) {
106
+ expect(typeof result).toBe('string');
107
+ expect(analyzer.pythonCommand).toBe(result);
108
+ }
109
+ });
110
+
111
+ test('getPythonCommand returns null when python not available', async () => {
112
+ mockSpawn.mockImplementation(() => {
113
+ return createMockProcess('', '', 1);
114
+ });
115
+
116
+ const result = await analyzer.getPythonCommand();
117
+ expect(result).toBeNull();
118
+ });
119
+
120
+ // ── analyze ──
121
+ test('analyze returns empty when python not available', async () => {
122
+ mockSpawn.mockImplementation(() => {
123
+ return createMockProcess('', '', 1);
124
+ });
125
+
126
+ const result = await analyzer.analyze('test.py', 'print("hello")');
127
+ expect(result).toEqual([]);
128
+ });
129
+
130
+ test('analyze returns diagnostics for syntax errors', async () => {
131
+ // First call for getPythonCommand
132
+ let callCount = 0;
133
+ mockSpawn.mockImplementation((cmd, args) => {
134
+ callCount++;
135
+ if (args.includes('--version')) {
136
+ return createMockProcess('Python 3.11.0', '', 0);
137
+ }
138
+ // Syntax check script
139
+ const jsonResult = JSON.stringify({
140
+ success: false,
141
+ errors: [{ file: 'test.py', line: 1, column: 5, message: 'invalid syntax', text: 'def(' }]
142
+ });
143
+ return createMockProcess(jsonResult, '', 0);
144
+ });
145
+
146
+ const result = await analyzer.analyze('test.py', 'def(');
147
+ expect(Array.isArray(result)).toBe(true);
148
+ if (result.length > 0) {
149
+ expect(result[0].severity).toBe('error');
150
+ expect(result[0].rule).toBe('SyntaxError');
151
+ }
152
+ });
153
+
154
+ test('analyze returns empty on exception', async () => {
155
+ mockSpawn.mockImplementation(() => {
156
+ throw new Error('spawn failed');
157
+ });
158
+
159
+ const result = await analyzer.analyze('test.py', 'print("hello")');
160
+ expect(result).toEqual([]);
161
+ });
162
+
163
+ // ── checkSyntax ──
164
+ test('checkSyntax handles successful parse', async () => {
165
+ mockSpawn.mockImplementation(() => {
166
+ return createMockProcess(JSON.stringify({ success: true, errors: [] }), '', 0);
167
+ });
168
+
169
+ const result = await analyzer.checkSyntax('test.py', 'x = 1', 'python3');
170
+ expect(result).toEqual([]);
171
+ });
172
+
173
+ test('checkSyntax handles unparseable output', async () => {
174
+ mockSpawn.mockImplementation(() => {
175
+ return createMockProcess('not json at all', '', 0);
176
+ });
177
+
178
+ const result = await analyzer.checkSyntax('test.py', 'x = 1', 'python3');
179
+ expect(result).toEqual([]);
180
+ });
181
+
182
+ // ── runCommand ──
183
+ test('runCommand resolves with success for exit code 0', async () => {
184
+ mockSpawn.mockReturnValue(createMockProcess('output', '', 0));
185
+
186
+ const result = await analyzer.runCommand('echo', ['hello']);
187
+ expect(result.success).toBe(true);
188
+ expect(result.code).toBe(0);
189
+ });
190
+
191
+ test('runCommand resolves with failure for non-zero exit code', async () => {
192
+ mockSpawn.mockReturnValue(createMockProcess('', 'error', 1));
193
+
194
+ const result = await analyzer.runCommand('bad', []);
195
+ expect(result.success).toBe(false);
196
+ expect(result.code).toBe(1);
197
+ });
198
+
199
+ test('runCommand rejects on spawn error', async () => {
200
+ const errorProc = createMockProcess('', '', 0);
201
+ errorProc.on = jest.fn((event, cb) => {
202
+ if (event === 'error') setTimeout(() => cb(new Error('spawn ENOENT')), 5);
203
+ });
204
+ mockSpawn.mockReturnValue(errorProc);
205
+
206
+ await expect(analyzer.runCommand('nonexistent', [])).rejects.toThrow();
207
+ });
208
+ });
@@ -0,0 +1,303 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock child_process
5
+ const mockExecAsync = jest.fn();
6
+ jest.unstable_mockModule('child_process', () => ({
7
+ exec: jest.fn((cmd, opts, cb) => {
8
+ if (typeof opts === 'function') { cb = opts; opts = {}; }
9
+ const err = new Error('not found');
10
+ err.code = 'ENOENT';
11
+ cb(err);
12
+ })
13
+ }));
14
+
15
+ jest.unstable_mockModule('util', () => ({
16
+ promisify: jest.fn(() => mockExecAsync)
17
+ }));
18
+
19
+ // Mock fs/promises
20
+ jest.unstable_mockModule('fs/promises', () => ({
21
+ default: {
22
+ access: jest.fn().mockRejectedValue(new Error('ENOENT'))
23
+ },
24
+ access: jest.fn().mockRejectedValue(new Error('ENOENT'))
25
+ }));
26
+
27
+ // Mock constants
28
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
29
+ STATIC_ANALYSIS: {
30
+ SEVERITY: {
31
+ CRITICAL: 'critical',
32
+ ERROR: 'error',
33
+ WARNING: 'warning',
34
+ INFO: 'info',
35
+ SUGGESTION: 'suggestion'
36
+ }
37
+ }
38
+ }));
39
+
40
+ const { default: SecurityAnalyzer } = await import('../SecurityAnalyzer.js');
41
+
42
+ describe('SecurityAnalyzer', () => {
43
+ let analyzer;
44
+ let logger;
45
+
46
+ beforeEach(() => {
47
+ logger = createMockLogger();
48
+ analyzer = new SecurityAnalyzer(logger);
49
+ // Reset scanner cache so each test gets fresh detection
50
+ analyzer.availableScanners = null;
51
+ mockExecAsync.mockReset();
52
+ mockExecAsync.mockRejectedValue(new Error('not found'));
53
+ });
54
+
55
+ // --- constructor ---
56
+
57
+ test('constructor creates instance with logger', () => {
58
+ expect(analyzer).toBeInstanceOf(SecurityAnalyzer);
59
+ expect(analyzer.logger).toBe(logger);
60
+ });
61
+
62
+ test('constructor creates instance without logger', () => {
63
+ const a = new SecurityAnalyzer();
64
+ expect(a).toBeInstanceOf(SecurityAnalyzer);
65
+ expect(a.logger).toBeNull();
66
+ });
67
+
68
+ // --- detectAvailableScanners ---
69
+
70
+ test('detectAvailableScanners returns all false when no scanners are installed', async () => {
71
+ const scanners = await analyzer.detectAvailableScanners();
72
+ expect(scanners.semgrep).toBe(false);
73
+ expect(scanners.bandit).toBe(false);
74
+ expect(scanners.npmAudit).toBe(false);
75
+ expect(scanners.pipAudit).toBe(false);
76
+ expect(scanners.eslintSecurity).toBe(false);
77
+ });
78
+
79
+ test('detectAvailableScanners caches result on second call', async () => {
80
+ await analyzer.detectAvailableScanners();
81
+ const callCount = mockExecAsync.mock.calls.length;
82
+ await analyzer.detectAvailableScanners();
83
+ // Should not have made additional calls
84
+ expect(mockExecAsync.mock.calls.length).toBe(callCount);
85
+ });
86
+
87
+ test('detectAvailableScanners detects npm when available', async () => {
88
+ // Make npm --version succeed
89
+ mockExecAsync.mockImplementation((cmd) => {
90
+ if (cmd === 'npm --version') return Promise.resolve({ stdout: '10.0.0' });
91
+ return Promise.reject(new Error('not found'));
92
+ });
93
+ analyzer.availableScanners = null;
94
+ const scanners = await analyzer.detectAvailableScanners();
95
+ expect(scanners.npmAudit).toBe(true);
96
+ });
97
+
98
+ test('detectAvailableScanners detects bandit when available', async () => {
99
+ mockExecAsync.mockImplementation((cmd) => {
100
+ if (cmd === 'bandit --version') return Promise.resolve({ stdout: '1.7.0' });
101
+ return Promise.reject(new Error('not found'));
102
+ });
103
+ analyzer.availableScanners = null;
104
+ const scanners = await analyzer.detectAvailableScanners();
105
+ expect(scanners.bandit).toBe(true);
106
+ });
107
+
108
+ // --- analyze ---
109
+
110
+ test('analyze returns empty array for benign content with no scanners', async () => {
111
+ const result = await analyzer.analyze('app.js', 'const x = 1 + 2;');
112
+ expect(Array.isArray(result)).toBe(true);
113
+ expect(result.length).toBe(0);
114
+ });
115
+
116
+ test('analyze skips test files by default', async () => {
117
+ const result = await analyzer.analyze('app.test.js', 'const password = "secret123";');
118
+ expect(result).toEqual([]);
119
+ });
120
+
121
+ test('analyze scans test files when skipTestFiles is false', async () => {
122
+ const result = await analyzer.analyze('app.test.js', 'const x = 1;', { skipTestFiles: false });
123
+ expect(Array.isArray(result)).toBe(true);
124
+ });
125
+
126
+ // --- detectLanguage ---
127
+
128
+ test('detectLanguage returns javascript for .js files', () => {
129
+ expect(analyzer.detectLanguage('app.js')).toBe('javascript');
130
+ expect(analyzer.detectLanguage('component.jsx')).toBe('javascript');
131
+ expect(analyzer.detectLanguage('module.mjs')).toBe('javascript');
132
+ });
133
+
134
+ test('detectLanguage returns typescript for .ts files', () => {
135
+ expect(analyzer.detectLanguage('app.ts')).toBe('typescript');
136
+ expect(analyzer.detectLanguage('component.tsx')).toBe('typescript');
137
+ });
138
+
139
+ test('detectLanguage returns python for .py files', () => {
140
+ expect(analyzer.detectLanguage('script.py')).toBe('python');
141
+ });
142
+
143
+ test('detectLanguage returns null for unsupported extensions', () => {
144
+ expect(analyzer.detectLanguage('style.css')).toBeNull();
145
+ expect(analyzer.detectLanguage('data.json')).toBeNull();
146
+ });
147
+
148
+ // --- isTestFile ---
149
+
150
+ test('isTestFile detects various test file patterns', () => {
151
+ expect(analyzer.isTestFile('app.test.js')).toBe(true);
152
+ expect(analyzer.isTestFile('app.spec.ts')).toBe(true);
153
+ expect(analyzer.isTestFile('__tests__/foo.js')).toBe(true);
154
+ expect(analyzer.isTestFile('src/app.js')).toBe(false);
155
+ });
156
+
157
+ // --- parseSemgrepResults ---
158
+
159
+ test('parseSemgrepResults parses valid semgrep output', () => {
160
+ const output = {
161
+ results: [{
162
+ path: 'app.js',
163
+ start: { line: 10, col: 5 },
164
+ check_id: 'security.hardcoded-password',
165
+ extra: {
166
+ severity: 'ERROR',
167
+ message: 'Hardcoded password detected',
168
+ metadata: { cwe: 'CWE-798', owasp: 'A2', confidence: 'HIGH' }
169
+ }
170
+ }]
171
+ };
172
+ const issues = analyzer.parseSemgrepResults(output);
173
+ expect(issues.length).toBe(1);
174
+ expect(issues[0].file).toBe('app.js');
175
+ expect(issues[0].line).toBe(10);
176
+ expect(issues[0].severity).toBe('critical');
177
+ expect(issues[0].scanner).toBe('semgrep');
178
+ expect(issues[0].cwe).toBe('CWE-798');
179
+ });
180
+
181
+ test('parseSemgrepResults returns empty array for no results', () => {
182
+ expect(analyzer.parseSemgrepResults({})).toEqual([]);
183
+ expect(analyzer.parseSemgrepResults({ results: [] })).toEqual([]);
184
+ });
185
+
186
+ // --- parseBanditResults ---
187
+
188
+ test('parseBanditResults parses valid bandit output', () => {
189
+ const output = {
190
+ results: [{
191
+ filename: 'app.py',
192
+ line_number: 42,
193
+ test_id: 'B101',
194
+ issue_severity: 'HIGH',
195
+ issue_text: 'Use of assert detected',
196
+ issue_confidence: 'HIGH',
197
+ issue_cwe: { id: 703 }
198
+ }]
199
+ };
200
+ const issues = analyzer.parseBanditResults(output);
201
+ expect(issues.length).toBe(1);
202
+ expect(issues[0].file).toBe('app.py');
203
+ expect(issues[0].line).toBe(42);
204
+ expect(issues[0].severity).toBe('critical');
205
+ expect(issues[0].scanner).toBe('bandit');
206
+ expect(issues[0].cwe).toBe('CWE-703');
207
+ });
208
+
209
+ test('parseBanditResults returns empty array for no results', () => {
210
+ expect(analyzer.parseBanditResults({})).toEqual([]);
211
+ });
212
+
213
+ // --- parseNpmAuditResults ---
214
+
215
+ test('parseNpmAuditResults parses npm audit v7+ format', () => {
216
+ const output = {
217
+ vulnerabilities: {
218
+ 'lodash': {
219
+ severity: 'critical',
220
+ via: [{ source: 12345, title: 'Prototype Pollution', cve: 'CVE-2021-23337', url: 'https://example.com' }],
221
+ range: '<4.17.21',
222
+ fixAvailable: true
223
+ }
224
+ }
225
+ };
226
+ const issues = analyzer.parseNpmAuditResults(output);
227
+ expect(issues.length).toBe(1);
228
+ expect(issues[0].package).toBe('lodash');
229
+ expect(issues[0].severity).toBe('critical');
230
+ expect(issues[0].scanner).toBe('npm-audit');
231
+ });
232
+
233
+ test('parseNpmAuditResults returns empty array when no vulnerabilities', () => {
234
+ expect(analyzer.parseNpmAuditResults({})).toEqual([]);
235
+ });
236
+
237
+ // --- getScannerStatus ---
238
+
239
+ test('getScannerStatus returns scanners and recommendations', async () => {
240
+ const status = await analyzer.getScannerStatus();
241
+ expect(status).toHaveProperty('scanners');
242
+ expect(status).toHaveProperty('recommendations');
243
+ expect(Array.isArray(status.recommendations)).toBe(true);
244
+ // With no scanners available, we should get recommendations
245
+ expect(status.recommendations.length).toBeGreaterThan(0);
246
+ });
247
+
248
+ // --- severity mapping ---
249
+
250
+ test('mapSemgrepSeverity maps correctly', () => {
251
+ expect(analyzer.mapSemgrepSeverity('ERROR')).toBe('critical');
252
+ expect(analyzer.mapSemgrepSeverity('WARNING')).toBe('error');
253
+ expect(analyzer.mapSemgrepSeverity('INFO')).toBe('warning');
254
+ expect(analyzer.mapSemgrepSeverity('UNKNOWN')).toBe('warning');
255
+ });
256
+
257
+ test('mapBanditSeverity maps correctly', () => {
258
+ expect(analyzer.mapBanditSeverity('HIGH')).toBe('critical');
259
+ expect(analyzer.mapBanditSeverity('MEDIUM')).toBe('error');
260
+ expect(analyzer.mapBanditSeverity('LOW')).toBe('warning');
261
+ });
262
+
263
+ test('mapNpmSeverity maps correctly', () => {
264
+ expect(analyzer.mapNpmSeverity('critical')).toBe('critical');
265
+ expect(analyzer.mapNpmSeverity('high')).toBe('critical');
266
+ expect(analyzer.mapNpmSeverity('moderate')).toBe('error');
267
+ expect(analyzer.mapNpmSeverity('low')).toBe('warning');
268
+ expect(analyzer.mapNpmSeverity('info')).toBe('info');
269
+ });
270
+
271
+ test('mapESLintSeverity maps 2 to error, 1 to warning', () => {
272
+ expect(analyzer.mapESLintSeverity(2)).toBe('error');
273
+ expect(analyzer.mapESLintSeverity(1)).toBe('warning');
274
+ });
275
+
276
+ // --- normalizeResults ---
277
+
278
+ test('normalizeResults maps all fields to common format', () => {
279
+ const raw = [{
280
+ file: 'x.js', line: 5, column: 3, severity: 'critical',
281
+ rule: 'test-rule', message: 'Bad thing', scanner: 'test',
282
+ cwe: 'CWE-1', fixable: true
283
+ }];
284
+ const normalized = analyzer.normalizeResults(raw);
285
+ expect(normalized.length).toBe(1);
286
+ expect(normalized[0].category).toBe('security');
287
+ expect(normalized[0].fixable).toBe(true);
288
+ expect(normalized[0].cwe).toBe('CWE-1');
289
+ });
290
+
291
+ // --- hasScannersForLanguage ---
292
+
293
+ test('hasScannersForLanguage returns false when no scanners available', () => {
294
+ const available = { semgrep: false, bandit: false, eslintSecurity: false };
295
+ expect(analyzer.hasScannersForLanguage(available, 'javascript')).toBe(false);
296
+ expect(analyzer.hasScannersForLanguage(available, 'python')).toBe(false);
297
+ expect(analyzer.hasScannersForLanguage(available, 'ruby')).toBe(false);
298
+ });
299
+
300
+ test('hasScannersForLanguage returns true when semgrep available for JS', () => {
301
+ expect(analyzer.hasScannersForLanguage({ semgrep: true, eslintSecurity: false }, 'javascript')).toBe(true);
302
+ });
303
+ });