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
package/package.json
CHANGED
|
@@ -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
|
+
});
|