devassist-agent 1.0.0
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/LICENSE +21 -0
- package/README.md +467 -0
- package/bin/devassist.js +220 -0
- package/package.json +44 -0
- package/src/ai/adapter.js +464 -0
- package/src/ai/providers/claude.js +80 -0
- package/src/ai/providers/hunyuan.js +87 -0
- package/src/ai/providers/openai.js +74 -0
- package/src/ai/providers/qwen.js +81 -0
- package/src/cli/commands/ai.js +944 -0
- package/src/cli/commands/ask.js +79 -0
- package/src/cli/commands/backup.js +30 -0
- package/src/cli/commands/check.js +327 -0
- package/src/cli/commands/clean.js +130 -0
- package/src/cli/commands/comparison-report.js +326 -0
- package/src/cli/commands/convention.js +91 -0
- package/src/cli/commands/debt.js +49 -0
- package/src/cli/commands/deploy.js +88 -0
- package/src/cli/commands/diff.js +193 -0
- package/src/cli/commands/doctor.js +186 -0
- package/src/cli/commands/fix.js +195 -0
- package/src/cli/commands/init.js +431 -0
- package/src/cli/commands/inject.js +254 -0
- package/src/cli/commands/report.js +310 -0
- package/src/cli/commands/restore.js +78 -0
- package/src/cli/commands/schema.js +93 -0
- package/src/cli/commands/watch.js +212 -0
- package/src/cli/shared/code-context.js +51 -0
- package/src/cli/shared/config-loader.js +89 -0
- package/src/cli/shared/file-collector.js +116 -0
- package/src/cli/shared/inline-ignore.js +142 -0
- package/src/cli/shared/watch-list.js +281 -0
- package/src/core/event-bus.js +83 -0
- package/src/core/fsm.js +103 -0
- package/src/core/logger.js +54 -0
- package/src/core/rule-engine.js +117 -0
- package/src/index.js +64 -0
- package/src/modules/dev-time/arch-risk-assessor.js +250 -0
- package/src/modules/dev-time/code-quality-guard.js +1340 -0
- package/src/modules/dev-time/convention-store.js +201 -0
- package/src/modules/dev-time/impact-analyzer.js +292 -0
- package/src/modules/dev-time/pre-deploy-guard.js +284 -0
- package/src/modules/dev-time/schema-registry.js +284 -0
- package/src/modules/dev-time/tech-debt-tracker.js +225 -0
- package/src/modules/dev-time/version-manager.js +280 -0
- package/src/modules/runtime/channel-agent.js +404 -0
- package/src/modules/runtime/channel-middleware.js +316 -0
- package/src/modules/runtime/index.js +64 -0
- package/src/modules/runtime/infrastructure-guard.js +582 -0
- package/src/modules/runtime/notification-center.js +443 -0
- package/src/modules/runtime/schema-gatekeeper.js +664 -0
- package/src/modules/runtime/tool-registry.js +329 -0
- package/templates/ci/github-actions.yml +60 -0
- package/templates/ci/gitlab-ci.yml +30 -0
- package/templates/ci/pre-commit-hook.sh +18 -0
- package/templates/git-hooks/pre-commit +61 -0
- package/tests/run-all.js +434 -0
- package/tests/test-layer2.js +461 -0
- package/tests/test-new-rules.js +157 -0
package/tests/run-all.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevAssist test suite - verifies core functionality
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. EventBus pub/sub
|
|
6
|
+
* 2. RuleEngine registration and execution
|
|
7
|
+
* 3. FSM state transitions and loop protection
|
|
8
|
+
* 4. ConventionStore add/check/violation detection
|
|
9
|
+
* 5. CodeQualityGuard rules (null safety, duplicates, race condition)
|
|
10
|
+
* 6. ImpactAnalyzer change detection (massive reduction)
|
|
11
|
+
* 7. SchemaRegistry drift detection (data.list vs data.items)
|
|
12
|
+
* 8. VersionManager non-destructive restore
|
|
13
|
+
* 9. AI adapter degradation (no provider configured)
|
|
14
|
+
* 10. ArchRiskAssessor recovery loop detection
|
|
15
|
+
* 11. API naming drift detection (Phase 3)
|
|
16
|
+
* 12. fetch without catch detection (Phase 3)
|
|
17
|
+
* 13. console.log residual detection (Phase 3)
|
|
18
|
+
* 14. Missing await detection (Phase 3)
|
|
19
|
+
* 15. Event listener leak detection (Phase 3)
|
|
20
|
+
* 16. Inject command - convention file generation (Phase 3)
|
|
21
|
+
* 17. AI adapter Chinese prompt + code context (Phase 3)
|
|
22
|
+
* 18. Inline ignore comments (Phase 4)
|
|
23
|
+
* 19. Config loader - rule enable/disable, severity override (Phase 4)
|
|
24
|
+
* 20. Code context extractor (Phase 4)
|
|
25
|
+
* 21. Shared file collector (Phase 4)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const os = require('os');
|
|
31
|
+
|
|
32
|
+
let passed = 0;
|
|
33
|
+
let failed = 0;
|
|
34
|
+
const failures = [];
|
|
35
|
+
|
|
36
|
+
function assert(condition, message) {
|
|
37
|
+
if (condition) {
|
|
38
|
+
passed++;
|
|
39
|
+
} else {
|
|
40
|
+
failed++;
|
|
41
|
+
failures.push(message);
|
|
42
|
+
console.log(` FAIL: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runTests() {
|
|
47
|
+
console.log('\n DevAssist Test Suite\n ====================\n');
|
|
48
|
+
|
|
49
|
+
// --- Test 1: EventBus ---
|
|
50
|
+
console.log(' [1] EventBus...');
|
|
51
|
+
const { EventBus } = require('../src/core/event-bus');
|
|
52
|
+
const bus = new EventBus();
|
|
53
|
+
let received = null;
|
|
54
|
+
bus.on('test-event', (payload) => { received = payload; });
|
|
55
|
+
await bus.emit('test-event', { data: 'hello' });
|
|
56
|
+
assert(received && received.data === 'hello', 'EventBus should deliver payload');
|
|
57
|
+
assert(bus.getHistory('test-event').length === 1, 'EventBus should record history');
|
|
58
|
+
console.log(' OK\n');
|
|
59
|
+
|
|
60
|
+
// --- Test 2: RuleEngine ---
|
|
61
|
+
console.log(' [2] RuleEngine...');
|
|
62
|
+
const { RuleEngine } = require('../src/core/rule-engine');
|
|
63
|
+
const re = new RuleEngine();
|
|
64
|
+
re.register({
|
|
65
|
+
id: 'test-rule',
|
|
66
|
+
severity: 'warn',
|
|
67
|
+
category: 'test',
|
|
68
|
+
check(ctx) {
|
|
69
|
+
if (ctx.value > 10) return { message: 'value too high', file: 'test.js' };
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const result1 = re.run({ value: 5 });
|
|
74
|
+
assert(result1.stats.passed === 1 && result1.findings.length === 0, 'Rule should pass when value < 10');
|
|
75
|
+
const result2 = re.run({ value: 15 });
|
|
76
|
+
assert(result2.findings.length === 1 && result2.findings[0].severity === 'warn', 'Rule should warn when value > 10');
|
|
77
|
+
console.log(' OK\n');
|
|
78
|
+
|
|
79
|
+
// --- Test 3: FSM ---
|
|
80
|
+
console.log(' [3] FSM...');
|
|
81
|
+
const { FSM, STATES } = require('../src/core/fsm');
|
|
82
|
+
const fsm = new FSM();
|
|
83
|
+
assert(fsm.state === STATES.IDLE, 'FSM should start in IDLE');
|
|
84
|
+
fsm.transition(STATES.PERCEIVING, 'test');
|
|
85
|
+
assert(fsm.state === STATES.PERCEIVING, 'FSM should transition to PERCEIVING');
|
|
86
|
+
fsm.transition(STATES.ANALYZING, 'test');
|
|
87
|
+
fsm.transition(STATES.DECIDING, 'test');
|
|
88
|
+
fsm.transition(STATES.ACTING, 'test');
|
|
89
|
+
fsm.transition(STATES.LEARNING, 'test');
|
|
90
|
+
fsm.transition(STATES.IDLE, 'done');
|
|
91
|
+
assert(fsm.state === STATES.IDLE, 'FSM should return to IDLE after full cycle');
|
|
92
|
+
// Test invalid transition
|
|
93
|
+
try {
|
|
94
|
+
fsm.transition(STATES.ACTING, 'invalid');
|
|
95
|
+
assert(false, 'FSM should reject invalid transition IDLE -> ACTING');
|
|
96
|
+
} catch (err) {
|
|
97
|
+
assert(true, 'FSM correctly rejected invalid transition');
|
|
98
|
+
}
|
|
99
|
+
console.log(' OK\n');
|
|
100
|
+
|
|
101
|
+
// --- Test 4: ConventionStore ---
|
|
102
|
+
console.log(' [4] ConventionStore...');
|
|
103
|
+
const { ConventionStore } = require('../src/modules/dev-time/convention-store');
|
|
104
|
+
const tmpDir = path.join(os.tmpdir(), 'devassist-test-' + Date.now());
|
|
105
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
106
|
+
const store = new ConventionStore(tmpDir);
|
|
107
|
+
store.add({
|
|
108
|
+
id: 'naming-test',
|
|
109
|
+
rule: 'Use data.list not data.items',
|
|
110
|
+
severity: 'block',
|
|
111
|
+
forbid: 'data\\.items',
|
|
112
|
+
});
|
|
113
|
+
const findings = store.check('test.js', 'const items = data.items;');
|
|
114
|
+
assert(findings.length > 0, 'ConventionStore should detect data.items violation');
|
|
115
|
+
assert(findings[0].severity === 'block', 'Convention violation should be block severity');
|
|
116
|
+
const clean = store.check('test.js', 'const items = data.list;');
|
|
117
|
+
assert(clean.length === 0, 'ConventionStore should not flag data.list');
|
|
118
|
+
console.log(' OK\n');
|
|
119
|
+
|
|
120
|
+
// --- Test 5: CodeQualityGuard ---
|
|
121
|
+
console.log(' [5] CodeQualityGuard...');
|
|
122
|
+
const { codeQualityRules } = require('../src/modules/dev-time/code-quality-guard');
|
|
123
|
+
const nullRule = codeQualityRules.find(r => r.id === 'cq-null-safety');
|
|
124
|
+
const nullResult = nullRule.check({
|
|
125
|
+
files: [{ path: 'test.js', content: 'const x = data.response.result.method();' }]
|
|
126
|
+
});
|
|
127
|
+
assert(nullResult && nullResult.length > 0, 'Should detect deep property access without null check');
|
|
128
|
+
|
|
129
|
+
const dupRule = codeQualityRules.find(r => r.id === 'cq-duplicate-declaration');
|
|
130
|
+
const dupResult = dupRule.check({
|
|
131
|
+
files: [{ path: 'test.js', content: 'var foo = 1;\nvar foo = 2;\n' }]
|
|
132
|
+
});
|
|
133
|
+
assert(dupResult && dupResult.length > 0, 'Should detect duplicate variable declaration');
|
|
134
|
+
|
|
135
|
+
const largeRule = codeQualityRules.find(r => r.id === 'cq-file-too-large');
|
|
136
|
+
const bigContent = 'x\n'.repeat(900);
|
|
137
|
+
const largeResult = largeRule.check({
|
|
138
|
+
files: [{ path: 'big.js', content: bigContent }],
|
|
139
|
+
config: { maxFileLines: 800 },
|
|
140
|
+
});
|
|
141
|
+
assert(largeResult && largeResult.length > 0, 'Should detect file too large (900 > 800)');
|
|
142
|
+
console.log(' OK\n');
|
|
143
|
+
|
|
144
|
+
// --- Test 6: ImpactAnalyzer ---
|
|
145
|
+
console.log(' [6] ImpactAnalyzer...');
|
|
146
|
+
const { ImpactAnalyzer } = require('../src/modules/dev-time/impact-analyzer');
|
|
147
|
+
const ia = new ImpactAnalyzer(tmpDir);
|
|
148
|
+
// Simulate a file that was 2720 lines, now being reduced to 106 lines
|
|
149
|
+
ia.graph.set('app.js', { imports: new Set(), importedBy: new Set(['index.html', 'main.js']), lines: 2720, exports: new Set(['init']) });
|
|
150
|
+
const impact = ia.analyzeChange(path.join(tmpDir, 'app.js'), 'const x = 1;\n'.repeat(106));
|
|
151
|
+
assert(impact.risk === 'critical', 'Should detect massive line reduction as critical');
|
|
152
|
+
assert(impact.findings.some(f => f.ruleId === 'ia-massive-reduction'), 'Should flag ia-massive-reduction');
|
|
153
|
+
console.log(' OK\n');
|
|
154
|
+
|
|
155
|
+
// --- Test 7: SchemaRegistry ---
|
|
156
|
+
console.log(' [7] SchemaRegistry...');
|
|
157
|
+
const { SchemaRegistry } = require('../src/modules/dev-time/schema-registry');
|
|
158
|
+
const sr = new SchemaRegistry(tmpDir);
|
|
159
|
+
// Register a table
|
|
160
|
+
sr.registerTable('sessions', { columns: [{ name: 'id', type: 'INT' }, { name: 'data', type: 'JSON' }] });
|
|
161
|
+
// Register API contract with data.list
|
|
162
|
+
sr.registerAPI('GET /api/sessions', { dataKey: 'data.list' });
|
|
163
|
+
// Check response that returns data.items instead of data.list
|
|
164
|
+
const apiFindings = sr.checkAPIResponse('GET /api/sessions', { code: 200, data: { items: [{ id: 1 }] } });
|
|
165
|
+
assert(apiFindings && apiFindings.length > 0, 'Should detect data.items vs data.list drift');
|
|
166
|
+
assert(apiFindings[0].ruleId === 'sr-api-key-drift', 'Should flag sr-api-key-drift');
|
|
167
|
+
// Check correct response
|
|
168
|
+
const okFindings = sr.checkAPIResponse('GET /api/sessions', { code: 200, data: { list: [{ id: 1 }] } });
|
|
169
|
+
assert(!okFindings || okFindings.length === 0, 'Should not flag correct data.list response');
|
|
170
|
+
console.log(' OK\n');
|
|
171
|
+
|
|
172
|
+
// --- Test 8: VersionManager ---
|
|
173
|
+
console.log(' [8] VersionManager...');
|
|
174
|
+
const { VersionManager } = require('../src/modules/dev-time/version-manager');
|
|
175
|
+
// Create test files
|
|
176
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
177
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'app.js'), 'console.log("hello");');
|
|
178
|
+
const vm = new VersionManager(tmpDir);
|
|
179
|
+
const backup1 = vm.backup('first backup');
|
|
180
|
+
assert(backup1.files.length > 0, 'Backup should contain files');
|
|
181
|
+
// Add a new file after backup
|
|
182
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'new.js'), 'console.log("new");');
|
|
183
|
+
// Prepare restore — should warn about new file
|
|
184
|
+
const restoreReport = vm.prepareRestore(backup1.versionId);
|
|
185
|
+
assert(restoreReport.newFilesNotInBackup.length > 0, 'Should detect new files not in backup');
|
|
186
|
+
assert(restoreReport.newFilesNotInBackup.some(f => f.includes('new.js')), 'Should list new.js as not in backup');
|
|
187
|
+
console.log(' OK\n');
|
|
188
|
+
|
|
189
|
+
// --- Test 9: AI Adapter degradation ---
|
|
190
|
+
console.log(' [9] AI Adapter degradation...');
|
|
191
|
+
const ai = require('../src/ai/adapter');
|
|
192
|
+
const findings_input = [{ ruleId: 'test', severity: 'warn', message: 'test', file: 'test.js' }];
|
|
193
|
+
const aiResult = await ai.analyze(null, findings_input, {});
|
|
194
|
+
assert(aiResult[0].aiAnalysisUnavailable === true, 'Should mark AI analysis as unavailable when no provider');
|
|
195
|
+
console.log(' OK\n');
|
|
196
|
+
|
|
197
|
+
// --- Test 10: ArchRiskAssessor ---
|
|
198
|
+
console.log(' [10] ArchRiskAssessor...');
|
|
199
|
+
const { ArchRiskAssessor } = require('../src/modules/dev-time/arch-risk-assessor');
|
|
200
|
+
const ar = new ArchRiskAssessor(tmpDir);
|
|
201
|
+
const recoveryCode = `
|
|
202
|
+
function recover(error) {
|
|
203
|
+
try {
|
|
204
|
+
doSomething();
|
|
205
|
+
} catch (e) {
|
|
206
|
+
recover(e); // self-invocation in catch — infinite loop risk!
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
const archFindings = ar.analyzeFile('recovery.js', recoveryCode);
|
|
211
|
+
assert(archFindings.some(f => f.ruleId === 'ar-recovery-loop'), 'Should detect recovery loop pattern');
|
|
212
|
+
console.log(' OK\n');
|
|
213
|
+
|
|
214
|
+
// --- Test 11-17: Phase 3 new rules ---
|
|
215
|
+
// Reuse codeQualityRules already imported in Test 5
|
|
216
|
+
const ruleMap = {};
|
|
217
|
+
for (const r of codeQualityRules) ruleMap[r.id] = r;
|
|
218
|
+
const api_response_naming = ruleMap['cq-api-naming-drift'];
|
|
219
|
+
const fetch_no_catch = ruleMap['cq-fetch-no-catch'];
|
|
220
|
+
const console_log_residual = ruleMap['cq-console-log'];
|
|
221
|
+
const missing_await = ruleMap['cq-missing-await'];
|
|
222
|
+
const event_listener_leak = ruleMap['cq-event-listener-leak'];
|
|
223
|
+
|
|
224
|
+
// --- Test 11: Phase 3 new rules - API naming drift ---
|
|
225
|
+
console.log(' [11] API naming drift detection...');
|
|
226
|
+
const namingCtx = {
|
|
227
|
+
files: [
|
|
228
|
+
{ path: 'api1.js', content: 'const data = response.data.items.map(x => x);' },
|
|
229
|
+
{ path: 'api2.js', content: 'const data = response.data.records.map(x => x);' },
|
|
230
|
+
{ path: 'api3.js', content: 'const data = response.data.list.map(x => x);' },
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
const namingResult = api_response_naming.check(namingCtx);
|
|
234
|
+
assert(namingResult && namingResult.length > 0, 'Should detect API naming drift with 3 variants');
|
|
235
|
+
assert(namingResult.some(f => f.severity === 'block'), 'API naming drift should be block severity');
|
|
236
|
+
console.log(' OK\n');
|
|
237
|
+
|
|
238
|
+
// --- Test 12: fetch without catch ---
|
|
239
|
+
console.log(' [12] fetch no-catch detection...');
|
|
240
|
+
const fetchCtx = {
|
|
241
|
+
files: [
|
|
242
|
+
{ path: 'app.js', content: 'fetch("/api/data").then(r => r.json())' },
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
const fetchResult = fetch_no_catch.check(fetchCtx);
|
|
246
|
+
assert(fetchResult && fetchResult.length > 0, 'Should detect fetch without .catch()');
|
|
247
|
+
console.log(' OK\n');
|
|
248
|
+
|
|
249
|
+
// --- Test 13: console.log residual ---
|
|
250
|
+
console.log(' [13] console.log residual detection...');
|
|
251
|
+
const consoleCtx = {
|
|
252
|
+
files: [
|
|
253
|
+
{ path: 'app.js', content: 'console.log("debug1");\nconsole.log("debug2");\nconsole.log("debug3");\nconsole.log("debug4");\nconsole.log("debug5");\nconsole.log("debug6");' },
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
const consoleResult = console_log_residual.check(consoleCtx);
|
|
257
|
+
assert(consoleResult && consoleResult.some(f => f.severity === 'warn'), 'Should warn on excessive console.log');
|
|
258
|
+
console.log(' OK\n');
|
|
259
|
+
|
|
260
|
+
// --- Test 14: missing await ---
|
|
261
|
+
console.log(' [14] missing await detection...');
|
|
262
|
+
const awaitCtx = {
|
|
263
|
+
files: [
|
|
264
|
+
{ path: 'app.js', content: 'loadData();\nfetchData();' },
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
const awaitResult = missing_await.check(awaitCtx);
|
|
268
|
+
assert(awaitResult && awaitResult.length > 0, 'Should detect missing await on async-like calls');
|
|
269
|
+
console.log(' OK\n');
|
|
270
|
+
|
|
271
|
+
// --- Test 15: event listener leak ---
|
|
272
|
+
console.log(' [15] event listener leak detection...');
|
|
273
|
+
const leakCtx = {
|
|
274
|
+
files: [
|
|
275
|
+
{ path: 'app.js', content: Array(101).fill('document.addEventListener("click", handler);').join('\n') },
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
const leakResult = event_listener_leak.check(leakCtx);
|
|
279
|
+
assert(leakResult && leakResult.length > 0, 'Should detect event listener without cleanup');
|
|
280
|
+
console.log(' OK\n');
|
|
281
|
+
|
|
282
|
+
// --- Test 16: inject command generates .cursorrules ---
|
|
283
|
+
console.log(' [16] inject command (convention file generation)...');
|
|
284
|
+
// Test by generating content directly (reuse ConventionStore from Test 4)
|
|
285
|
+
const injectStore = new ConventionStore(tmpDir);
|
|
286
|
+
injectStore.add({ id: 'test-rule', rule: 'Test rule', severity: 'block', category: 'api' });
|
|
287
|
+
const promptBlock = injectStore.getPromptBlock({});
|
|
288
|
+
assert(promptBlock.includes('test-rule'), 'Prompt block should contain convention id');
|
|
289
|
+
assert(promptBlock.includes('Test rule'), 'Prompt block should contain convention rule');
|
|
290
|
+
console.log(' OK\n');
|
|
291
|
+
|
|
292
|
+
// --- Test 17: AI adapter Chinese prompt ---
|
|
293
|
+
console.log(' [17] AI adapter Chinese prompt...');
|
|
294
|
+
const { buildPrompt } = ai;
|
|
295
|
+
const testFindings = [{ ruleId: 'test', severity: 'warn', message: '测试问题', file: 'test.js' }];
|
|
296
|
+
const { systemPrompt, userPrompt } = buildPrompt(testFindings, {});
|
|
297
|
+
assert(systemPrompt.includes('根因分析'), 'System prompt should be in Chinese');
|
|
298
|
+
assert(userPrompt.includes('测试问题'), 'User prompt should contain finding message');
|
|
299
|
+
assert(userPrompt.includes('发现'), 'User prompt should use Chinese labels');
|
|
300
|
+
console.log(' OK\n');
|
|
301
|
+
|
|
302
|
+
// --- Test 18: Inline ignore comments ---
|
|
303
|
+
console.log(' [18] inline ignore comments...');
|
|
304
|
+
const { filterIgnored } = require('../src/cli/shared/inline-ignore');
|
|
305
|
+
const ignoreCode = [
|
|
306
|
+
'const x = data.items.map(i => i);',
|
|
307
|
+
'// devassist-ignore-next-line',
|
|
308
|
+
'const y = data.records.map(r => r);',
|
|
309
|
+
'const z = data.list.map(l => l);',
|
|
310
|
+
].join('\n');
|
|
311
|
+
const ignoreFindings = [
|
|
312
|
+
{ file: 'test.js', line: 1, ruleId: 'cq-api-naming-drift', severity: 'block', message: 'items' },
|
|
313
|
+
{ file: 'test.js', line: 3, ruleId: 'cq-api-naming-drift', severity: 'block', message: 'records' },
|
|
314
|
+
{ file: 'test.js', line: 4, ruleId: 'cq-api-naming-drift', severity: 'block', message: 'list' },
|
|
315
|
+
];
|
|
316
|
+
const filtered = filterIgnored(ignoreFindings, [{ path: 'test.js', content: ignoreCode }]);
|
|
317
|
+
assert(filtered.length === 2, 'Should filter out the ignored line (line 3), keeping 2 of 3');
|
|
318
|
+
assert(!filtered.some(f => f.line === 3), 'Line 3 should be filtered out by ignore-next-line');
|
|
319
|
+
console.log(' OK\n');
|
|
320
|
+
|
|
321
|
+
// --- Test 19: Config loader ---
|
|
322
|
+
console.log(' [19] config loader...');
|
|
323
|
+
const { loadConfig, applyConfigToFindings } = require('../src/cli/shared/config-loader');
|
|
324
|
+
const testConfig = { maxFileLines: 500, rules: { 'cq-magic-numbers': { enabled: false }, 'cq-console-log': { severity: 'warn' } } };
|
|
325
|
+
const testFindings2 = [
|
|
326
|
+
{ ruleId: 'cq-magic-numbers', severity: 'info', message: 'magic number 42', file: 'a.js', line: 1 },
|
|
327
|
+
{ ruleId: 'cq-console-log', severity: 'info', message: 'console.log', file: 'b.js', line: 2 },
|
|
328
|
+
{ ruleId: 'cq-null-safety', severity: 'warn', message: 'null risk', file: 'c.js', line: 3 },
|
|
329
|
+
];
|
|
330
|
+
const filtered2 = applyConfigToFindings(testConfig, testFindings2);
|
|
331
|
+
assert(filtered2.length === 2, 'Should disable magic-numbers rule, keeping 2 of 3');
|
|
332
|
+
assert(!filtered2.some(f => f.ruleId === 'cq-magic-numbers'), 'Magic numbers should be disabled');
|
|
333
|
+
assert(filtered2.some(f => f.ruleId === 'cq-console-log' && f.severity === 'warn'), 'Console log severity should be overridden to warn');
|
|
334
|
+
console.log(' OK\n');
|
|
335
|
+
|
|
336
|
+
// --- Test 20: Code context extractor ---
|
|
337
|
+
console.log(' [20] code context extractor...');
|
|
338
|
+
const { extractContext, enrichWithContext } = require('../src/cli/shared/code-context');
|
|
339
|
+
const testCode = 'line1\nline2\nline3\nline4\nline5\nconst bad = data.items;\nline7\nline8\nline9\nline10\nline11';
|
|
340
|
+
const ctx2 = extractContext(testCode, 6, 2);
|
|
341
|
+
assert(ctx2.includes('>>>'), 'Context should mark the target line with >>>');
|
|
342
|
+
assert(ctx2.includes('data.items'), 'Context should include the problematic line');
|
|
343
|
+
const enriched = enrichWithContext(
|
|
344
|
+
[{ file: 'test.js', line: 6, ruleId: 'test', severity: 'warn', message: 'test' }],
|
|
345
|
+
[{ path: 'test.js', content: testCode }]
|
|
346
|
+
);
|
|
347
|
+
assert(enriched[0].codeContext && enriched[0].codeContext.includes('>>>'), 'Finding should be enriched with code context');
|
|
348
|
+
console.log(' OK\n');
|
|
349
|
+
|
|
350
|
+
// --- Test 21: Shared file collector ---
|
|
351
|
+
console.log(' [21] shared file collector...');
|
|
352
|
+
const { collectFileContents } = require('../src/cli/shared/file-collector');
|
|
353
|
+
// Collect files from the devassist project itself
|
|
354
|
+
const collected = collectFileContents(path.join(__dirname, '..'));
|
|
355
|
+
assert(collected.length > 0, 'Should collect source files from devassist project');
|
|
356
|
+
assert(collected.some(f => f.path.includes('check.js')), 'Should include check.js');
|
|
357
|
+
assert(collected.every(f => typeof f.content === 'string'), 'All files should have content');
|
|
358
|
+
console.log(' OK\n');
|
|
359
|
+
|
|
360
|
+
// --- Test 22: Security and performance rules (Phase 5) ---
|
|
361
|
+
console.log(' [22] security and performance rules...');
|
|
362
|
+
{
|
|
363
|
+
const { engine: eng } = require('../src/core/rule-engine');
|
|
364
|
+
const { codeQualityRules: rules5 } = require('../src/modules/dev-time/code-quality-guard');
|
|
365
|
+
eng.registerBatch(rules5);
|
|
366
|
+
|
|
367
|
+
const secCode = `
|
|
368
|
+
const crypto = require('crypto');
|
|
369
|
+
const { exec } = require('child_process');
|
|
370
|
+
function parseConfig(str) { return eval(str); }
|
|
371
|
+
function getUser(name) { return db.query(\`SELECT * FROM users WHERE name = '\${name}'\`); }
|
|
372
|
+
const apiKey = 'sk-1234567890abcdef1234567890abcdef';
|
|
373
|
+
const password = 'supersecret123';
|
|
374
|
+
function runCmd(input) { exec('ls ' + input, () => {}); }
|
|
375
|
+
function hashPwd(pwd) { return crypto.createHash('md5').update(pwd).digest('hex'); }
|
|
376
|
+
function render(data) { document.getElementById('out').innerHTML = data; }
|
|
377
|
+
function merge(body) { return Object.assign({}, JSON.parse(body)); }
|
|
378
|
+
`;
|
|
379
|
+
const perfCode = `
|
|
380
|
+
const fs = require('fs');
|
|
381
|
+
async function readFile(p) { return fs.readFileSync(p, 'utf-8'); }
|
|
382
|
+
async function process(users) { for (const u of users) { await fetchProfile(u.id); } }
|
|
383
|
+
function buildHTML(items) { let html = ''; for (const i of items) { html += '<li>' + i + '</li>'; } return html; }
|
|
384
|
+
function validateAll(inputs) { for (const input of inputs) { const re = new RegExp('^[a-z]+$'); if (!re.test(input)) return false; } return true; }
|
|
385
|
+
function parseData(raw) { return JSON.parse(raw); }
|
|
386
|
+
async function getAll() { return await Model.find({}); }
|
|
387
|
+
`;
|
|
388
|
+
const ctx5 = {
|
|
389
|
+
files: [
|
|
390
|
+
{ path: 'security-sample.js', content: secCode },
|
|
391
|
+
{ path: 'perf-sample.js', content: perfCode },
|
|
392
|
+
],
|
|
393
|
+
projectRoot: '.',
|
|
394
|
+
config: {},
|
|
395
|
+
};
|
|
396
|
+
const result5 = eng.run(ctx5);
|
|
397
|
+
const ids = result5.findings.map(f => f.ruleId);
|
|
398
|
+
assert(ids.includes('sec-eval-usage'), 'sec-eval-usage triggers');
|
|
399
|
+
assert(ids.includes('sec-sql-injection'), 'sec-sql-injection triggers');
|
|
400
|
+
assert(ids.includes('sec-hardcoded-secrets'), 'sec-hardcoded-secrets triggers');
|
|
401
|
+
assert(ids.includes('sec-command-injection'), 'sec-command-injection triggers');
|
|
402
|
+
assert(ids.includes('sec-insecure-crypto'), 'sec-insecure-crypto triggers');
|
|
403
|
+
assert(ids.includes('sec-xss-risk'), 'sec-xss-risk triggers');
|
|
404
|
+
assert(ids.includes('sec-proto-pollution'), 'sec-proto-pollution triggers');
|
|
405
|
+
assert(ids.includes('perf-sync-io-in-async'), 'perf-sync-io-in-async triggers');
|
|
406
|
+
assert(ids.includes('perf-n-plus-1'), 'perf-n-plus-1 triggers');
|
|
407
|
+
assert(ids.includes('perf-string-concat-loop'), 'perf-string-concat-loop triggers');
|
|
408
|
+
assert(ids.includes('perf-regex-in-loop'), 'perf-regex-in-loop triggers');
|
|
409
|
+
assert(ids.includes('perf-json-parse-unsafe'), 'perf-json-parse-unsafe triggers');
|
|
410
|
+
assert(ids.includes('perf-no-pagination'), 'perf-no-pagination triggers');
|
|
411
|
+
}
|
|
412
|
+
console.log(' OK\n');
|
|
413
|
+
|
|
414
|
+
// --- Summary ---
|
|
415
|
+
console.log(' ====================');
|
|
416
|
+
console.log(` ${passed} passed, ${failed} failed`);
|
|
417
|
+
if (failed > 0) {
|
|
418
|
+
console.log('\n Failures:');
|
|
419
|
+
failures.forEach(f => console.log(` - ${f}`));
|
|
420
|
+
}
|
|
421
|
+
console.log();
|
|
422
|
+
|
|
423
|
+
// Cleanup
|
|
424
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
|
425
|
+
|
|
426
|
+
return failed === 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
runTests().then(success => {
|
|
430
|
+
process.exit(success ? 0 : 1);
|
|
431
|
+
}).catch(err => {
|
|
432
|
+
console.error('Test suite error:', err);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
});
|