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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/bin/devassist.js +220 -0
  4. package/package.json +44 -0
  5. package/src/ai/adapter.js +464 -0
  6. package/src/ai/providers/claude.js +80 -0
  7. package/src/ai/providers/hunyuan.js +87 -0
  8. package/src/ai/providers/openai.js +74 -0
  9. package/src/ai/providers/qwen.js +81 -0
  10. package/src/cli/commands/ai.js +944 -0
  11. package/src/cli/commands/ask.js +79 -0
  12. package/src/cli/commands/backup.js +30 -0
  13. package/src/cli/commands/check.js +327 -0
  14. package/src/cli/commands/clean.js +130 -0
  15. package/src/cli/commands/comparison-report.js +326 -0
  16. package/src/cli/commands/convention.js +91 -0
  17. package/src/cli/commands/debt.js +49 -0
  18. package/src/cli/commands/deploy.js +88 -0
  19. package/src/cli/commands/diff.js +193 -0
  20. package/src/cli/commands/doctor.js +186 -0
  21. package/src/cli/commands/fix.js +195 -0
  22. package/src/cli/commands/init.js +431 -0
  23. package/src/cli/commands/inject.js +254 -0
  24. package/src/cli/commands/report.js +310 -0
  25. package/src/cli/commands/restore.js +78 -0
  26. package/src/cli/commands/schema.js +93 -0
  27. package/src/cli/commands/watch.js +212 -0
  28. package/src/cli/shared/code-context.js +51 -0
  29. package/src/cli/shared/config-loader.js +89 -0
  30. package/src/cli/shared/file-collector.js +116 -0
  31. package/src/cli/shared/inline-ignore.js +142 -0
  32. package/src/cli/shared/watch-list.js +281 -0
  33. package/src/core/event-bus.js +83 -0
  34. package/src/core/fsm.js +103 -0
  35. package/src/core/logger.js +54 -0
  36. package/src/core/rule-engine.js +117 -0
  37. package/src/index.js +64 -0
  38. package/src/modules/dev-time/arch-risk-assessor.js +250 -0
  39. package/src/modules/dev-time/code-quality-guard.js +1340 -0
  40. package/src/modules/dev-time/convention-store.js +201 -0
  41. package/src/modules/dev-time/impact-analyzer.js +292 -0
  42. package/src/modules/dev-time/pre-deploy-guard.js +284 -0
  43. package/src/modules/dev-time/schema-registry.js +284 -0
  44. package/src/modules/dev-time/tech-debt-tracker.js +225 -0
  45. package/src/modules/dev-time/version-manager.js +280 -0
  46. package/src/modules/runtime/channel-agent.js +404 -0
  47. package/src/modules/runtime/channel-middleware.js +316 -0
  48. package/src/modules/runtime/index.js +64 -0
  49. package/src/modules/runtime/infrastructure-guard.js +582 -0
  50. package/src/modules/runtime/notification-center.js +443 -0
  51. package/src/modules/runtime/schema-gatekeeper.js +664 -0
  52. package/src/modules/runtime/tool-registry.js +329 -0
  53. package/templates/ci/github-actions.yml +60 -0
  54. package/templates/ci/gitlab-ci.yml +30 -0
  55. package/templates/ci/pre-commit-hook.sh +18 -0
  56. package/templates/git-hooks/pre-commit +61 -0
  57. package/tests/run-all.js +434 -0
  58. package/tests/test-layer2.js +461 -0
  59. package/tests/test-new-rules.js +157 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * DevAssist Layer 2 Integration Tests
3
+ *
4
+ * Tests the 6 runtime base code modules:
5
+ * 1. ToolRegistry - register/unregister/invoke/healthCheck/reload
6
+ * 2. ChannelAgent - protocol handling/routing/FSM/telemetry
7
+ * 3. ChannelMiddleware - compose/auth/rateLimit/schema
8
+ * 4. InfrastructureGuard - probes/FSM/adaptive frequency
9
+ * 5. NotificationCenter - channels/dedup/rate limit
10
+ * 6. SchemaGatekeeper - 4-layer validation/security checks
11
+ * 7. Integration: full request flow (middleware -> agent -> tool -> response)
12
+ */
13
+
14
+ let passed = 0;
15
+ let failed = 0;
16
+ const failures = [];
17
+
18
+ function assert(condition, message) {
19
+ if (condition) {
20
+ passed++;
21
+ } else {
22
+ failed++;
23
+ failures.push(message);
24
+ console.log(` FAIL: ${message}`);
25
+ }
26
+ }
27
+
28
+ async function runTests() {
29
+ console.log('\n DevAssist Layer 2 Integration Tests\n ===================================\n');
30
+
31
+ // --- Test 1: ToolRegistry ---
32
+ console.log(' [1] ToolRegistry...');
33
+ const { ToolRegistry, MODULE_TYPES } = require('../src/modules/runtime/tool-registry');
34
+
35
+ const reg = new ToolRegistry();
36
+
37
+ // Register a tool
38
+ const testTool = {
39
+ id: 'test.echo',
40
+ name: 'Echo Tool',
41
+ version: '1.0.0',
42
+ type: MODULE_TYPES.RESIDENT,
43
+ category: 'test',
44
+ schema: {
45
+ input: { required: ['message'], properties: { message: 'string' } },
46
+ output: { required: ['echo'], properties: { echo: 'string' } },
47
+ },
48
+ handler: async (input) => ({ echo: input.message }),
49
+ healthCheck: async () => ({ healthy: true, details: { note: 'always healthy' } }),
50
+ };
51
+ await reg.register(testTool);
52
+ assert(reg.get('test.echo') !== null, 'Tool should be registered');
53
+ assert(reg.list().length === 1, 'Should list 1 tool');
54
+ assert(reg.getManifest()[0].id === 'test.echo', 'Manifest should include tool');
55
+
56
+ // Invoke tool
57
+ const result = await reg.invoke('test.echo', { message: 'hello' }, { requestId: 't1' });
58
+ assert(result.echo === 'hello', 'Tool should echo the message');
59
+
60
+ // Health check
61
+ const health = await reg.healthCheck();
62
+ assert(health.healthy === true, 'All tools should be healthy');
63
+ assert(health.tools[0].id === 'test.echo', 'Health check should include tool id');
64
+
65
+ // Stats
66
+ const stats = reg.getStats();
67
+ assert(stats.totalTools === 1, 'Stats should show 1 tool');
68
+ assert(stats.totalInvocations === 1, 'Stats should show 1 invocation');
69
+
70
+ // Unregister
71
+ await reg.unregister('test.echo');
72
+ assert(reg.get('test.echo') === null, 'Tool should be unregistered');
73
+ assert(reg.list().length === 0, 'Should list 0 tools after unregister');
74
+
75
+ // Register validation: missing field
76
+ try {
77
+ await reg.register({ id: 'bad', name: 'Bad' }); // missing required fields
78
+ assert(false, 'Should reject tool with missing fields');
79
+ } catch (e) {
80
+ assert(true, 'Should reject tool with missing fields');
81
+ }
82
+
83
+ // Register validation: duplicate id
84
+ await reg.register(testTool);
85
+ try {
86
+ await reg.register(testTool);
87
+ assert(false, 'Should reject duplicate tool id');
88
+ } catch (e) {
89
+ assert(true, 'Should reject duplicate tool id');
90
+ }
91
+
92
+ // Error in handler
93
+ const errorTool = {
94
+ id: 'test.error',
95
+ name: 'Error Tool',
96
+ version: '1.0.0',
97
+ type: MODULE_TYPES.RESIDENT,
98
+ category: 'test',
99
+ schema: { input: { required: [], properties: {} }, output: { required: [], properties: {} } },
100
+ handler: async () => { throw new Error('intentional error'); },
101
+ };
102
+ await reg.register(errorTool);
103
+ try {
104
+ await reg.invoke('test.error', {}, {});
105
+ assert(false, 'Should throw on tool error');
106
+ } catch (e) {
107
+ assert(e.message === 'intentional error', 'Should propagate tool error');
108
+ assert(stats.totalErrors === 0 || reg.getStats().totalErrors > 0, 'Should track error count');
109
+ }
110
+
111
+ console.log(' OK\n');
112
+
113
+ // --- Test 2: ChannelAgent ---
114
+ console.log(' [2] ChannelAgent...');
115
+ const { ChannelAgent, PROTOCOL_VERSION, STATE, PRIORITY } = require('../src/modules/runtime/channel-agent');
116
+
117
+ const reg2 = new ToolRegistry();
118
+ await reg2.register({
119
+ id: 'chat.send',
120
+ name: 'Chat Send',
121
+ version: '1.0.0',
122
+ type: MODULE_TYPES.RESIDENT,
123
+ category: 'chat',
124
+ schema: { input: { required: ['message'], properties: { message: 'string' } }, output: { required: [], properties: {} } },
125
+ handler: async (input) => ({ reply: `Echo: ${input.message}` }),
126
+ });
127
+
128
+ const agent = new ChannelAgent({ registry: reg2 });
129
+ await agent.init();
130
+ assert(agent.state === STATE.ONLINE, 'Agent should be ONLINE after init');
131
+
132
+ // Route action to tool
133
+ agent.route('chat.send', 'chat.send', { priority: PRIORITY.NORMAL });
134
+
135
+ // Handle protocol request
136
+ const reqBody = {
137
+ protocol: PROTOCOL_VERSION,
138
+ seq: 1,
139
+ action: 'chat.send',
140
+ timestamp: Date.now(),
141
+ token: 'test-token',
142
+ payload: { message: 'hello world' },
143
+ signature: '',
144
+ };
145
+ const response = await agent.handleRequest(reqBody, { ip: '127.0.0.1' });
146
+ assert(response.protocol === PROTOCOL_VERSION, 'Response should have protocol version');
147
+ assert(response.code === 200, 'Response should be 200 OK');
148
+ assert(response.data.reply === 'Echo: hello world', 'Response data should contain echo');
149
+
150
+ // Unknown action
151
+ const unknownReq = { ...reqBody, action: 'unknown.action', seq: 2 };
152
+ const unknownRes = await agent.handleRequest(unknownReq, {});
153
+ assert(unknownRes.code === 404, 'Unknown action should return 404');
154
+
155
+ // Built-in heartbeat
156
+ const hbReq = { protocol: PROTOCOL_VERSION, seq: 3, action: 'heartbeat.ping', timestamp: Date.now(), payload: {} };
157
+ const hbRes = await agent.handleRequest(hbReq, {});
158
+ assert(hbRes.code === 200, 'Heartbeat should return 200');
159
+ assert(hbRes.data.alive === true, 'Heartbeat should say alive');
160
+
161
+ // Server state
162
+ const stateReq = { protocol: PROTOCOL_VERSION, seq: 4, action: 'channel.state', timestamp: Date.now(), payload: {} };
163
+ const stateRes = await agent.handleRequest(stateReq, {});
164
+ assert(stateRes.code === 200, 'State request should return 200');
165
+ assert(stateRes.data.state === STATE.ONLINE, 'State should be ONLINE');
166
+
167
+ // Learning report
168
+ const report = agent.getLearningReport();
169
+ assert(report.totalRequests >= 1, 'Learning should track requests');
170
+
171
+ // Telemetry
172
+ agent.storeTelemetry('client-1', { state: 'online', queue_length: 0 });
173
+ const serverState = agent.getServerState();
174
+ assert(serverState.activeClients >= 1, 'Should have at least 1 active client');
175
+
176
+ agent.destroy();
177
+ assert(agent.state === STATE.OFFLINE, 'Agent should be OFFLINE after destroy');
178
+
179
+ console.log(' OK\n');
180
+
181
+ // --- Test 3: ChannelMiddleware ---
182
+ console.log(' [3] ChannelMiddleware...');
183
+ const { compose, requestLogger, authGuard, rateLimiter, schemaValidator, errorHandler } = require('../src/modules/runtime/channel-middleware');
184
+
185
+ // compose basic
186
+ const order = [];
187
+ const mw1 = async (ctx, next) => { order.push('mw1-before'); await next(); order.push('mw1-after'); };
188
+ const mw2 = async (ctx, next) => { order.push('mw2-before'); await next(); order.push('mw2-after'); };
189
+ const composed = compose([mw1, mw2]);
190
+ await composed({}, async () => { order.push('handler'); });
191
+ assert(JSON.stringify(order) === JSON.stringify(['mw1-before','mw2-before','handler','mw2-after','mw1-after']),
192
+ 'Middleware should execute in onion order');
193
+
194
+ // Auth guard: excluded action should pass
195
+ const authCtx = { body: { action: 'heartbeat.ping' } };
196
+ let authNextCalled = false;
197
+ await authGuard()({ ...authCtx }, async () => { authNextCalled = true; });
198
+ assert(authNextCalled, 'Auth guard should allow excluded actions');
199
+
200
+ // Auth guard: non-excluded without token should be blocked
201
+ const authCtx2 = { body: { action: 'chat.send', seq: 1 } };
202
+ let authNextCalled2 = false;
203
+ await authGuard()(authCtx2, async () => { authNextCalled2 = true; });
204
+ assert(!authNextCalled2, 'Auth guard should block requests without token');
205
+ assert(authCtx2.response?.code === 401, 'Auth guard should return 401');
206
+
207
+ // Auth guard: with token should pass
208
+ const authCtx3 = { body: { action: 'chat.send', token: 'fake.jwt.token', seq: 1 } };
209
+ let authNextCalled3 = false;
210
+ await authGuard()(authCtx3, async () => { authNextCalled3 = true; });
211
+ assert(authNextCalled3, 'Auth guard should allow requests with token');
212
+
213
+ // Rate limiter
214
+ const rlCtx = { ip: '10.0.0.1' };
215
+ const rl = rateLimiter({ windowMs: 1000, max: 2 });
216
+ let rlPassed = 0;
217
+ for (let i = 0; i < 3; i++) {
218
+ let called = false;
219
+ await rl({ ...rlCtx }, async () => { called = true; });
220
+ if (called) rlPassed++;
221
+ }
222
+ assert(rlPassed === 2, 'Rate limiter should allow 2 of 3 requests');
223
+
224
+ // Schema validator
225
+ const sv = schemaValidator({
226
+ getSchema: (action) => action === 'test' ? { required: ['name'], properties: { name: 'string' } } : null,
227
+ });
228
+ const svCtx = { body: { action: 'test', payload: { name: 123 }, seq: 1 } };
229
+ let svCalled = false;
230
+ await sv(svCtx, async () => { svCalled = true; });
231
+ assert(!svCalled, 'Schema validator should block type mismatch');
232
+ assert(svCtx.response?.code === 400, 'Schema validator should return 400');
233
+
234
+ // Error handler catches errors
235
+ let ehResponse = null;
236
+ await errorHandler()({}, async () => { throw new Error('test error'); }).then(() => {});
237
+ // errorHandler sets ctx.response but ctx is a new object each time
238
+ // So we need to pass a persistent ctx
239
+ const ehCtx = {};
240
+ await errorHandler()(ehCtx, async () => { throw new Error('test error'); });
241
+ assert(ehCtx.response?.code === 500, 'Error handler should return 500');
242
+
243
+ console.log(' OK\n');
244
+
245
+ // --- Test 4: InfrastructureGuard ---
246
+ console.log(' [4] InfrastructureGuard...');
247
+ const { InfrastructureGuard, MemoryProbe, DiskProbe, HEALTH_STATE } = require('../src/modules/runtime/infrastructure-guard');
248
+
249
+ const guard = new InfrastructureGuard();
250
+ guard.addMemoryProbe({ maxHeapMB: 4096, interval: 999999 }); // High limit so it's always healthy
251
+ guard.addDiskProbe({ path: process.cwd(), minFreeMB: 1, interval: 999999 });
252
+
253
+ // Check now
254
+ const checkResult = await guard.checkNow();
255
+ assert(checkResult.healthy === true, 'Guard should be healthy with high limits');
256
+ assert(checkResult.probes.memory !== undefined, 'Should have memory probe result');
257
+ assert(checkResult.probes.disk !== undefined, 'Should have disk probe result');
258
+ assert(checkResult.state === HEALTH_STATE.HEALTHY, 'State should be HEALTHY');
259
+
260
+ // Learning
261
+ const learning = checkResult.learning;
262
+ assert(learning.totalChecks >= 1, 'Learning should track checks');
263
+
264
+ // Status
265
+ const status = guard.getStatus();
266
+ assert(status.state === HEALTH_STATE.HEALTHY, 'Status state should be HEALTHY');
267
+ assert(status.probes.memory !== undefined, 'Status should have memory probe');
268
+
269
+ // Probe that fails
270
+ const guard2 = new InfrastructureGuard();
271
+ guard2.addMemoryProbe({ maxHeapMB: 1, interval: 999999 }); // Very low limit → always fails
272
+ const failResult = await guard2.checkNow();
273
+ assert(failResult.healthy === false, 'Guard should be unhealthy with 1MB heap limit');
274
+ assert(failResult.probes.memory.healthy === false, 'Memory probe should fail');
275
+
276
+ console.log(' OK\n');
277
+
278
+ // --- Test 5: NotificationCenter ---
279
+ console.log(' [5] NotificationCenter...');
280
+ const { NotificationCenter, LogChannel, LEVELS } = require('../src/modules/runtime/notification-center');
281
+
282
+ const nc = new NotificationCenter();
283
+ // Log channel is registered by default
284
+ assert(nc.getChannels().includes('log'), 'Log channel should be registered by default');
285
+
286
+ // Send notification
287
+ await nc.info('Test Info', 'This is a test info message', 'test');
288
+ await nc.warn('Test Warn', 'This is a test warning', 'test');
289
+ await nc.error('Test Error', 'This is a test error', 'test');
290
+ await nc.critical('Test Critical', 'This is a test critical', 'test');
291
+
292
+ const ncStats = nc.getStats();
293
+ assert(ncStats.totalSent >= 4, 'Should have sent at least 4 notifications');
294
+
295
+ // Dedup test
296
+ const nc2 = new NotificationCenter();
297
+ await nc2.error('Same Error', 'Same message', 'same-source');
298
+ await nc2.error('Same Error', 'Same message', 'same-source'); // Should be deduped
299
+ assert(nc2.getStats().totalSent === 1, 'Duplicate notification should be deduped');
300
+
301
+ // Level filtering: log channel has minLevel INFO, so INFO should pass
302
+ const nc3 = new NotificationCenter();
303
+ // Register a channel with high min level
304
+ const { Channel } = require('../src/modules/runtime/notification-center');
305
+ class TestChannel extends Channel {
306
+ constructor() { super('test', { minLevel: LEVELS.ERROR }); this.received = []; }
307
+ async send(n) { this.received.push(n); this._sentCount++; return true; }
308
+ }
309
+ const tc = new TestChannel();
310
+ nc3.registerChannel(tc);
311
+ await nc3.info('Info', 'info msg', 'test'); // Should NOT reach test channel
312
+ await nc3.error('Error', 'error msg', 'test'); // Should reach test channel
313
+ assert(tc.received.length === 1, 'Test channel should only receive ERROR, not INFO');
314
+
315
+ console.log(' OK\n');
316
+
317
+ // --- Test 6: SchemaGatekeeper ---
318
+ console.log(' [6] SchemaGatekeeper...');
319
+ const { gatekeeper, LAYERS } = require('../src/modules/runtime/schema-gatekeeper');
320
+
321
+ // Valid data
322
+ const validData = { name: 'test', age: 25 };
323
+ const validSchema = { required: ['name'], properties: { name: 'string', age: 'number' } };
324
+ const validResult = gatekeeper.validate(validData, validSchema, { direction: 'request' });
325
+ assert(validResult.passed === true, 'Valid data should pass validation');
326
+
327
+ // Missing required field
328
+ const missingData = { age: 25 };
329
+ const missingResult = gatekeeper.validate(missingData, validSchema, { direction: 'request' });
330
+ assert(missingResult.passed === false, 'Missing required field should fail');
331
+ assert(missingResult.rejections.length > 0, 'Should have rejection details');
332
+
333
+ // Type mismatch
334
+ const badTypeData = { name: 123, age: 'twenty' };
335
+ const badTypeResult = gatekeeper.validate(badTypeData, validSchema, { direction: 'request' });
336
+ assert(badTypeResult.passed === false, 'Type mismatch should fail');
337
+
338
+ // SQL injection
339
+ const sqlData = { query: "'; DROP TABLE users; --" };
340
+ const sqlResult = gatekeeper.validate(sqlData, null, { direction: 'request' });
341
+ assert(sqlResult.passed === false, 'SQL injection should be rejected');
342
+
343
+ // XSS
344
+ const xssData = { html: '<script>alert(1)</script>' };
345
+ const xssResult = gatekeeper.validate(xssData, null, { direction: 'request' });
346
+ assert(xssResult.passed === false, 'XSS should be rejected');
347
+
348
+ // Path traversal
349
+ const pathData = { file: '../../../etc/passwd' };
350
+ const pathResult = gatekeeper.validate(pathData, null, { direction: 'request' });
351
+ assert(pathResult.passed === false, 'Path traversal should be rejected');
352
+
353
+ // Undefined values
354
+ const undefData = { a: undefined, b: 'ok' };
355
+ const undefResult = gatekeeper.validate(undefData, null, { direction: 'request' });
356
+ assert(undefResult.passed === false, 'Undefined values should be rejected');
357
+
358
+ // Sensitive fields in response
359
+ const sensitiveData = { code: 200, data: { password: 'secret123', name: 'user' } };
360
+ const sensitiveResult = gatekeeper.validate(sensitiveData, null, { direction: 'response' });
361
+ assert(sensitiveResult.warnings.some(w => w.message.includes('Sensitive')), 'Sensitive fields should trigger warning');
362
+
363
+ // Stats
364
+ const gkStats = gatekeeper.getStats();
365
+ assert(gkStats.totalChecks >= 8, 'Should have tracked all checks');
366
+ assert(gkStats.ruleCount >= 13, 'Should have at least 13 rules registered');
367
+
368
+ // List rules
369
+ const rules = gatekeeper.listRules();
370
+ assert(rules.length >= 13, 'Should list all rules');
371
+ const layers = new Set(rules.map(r => r.layer));
372
+ assert(layers.has(LAYERS.SYNTAX), 'Should have syntax layer rules');
373
+ assert(layers.has(LAYERS.SCHEMA), 'Should have schema layer rules');
374
+ assert(layers.has(LAYERS.SEMANTIC), 'Should have semantic layer rules');
375
+ assert(layers.has(LAYERS.SECURITY), 'Should have security layer rules');
376
+
377
+ console.log(' OK\n');
378
+
379
+ // --- Test 7: Full Integration (middleware -> agent -> tool -> response) ---
380
+ console.log(' [7] Full Integration Flow...');
381
+ const { registry: sharedRegistry } = require('../src/modules/runtime/tool-registry');
382
+
383
+ // Create fresh registry for integration test
384
+ const intReg = new ToolRegistry();
385
+ await intReg.register({
386
+ id: 'echo.tool',
387
+ name: 'Echo',
388
+ version: '1.0.0',
389
+ type: MODULE_TYPES.RESIDENT,
390
+ category: 'test',
391
+ schema: {
392
+ input: { required: ['message'], properties: { message: 'string' } },
393
+ output: { required: ['reply'], properties: { reply: 'string' } },
394
+ },
395
+ handler: async (input) => ({ reply: input.message }),
396
+ });
397
+
398
+ const intAgent = new ChannelAgent({ registry: intReg });
399
+ await intAgent.init();
400
+ intAgent.route('echo', 'echo.tool', { priority: PRIORITY.NORMAL });
401
+
402
+ // Build middleware chain
403
+ const intMiddleware = compose([
404
+ errorHandler(),
405
+ requestLogger(),
406
+ rateLimiter({ windowMs: 60000, max: 100 }),
407
+ ]);
408
+
409
+ // Simulate a full request
410
+ const intReqBody = {
411
+ protocol: PROTOCOL_VERSION,
412
+ seq: 100,
413
+ action: 'echo',
414
+ timestamp: Date.now(),
415
+ payload: { message: 'integration test' },
416
+ };
417
+
418
+ const intCtx = {
419
+ body: intReqBody,
420
+ ip: '192.168.1.1',
421
+ headers: {},
422
+ };
423
+
424
+ let intResponse = null;
425
+ await intMiddleware(intCtx, async () => {
426
+ intResponse = await intAgent.handleRequest(intReqBody, intCtx);
427
+ });
428
+
429
+ assert(intResponse !== null, 'Should get a response from integration flow');
430
+ assert(intResponse.code === 200, 'Integration response should be 200');
431
+ assert(intResponse.data.reply === 'integration test', 'Integration response should contain echo');
432
+
433
+ // Verify learning tracked it
434
+ const intReport = intAgent.getLearningReport();
435
+ assert(intReport.totalRequests >= 1, 'Learning should track integration request');
436
+ assert(intReport.totalErrors === 0, 'Should have no errors in integration flow');
437
+
438
+ intAgent.destroy();
439
+
440
+ console.log(' OK\n');
441
+
442
+ // --- Summary ---
443
+ console.log(' ===================================');
444
+ console.log(` Layer 2 Tests: ${passed} passed, ${failed} failed\n`);
445
+
446
+ if (failed > 0) {
447
+ console.log(' Failures:');
448
+ failures.forEach(f => console.log(` - ${f}`));
449
+ console.log('');
450
+ }
451
+
452
+ return { passed, failed };
453
+ }
454
+
455
+ // Run
456
+ runTests().then(({ passed, failed }) => {
457
+ process.exit(failed > 0 ? 1 : 0);
458
+ }).catch(err => {
459
+ console.error('Test runner error:', err);
460
+ process.exit(1);
461
+ });
@@ -0,0 +1,157 @@
1
+ // Test file to verify new security and performance rules trigger correctly
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { execSync } = require('child_process');
5
+
6
+ let passed = 0;
7
+ let failed = 0;
8
+
9
+ function assert(condition, msg) {
10
+ if (condition) {
11
+ console.log(' OK - ' + msg);
12
+ passed++;
13
+ } else {
14
+ console.log(' FAIL - ' + msg);
15
+ failed++;
16
+ }
17
+ }
18
+
19
+ // Sample code with security issues
20
+ const securityTestCode = `
21
+ const crypto = require('crypto');
22
+ const { exec } = require('child_process');
23
+
24
+ // sec-eval-usage
25
+ function parseConfig(str) {
26
+ return eval(str);
27
+ }
28
+
29
+ // sec-sql-injection
30
+ function getUser(name) {
31
+ const query = \`SELECT * FROM users WHERE name = '\${name}'\`;
32
+ return db.query(query);
33
+ }
34
+
35
+ // sec-hardcoded-secrets
36
+ const apiKey = 'sk-1234567890abcdef1234567890abcdef';
37
+ const password = 'supersecret123';
38
+
39
+ // sec-command-injection
40
+ function runCommand(userInput) {
41
+ exec('ls ' + userInput, (err, stdout) => {});
42
+ }
43
+
44
+ // sec-insecure-crypto
45
+ function hashPassword(pwd) {
46
+ return crypto.createHash('md5').update(pwd).digest('hex');
47
+ }
48
+
49
+ // sec-xss-risk
50
+ function renderContent(userInput) {
51
+ document.getElementById('output').innerHTML = userInput;
52
+ }
53
+
54
+ // sec-proto-pollution
55
+ function mergeConfig(userBody) {
56
+ return Object.assign({}, JSON.parse(userBody));
57
+ }
58
+ `;
59
+
60
+ // Sample code with performance issues
61
+ const perfTestCode = `
62
+ const fs = require('fs');
63
+ const path = require('path');
64
+
65
+ // perf-sync-io-in-async
66
+ async function readFile(filePath) {
67
+ const content = fs.readFileSync(filePath, 'utf-8');
68
+ return content;
69
+ }
70
+
71
+ // perf-n-plus-1
72
+ async function processUsers(users) {
73
+ for (const user of users) {
74
+ const profile = await fetchProfile(user.id);
75
+ console.log(profile);
76
+ }
77
+ }
78
+
79
+ // perf-string-concat-loop
80
+ function buildHTML(items) {
81
+ let html = '';
82
+ for (const item of items) {
83
+ html += '<li>' + item + '</li>';
84
+ }
85
+ return html;
86
+ }
87
+
88
+ // perf-regex-in-loop
89
+ function validateAll(inputs) {
90
+ for (const input of inputs) {
91
+ const re = new RegExp('^[a-z]+$');
92
+ if (!re.test(input)) return false;
93
+ }
94
+ return true;
95
+ }
96
+
97
+ // perf-json-parse-unsafe
98
+ function parseData(raw) {
99
+ const data = JSON.parse(raw);
100
+ return data;
101
+ }
102
+
103
+ // perf-no-pagination
104
+ async function getAllUsers() {
105
+ const users = await User.find({});
106
+ return users;
107
+ }
108
+ `;
109
+
110
+ console.log('\n[22] new security and performance rules...');
111
+
112
+ // Run check on the test code
113
+ const { engine } = require('../src/core/rule-engine');
114
+ const { codeQualityRules } = require('../src/modules/dev-time/code-quality-guard');
115
+
116
+ engine.registerBatch(codeQualityRules);
117
+
118
+ const ctx = {
119
+ files: [
120
+ { path: 'security-sample.js', content: securityTestCode },
121
+ { path: 'perf-sample.js', content: perfTestCode },
122
+ ],
123
+ projectRoot: '.',
124
+ config: {},
125
+ };
126
+
127
+ const { findings } = engine.run(ctx);
128
+
129
+ // Check each new rule
130
+ const ruleIds = findings.map(f => f.ruleId);
131
+
132
+ assert(ruleIds.includes('sec-eval-usage'), 'sec-eval-usage triggers on eval()');
133
+ assert(ruleIds.includes('sec-sql-injection'), 'sec-sql-injection triggers on string concat SQL');
134
+ assert(ruleIds.includes('sec-hardcoded-secrets'), 'sec-hardcoded-secrets triggers on hardcoded API key');
135
+ assert(ruleIds.includes('sec-command-injection'), 'sec-command-injection triggers on exec + concat');
136
+ assert(ruleIds.includes('sec-insecure-crypto'), 'sec-insecure-crypto triggers on MD5');
137
+ assert(ruleIds.includes('sec-xss-risk'), 'sec-xss-risk triggers on innerHTML');
138
+ assert(ruleIds.includes('sec-proto-pollution'), 'sec-proto-pollution triggers on Object.assign + user input');
139
+
140
+ assert(ruleIds.includes('perf-sync-io-in-async'), 'perf-sync-io-in-async triggers on readFileSync in async');
141
+ assert(ruleIds.includes('perf-n-plus-1'), 'perf-n-plus-1 triggers on await in loop');
142
+ assert(ruleIds.includes('perf-string-concat-loop'), 'perf-string-concat-loop triggers on += in loop');
143
+ assert(ruleIds.includes('perf-regex-in-loop'), 'perf-regex-in-loop triggers on new RegExp in loop');
144
+ assert(ruleIds.includes('perf-json-parse-unsafe'), 'perf-json-parse-unsafe triggers on JSON.parse without try/catch');
145
+ assert(ruleIds.includes('perf-no-pagination'), 'perf-no-pagination triggers on find() without limit');
146
+
147
+ // Verify security rules have severity 'block'
148
+ const blockSecFindings = findings.filter(f => f.category === 'security' && f.severity === 'block');
149
+ assert(blockSecFindings.length >= 4, 'at least 4 security BLOCK findings (eval, sql, secrets, cmd injection, xss)');
150
+
151
+ // Verify performance rules have correct severity
152
+ const perfFindings = findings.filter(f => f.category === 'performance');
153
+ assert(perfFindings.length >= 5, 'at least 5 performance findings');
154
+
155
+ console.log(' ' + findings.length + ' total findings from new rules');
156
+ console.log('');
157
+ console.log(' ' + passed + ' passed, ' + failed + ' failed');