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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onbuzz",
3
- "version": "3.6.1",
3
+ "version": "3.6.3",
4
4
  "description": "Loxia OnBuzz - Your AI Fleet",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,31 @@
1
+ /** Malformed JSON strings for testing parsers and repair functions */
2
+
3
+ export const validJson = '{"name": "test", "value": 42}';
4
+ export const validJsonArray = '[1, 2, 3]';
5
+ export const emptyObject = '{}';
6
+ export const emptyArray = '[]';
7
+
8
+ // Truncated
9
+ export const truncatedObject = '{"name": "test", "nested": {"key": "val';
10
+ export const truncatedArray = '[1, 2, 3, {"name": "test"';
11
+ export const truncatedString = '{"message": "hello wor';
12
+
13
+ // Trailing commas
14
+ export const trailingCommaObject = '{"a": 1, "b": 2,}';
15
+ export const trailingCommaArray = '[1, 2, 3,]';
16
+ export const trailingCommaNestedObject = '{"a": {"b": 1,}, "c": [1, 2,],}';
17
+
18
+ // Missing quotes
19
+ export const unquotedKeys = '{name: "test", value: 42}';
20
+
21
+ // Extra content
22
+ export const jsonWithTrailingText = '{"name": "test"} some extra text';
23
+
24
+ // Single quotes
25
+ export const singleQuotes = "{'name': 'test', 'value': 42}";
26
+
27
+ // Not JSON at all
28
+ export const plainText = 'This is just plain text, not JSON at all.';
29
+ export const htmlContent = '<html><body><p>Not JSON</p></body></html>';
30
+ export const emptyString = '';
31
+ export const whitespaceOnly = ' \n\t ';
@@ -0,0 +1,9 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs/promises';
4
+
5
+ export default async function globalSetup() {
6
+ const tmpDir = path.join(os.tmpdir(), `loxia-test-${Date.now()}`);
7
+ await fs.mkdir(tmpDir, { recursive: true });
8
+ process.env.LOXIA_TEST_TMPDIR = tmpDir;
9
+ }
@@ -0,0 +1,12 @@
1
+ import fs from 'fs/promises';
2
+
3
+ export default async function globalTeardown() {
4
+ const tmpDir = process.env.LOXIA_TEST_TMPDIR;
5
+ if (tmpDir) {
6
+ try {
7
+ await fs.rm(tmpDir, { recursive: true, force: true });
8
+ } catch {
9
+ // Best-effort cleanup
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared mock factories for unit tests.
3
+ * Import these to avoid repeating mock construction across test files.
4
+ */
5
+ import { jest } from '@jest/globals';
6
+
7
+ /** Standard mock logger matching Logger interface */
8
+ export function createMockLogger() {
9
+ return {
10
+ info: jest.fn(),
11
+ warn: jest.fn(),
12
+ error: jest.fn(),
13
+ debug: jest.fn(),
14
+ verbose: jest.fn(),
15
+ logAgentActivity: jest.fn(),
16
+ logToolExecution: jest.fn(),
17
+ logSystemEvent: jest.fn(),
18
+ logApiRequest: jest.fn(),
19
+ child: jest.fn().mockReturnThis()
20
+ };
21
+ }
22
+
23
+ /** Standard mock config with sensible defaults */
24
+ export function createMockConfig(overrides = {}) {
25
+ return {
26
+ apiKeys: { anthropic: 'test-key-fake' },
27
+ models: {},
28
+ tools: {},
29
+ system: {
30
+ maxAgentsPerProject: 10,
31
+ stateDirectory: '.loxia-state',
32
+ maxContextSize: 200000
33
+ },
34
+ ...overrides
35
+ };
36
+ }
37
+
38
+ /** Mock AI service */
39
+ export function createMockAiService() {
40
+ return {
41
+ sendMessage: jest.fn().mockResolvedValue({
42
+ content: 'mock response',
43
+ tokenUsage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 }
44
+ }),
45
+ getAvailableModels: jest.fn().mockReturnValue([]),
46
+ initialize: jest.fn().mockResolvedValue(undefined)
47
+ };
48
+ }
49
+
50
+ /** Mock state manager */
51
+ export function createMockStateManager() {
52
+ const store = new Map();
53
+ return {
54
+ getState: jest.fn((key) => store.get(key)),
55
+ setState: jest.fn((key, value) => store.set(key, value)),
56
+ deleteState: jest.fn((key) => store.delete(key)),
57
+ listStates: jest.fn(() => [...store.keys()]),
58
+ initialize: jest.fn().mockResolvedValue(undefined),
59
+ initializeStateDirectory: jest.fn().mockResolvedValue(undefined),
60
+ persistAgentState: jest.fn().mockResolvedValue(undefined),
61
+ getProjectState: jest.fn().mockResolvedValue({}),
62
+ loadProjectState: jest.fn().mockResolvedValue({}),
63
+ saveProjectState: jest.fn().mockResolvedValue(undefined),
64
+ loadAgentIndex: jest.fn().mockResolvedValue({}),
65
+ updateAgentIndex: jest.fn().mockResolvedValue(undefined),
66
+ getStateDir: jest.fn().mockReturnValue('/tmp/test-state'),
67
+ getAgentsDir: jest.fn().mockReturnValue('/tmp/test-state/agents'),
68
+ resumeProject: jest.fn().mockResolvedValue({}),
69
+ _store: store
70
+ };
71
+ }
72
+
73
+ /** Mock agent pool */
74
+ export function createMockAgentPool() {
75
+ const agents = new Map();
76
+ return {
77
+ getAgent: jest.fn((id) => agents.get(id)),
78
+ getAllAgents: jest.fn(() => [...agents.values()]),
79
+ createAgent: jest.fn((params) => {
80
+ const agent = { id: `agent_test_${Date.now()}`, ...params, status: 'idle' };
81
+ agents.set(agent.id, agent);
82
+ return agent;
83
+ }),
84
+ removeAgent: jest.fn((id) => agents.delete(id)),
85
+ updateAgent: jest.fn((id, updates) => {
86
+ const agent = agents.get(id);
87
+ if (agent) Object.assign(agent, updates);
88
+ return agent;
89
+ }),
90
+ _agents: agents
91
+ };
92
+ }
93
+
94
+ /** Create a minimal Express app for route testing with supertest */
95
+ export async function createTestExpressApp(routeSetupFn) {
96
+ const { default: express } = await import('express');
97
+ const app = express();
98
+ app.use(express.json());
99
+ if (routeSetupFn) await routeSetupFn(app);
100
+ return app;
101
+ }
@@ -0,0 +1,41 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+ import CSSAnalyzer from '../CSSAnalyzer.js';
4
+
5
+ describe('CSSAnalyzer', () => {
6
+ let logger;
7
+
8
+ beforeEach(() => {
9
+ logger = createMockLogger();
10
+ });
11
+
12
+ test('constructor creates instance', () => {
13
+ const analyzer = new CSSAnalyzer(logger);
14
+ expect(analyzer).toBeInstanceOf(CSSAnalyzer);
15
+ expect(analyzer.logger).toBe(logger);
16
+ });
17
+
18
+ test('detectLanguage returns css for .css file', () => {
19
+ const analyzer = new CSSAnalyzer(logger);
20
+ expect(analyzer.detectLanguage('styles.css')).toBe('css');
21
+ });
22
+
23
+ test('detectLanguage returns scss for .scss file', () => {
24
+ const analyzer = new CSSAnalyzer(logger);
25
+ expect(analyzer.detectLanguage('styles.scss')).toBe('scss');
26
+ });
27
+
28
+ test('getSupportedExtensions returns array including .css', () => {
29
+ const analyzer = new CSSAnalyzer(logger);
30
+ const extensions = analyzer.getSupportedExtensions();
31
+ expect(Array.isArray(extensions)).toBe(true);
32
+ expect(extensions).toContain('.css');
33
+ expect(extensions).toContain('.scss');
34
+ expect(extensions).toContain('.less');
35
+ });
36
+
37
+ test('supportsAutoFix returns false', () => {
38
+ const analyzer = new CSSAnalyzer(logger);
39
+ expect(analyzer.supportsAutoFix()).toBe(false);
40
+ });
41
+ });
@@ -0,0 +1,362 @@
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: (cmd, opts, cb) => {
8
+ // promisify wraps exec, so we intercept at the promisified level
9
+ }
10
+ }));
11
+ jest.unstable_mockModule('util', () => ({
12
+ promisify: () => mockExecAsync
13
+ }));
14
+
15
+ // Mock fs/promises
16
+ const mockReadFile = jest.fn();
17
+ jest.unstable_mockModule('fs/promises', () => ({
18
+ default: { readFile: mockReadFile },
19
+ readFile: mockReadFile
20
+ }));
21
+
22
+ // Mock constants
23
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
24
+ STATIC_ANALYSIS: {
25
+ SEVERITY: {
26
+ CRITICAL: 'critical',
27
+ ERROR: 'error',
28
+ WARNING: 'warning',
29
+ INFO: 'info',
30
+ SUGGESTION: 'suggestion'
31
+ },
32
+ CATEGORY: {
33
+ SYNTAX: 'syntax',
34
+ TYPE: 'type',
35
+ IMPORT: 'import',
36
+ STYLE: 'style',
37
+ SECURITY: 'security',
38
+ PERFORMANCE: 'performance',
39
+ BEST_PRACTICE: 'best_practice'
40
+ }
41
+ }
42
+ }));
43
+
44
+ const { default: ConfigValidator } = await import('../ConfigValidator.js');
45
+
46
+ describe('ConfigValidator', () => {
47
+ let validator;
48
+ let logger;
49
+
50
+ beforeEach(() => {
51
+ logger = createMockLogger();
52
+ validator = new ConfigValidator(logger);
53
+ jest.clearAllMocks();
54
+ validator.availableScanners = null;
55
+ validator.scannerCache.clear();
56
+ });
57
+
58
+ // ── Constructor ──
59
+ test('constructor initializes with defaults', () => {
60
+ expect(validator.logger).toBe(logger);
61
+ expect(validator.availableScanners).toBeNull();
62
+ expect(validator.scannerCache).toBeInstanceOf(Map);
63
+ });
64
+
65
+ test('constructor works without logger', () => {
66
+ const v = new ConfigValidator();
67
+ expect(v.logger).toBeNull();
68
+ });
69
+
70
+ // ── detectAvailableValidators ──
71
+ test('detectAvailableValidators returns cached result on second call', async () => {
72
+ mockExecAsync.mockRejectedValue(new Error('not found'));
73
+
74
+ const first = await validator.detectAvailableValidators();
75
+ const second = await validator.detectAvailableValidators();
76
+ expect(first).toBe(second);
77
+ // execAsync should only be called during first detection
78
+ });
79
+
80
+ test('detectAvailableValidators detects checkov when available', async () => {
81
+ mockExecAsync.mockImplementation((cmd) => {
82
+ if (cmd.startsWith('checkov')) return Promise.resolve({ stdout: '2.0' });
83
+ return Promise.reject(new Error('not found'));
84
+ });
85
+
86
+ const result = await validator.detectAvailableValidators();
87
+ expect(result.checkov).toBe(true);
88
+ expect(result.hadolint).toBe(false);
89
+ expect(result.yamllint).toBe(false);
90
+ });
91
+
92
+ test('detectAvailableValidators returns all false when nothing available', async () => {
93
+ mockExecAsync.mockRejectedValue(new Error('not found'));
94
+
95
+ const result = await validator.detectAvailableValidators();
96
+ expect(result.checkov).toBe(false);
97
+ expect(result.hadolint).toBe(false);
98
+ expect(result.yamllint).toBe(false);
99
+ });
100
+
101
+ // ── detectFileType ──
102
+ test('detectFileType identifies Dockerfile', () => {
103
+ expect(validator.detectFileType('/path/Dockerfile')).toBe('dockerfile');
104
+ });
105
+
106
+ test('detectFileType identifies docker-compose.yml', () => {
107
+ expect(validator.detectFileType('/path/docker-compose.yml')).toBe('docker-compose');
108
+ expect(validator.detectFileType('/path/docker-compose.yaml')).toBe('docker-compose');
109
+ });
110
+
111
+ test('detectFileType identifies package.json', () => {
112
+ expect(validator.detectFileType('/path/package.json')).toBe('package.json');
113
+ });
114
+
115
+ test('detectFileType identifies tsconfig.json', () => {
116
+ expect(validator.detectFileType('/path/tsconfig.json')).toBe('tsconfig.json');
117
+ });
118
+
119
+ test('detectFileType identifies .env files', () => {
120
+ expect(validator.detectFileType('/path/.env')).toBe('env');
121
+ expect(validator.detectFileType('/path/production.env')).toBe('env');
122
+ });
123
+
124
+ test('detectFileType identifies github-actions from path', () => {
125
+ expect(validator.detectFileType('/project/.github/workflows/ci.yml')).toBe('github-actions');
126
+ });
127
+
128
+ test('detectFileType identifies kubernetes from path', () => {
129
+ expect(validator.detectFileType('/project/kubernetes/deploy.yml')).toBe('kubernetes');
130
+ expect(validator.detectFileType('/project/k8s/service.yaml')).toBe('kubernetes');
131
+ });
132
+
133
+ test('detectFileType identifies terraform', () => {
134
+ expect(validator.detectFileType('/path/main.tf')).toBe('terraform');
135
+ expect(validator.detectFileType('/path/vars.tfvars')).toBe('terraform');
136
+ });
137
+
138
+ test('detectFileType identifies yaml', () => {
139
+ expect(validator.detectFileType('/path/config.yml')).toBe('yaml');
140
+ expect(validator.detectFileType('/path/config.yaml')).toBe('yaml');
141
+ });
142
+
143
+ test('detectFileType identifies json', () => {
144
+ expect(validator.detectFileType('/path/data.json')).toBe('json');
145
+ });
146
+
147
+ test('detectFileType returns unknown for unrecognized', () => {
148
+ expect(validator.detectFileType('/path/file.txt')).toBe('unknown');
149
+ });
150
+
151
+ // ── validateEnvFile ──
152
+ test('validateEnvFile detects hardcoded secrets', async () => {
153
+ mockReadFile.mockResolvedValue(
154
+ '# comment\nDB_HOST=localhost\nAPI_KEY=sk-1234567890abcdef\nSECRET_TOKEN=my-super-secret-value\n'
155
+ );
156
+
157
+ const issues = await validator.validateEnvFile('/path/.env');
158
+ expect(issues.length).toBeGreaterThan(0);
159
+ expect(issues[0].severity).toBe('critical');
160
+ expect(issues[0].rule).toBe('hardcoded-secret');
161
+ });
162
+
163
+ test('validateEnvFile ignores placeholders and short values', async () => {
164
+ mockReadFile.mockResolvedValue(
165
+ 'API_KEY=your-key-here\nTOKEN=changeme\nSECRET=$OTHER_VAR\nPASSWORD=abc\n'
166
+ );
167
+
168
+ const issues = await validator.validateEnvFile('/path/.env');
169
+ expect(issues.length).toBe(0);
170
+ });
171
+
172
+ test('validateEnvFile ignores comments and empty lines', async () => {
173
+ mockReadFile.mockResolvedValue('# this is a comment\n\n');
174
+
175
+ const issues = await validator.validateEnvFile('/path/.env');
176
+ expect(issues.length).toBe(0);
177
+ });
178
+
179
+ test('validateEnvFile returns empty on read error', async () => {
180
+ mockReadFile.mockRejectedValue(new Error('ENOENT'));
181
+
182
+ const issues = await validator.validateEnvFile('/path/.env');
183
+ expect(issues).toEqual([]);
184
+ });
185
+
186
+ // ── validateTsConfig ──
187
+ test('validateTsConfig warns about missing strict mode', async () => {
188
+ mockReadFile.mockResolvedValue(JSON.stringify({
189
+ compilerOptions: { target: 'es2020' }
190
+ }));
191
+
192
+ const issues = await validator.validateTsConfig('/path/tsconfig.json');
193
+ expect(issues.some(i => i.rule === 'strict-mode')).toBe(true);
194
+ });
195
+
196
+ test('validateTsConfig warns about noImplicitAny disabled', async () => {
197
+ mockReadFile.mockResolvedValue(JSON.stringify({
198
+ compilerOptions: { strict: true, noImplicitAny: false }
199
+ }));
200
+
201
+ const issues = await validator.validateTsConfig('/path/tsconfig.json');
202
+ expect(issues.some(i => i.rule === 'no-implicit-any')).toBe(true);
203
+ });
204
+
205
+ test('validateTsConfig no warnings for strict config', async () => {
206
+ mockReadFile.mockResolvedValue(JSON.stringify({
207
+ compilerOptions: { strict: true }
208
+ }));
209
+
210
+ const issues = await validator.validateTsConfig('/path/tsconfig.json');
211
+ expect(issues.length).toBe(0);
212
+ });
213
+
214
+ test('validateTsConfig returns error on invalid JSON', async () => {
215
+ mockReadFile.mockResolvedValue('not json {{{');
216
+
217
+ const issues = await validator.validateTsConfig('/path/tsconfig.json');
218
+ expect(issues.length).toBe(1);
219
+ expect(issues[0].rule).toBe('json-parse');
220
+ expect(issues[0].severity).toBe('error');
221
+ });
222
+
223
+ test('validateTsConfig handles missing compilerOptions', async () => {
224
+ mockReadFile.mockResolvedValue(JSON.stringify({}));
225
+
226
+ const issues = await validator.validateTsConfig('/path/tsconfig.json');
227
+ expect(issues.length).toBe(0);
228
+ });
229
+
230
+ // ── validate (routing) ──
231
+ test('validate routes env files without external tools', async () => {
232
+ mockExecAsync.mockRejectedValue(new Error('not found'));
233
+ mockReadFile.mockResolvedValue('SECRET=supersecretvalue123\n');
234
+
235
+ const issues = await validator.validate('/path/.env');
236
+ expect(issues.length).toBeGreaterThan(0);
237
+ });
238
+
239
+ test('validate returns empty for unknown file type', async () => {
240
+ mockExecAsync.mockRejectedValue(new Error('not found'));
241
+
242
+ const issues = await validator.validate('/path/file.txt');
243
+ expect(issues).toEqual([]);
244
+ });
245
+
246
+ // ── normalizeResults ──
247
+ test('normalizeResults fills in defaults', () => {
248
+ const results = validator.normalizeResults([
249
+ { file: 'test.yml', message: 'issue' }
250
+ ]);
251
+ expect(results[0].line).toBe(1);
252
+ expect(results[0].column).toBe(1);
253
+ expect(results[0].severity).toBe('warning');
254
+ expect(results[0].rule).toBe('unknown');
255
+ expect(results[0].cwe).toBeNull();
256
+ expect(results[0].remediation).toBeNull();
257
+ expect(results[0].references).toEqual([]);
258
+ });
259
+
260
+ // ── mapHadolintSeverity ──
261
+ test('mapHadolintSeverity maps known levels', () => {
262
+ expect(validator.mapHadolintSeverity('error')).toBe('error');
263
+ expect(validator.mapHadolintSeverity('warning')).toBe('warning');
264
+ expect(validator.mapHadolintSeverity('info')).toBe('info');
265
+ expect(validator.mapHadolintSeverity('style')).toBe('info');
266
+ });
267
+
268
+ test('mapHadolintSeverity returns warning for unknown', () => {
269
+ expect(validator.mapHadolintSeverity('unknown')).toBe('warning');
270
+ expect(validator.mapHadolintSeverity(null)).toBe('warning');
271
+ });
272
+
273
+ // ── mapYamllintSeverity ──
274
+ test('mapYamllintSeverity maps known levels', () => {
275
+ expect(validator.mapYamllintSeverity('error')).toBe('error');
276
+ expect(validator.mapYamllintSeverity('warning')).toBe('warning');
277
+ });
278
+
279
+ test('mapYamllintSeverity returns warning for unknown', () => {
280
+ expect(validator.mapYamllintSeverity('something')).toBe('warning');
281
+ });
282
+
283
+ // ── mapCheckovSeverity ──
284
+ test('mapCheckovSeverity always returns error', () => {
285
+ expect(validator.mapCheckovSeverity('any')).toBe('error');
286
+ });
287
+
288
+ // ── parseHadolintResults ──
289
+ test('parseHadolintResults parses array of issues', () => {
290
+ const output = [
291
+ { line: 5, column: 1, level: 'warning', code: 'DL3008', message: 'Pin versions' }
292
+ ];
293
+ const issues = validator.parseHadolintResults(output, '/path/Dockerfile');
294
+ expect(issues.length).toBe(1);
295
+ expect(issues[0].rule).toBe('DL3008');
296
+ expect(issues[0].validator).toBe('hadolint');
297
+ });
298
+
299
+ test('parseHadolintResults returns empty for non-array', () => {
300
+ expect(validator.parseHadolintResults({}, '/path/Dockerfile')).toEqual([]);
301
+ });
302
+
303
+ // ── parseYamllintResults ──
304
+ test('parseYamllintResults parses formatted output', () => {
305
+ const output = 'file.yml:3:1: [warning] too many blank lines (empty-lines)\n';
306
+ const issues = validator.parseYamllintResults(output, '/path/file.yml');
307
+ expect(issues.length).toBe(1);
308
+ expect(issues[0].line).toBe(3);
309
+ expect(issues[0].rule).toBe('empty-lines');
310
+ expect(issues[0].validator).toBe('yamllint');
311
+ });
312
+
313
+ test('parseYamllintResults returns empty for non-matching output', () => {
314
+ const issues = validator.parseYamllintResults('some random output\n', '/path/file.yml');
315
+ expect(issues.length).toBe(0);
316
+ });
317
+
318
+ // ── parseCheckovResults ──
319
+ test('parseCheckovResults parses failed checks', () => {
320
+ const output = {
321
+ results: {
322
+ failed_checks: [
323
+ { check_id: 'CKV_1', check_name: 'Test check', file_line_range: [10, 20], guideline: 'http://fix.me' }
324
+ ]
325
+ }
326
+ };
327
+ const issues = validator.parseCheckovResults(output, '/path/main.tf');
328
+ expect(issues.length).toBe(1);
329
+ expect(issues[0].rule).toBe('CKV_1');
330
+ expect(issues[0].validator).toBe('checkov');
331
+ expect(issues[0].references).toEqual(['http://fix.me']);
332
+ });
333
+
334
+ test('parseCheckovResults returns empty for no results', () => {
335
+ expect(validator.parseCheckovResults({}, '/path/main.tf')).toEqual([]);
336
+ });
337
+
338
+ // ── getValidatorStatus ──
339
+ test('getValidatorStatus returns validators and recommendations', async () => {
340
+ mockExecAsync.mockRejectedValue(new Error('not found'));
341
+
342
+ const status = await validator.getValidatorStatus();
343
+ expect(status).toHaveProperty('validators');
344
+ expect(status).toHaveProperty('recommendations');
345
+ expect(status.recommendations.length).toBeGreaterThan(0);
346
+ });
347
+
348
+ // ── getInstallRecommendations ──
349
+ test('getInstallRecommendations returns recommendations for missing tools', () => {
350
+ const recs = validator.getInstallRecommendations({
351
+ checkov: false, hadolint: false, yamllint: false, jsonSchema: false
352
+ });
353
+ expect(recs.length).toBe(4);
354
+ });
355
+
356
+ test('getInstallRecommendations returns empty when all available', () => {
357
+ const recs = validator.getInstallRecommendations({
358
+ checkov: true, hadolint: true, yamllint: true, jsonSchema: true
359
+ });
360
+ expect(recs.length).toBe(0);
361
+ });
362
+ });