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
|
@@ -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');
|