ucn 3.7.9 → 3.7.10
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 +2 -1
- package/test/accuracy.test.js +0 -1863
- package/test/fixtures/go/go.mod +0 -3
- package/test/fixtures/go/main.go +0 -257
- package/test/fixtures/go/service.go +0 -187
- package/test/fixtures/java/DataService.java +0 -279
- package/test/fixtures/java/Main.java +0 -287
- package/test/fixtures/java/Utils.java +0 -199
- package/test/fixtures/java/pom.xml +0 -6
- package/test/fixtures/javascript/main.js +0 -109
- package/test/fixtures/javascript/package.json +0 -1
- package/test/fixtures/javascript/service.js +0 -88
- package/test/fixtures/javascript/utils.js +0 -67
- package/test/fixtures/python/main.py +0 -198
- package/test/fixtures/python/pyproject.toml +0 -3
- package/test/fixtures/python/service.py +0 -166
- package/test/fixtures/python/utils.py +0 -118
- package/test/fixtures/rust/Cargo.toml +0 -3
- package/test/fixtures/rust/main.rs +0 -253
- package/test/fixtures/rust/service.rs +0 -210
- package/test/fixtures/rust/utils.rs +0 -154
- package/test/fixtures/typescript/main.ts +0 -154
- package/test/fixtures/typescript/package.json +0 -1
- package/test/fixtures/typescript/repository.ts +0 -149
- package/test/fixtures/typescript/types.ts +0 -114
- package/test/mcp-edge-cases.js +0 -634
- package/test/parser.test.js +0 -13634
- package/test/public-repos-bugs.json +0 -32
- package/test/public-repos-test.js +0 -477
- package/test/systematic-test.js +0 -619
package/test/mcp-edge-cases.js
DELETED
|
@@ -1,634 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MCP Server Edge Case Test Suite
|
|
5
|
-
*
|
|
6
|
-
* Tests UCN MCP tools with null/crash safety, input validation,
|
|
7
|
-
* and normal operation edge cases.
|
|
8
|
-
*
|
|
9
|
-
* Communicates with the MCP server over stdio using newline-delimited JSON-RPC.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const { spawn } = require('child_process');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
const SERVER_PATH = path.join(__dirname, '..', 'mcp', 'server.js');
|
|
16
|
-
const PROJECT_DIR = path.resolve(__dirname, '..');
|
|
17
|
-
const TIMEOUT_MS = 30000;
|
|
18
|
-
|
|
19
|
-
// ============================================================================
|
|
20
|
-
// JSON-RPC over stdio transport (newline-delimited JSON)
|
|
21
|
-
// ============================================================================
|
|
22
|
-
|
|
23
|
-
class McpClient {
|
|
24
|
-
constructor() {
|
|
25
|
-
this.proc = null;
|
|
26
|
-
this.requestId = 0;
|
|
27
|
-
this.pending = new Map();
|
|
28
|
-
this.buffer = '';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
start() {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
this.proc = spawn('node', [SERVER_PATH], {
|
|
34
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
-
env: { ...process.env, NODE_ENV: 'test' }
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
this.proc.stderr.on('data', (data) => {
|
|
39
|
-
// MCP server logs to stderr - ignore
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
this.proc.stdout.on('data', (chunk) => {
|
|
43
|
-
this.buffer += chunk.toString();
|
|
44
|
-
this._processBuffer();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
this.proc.on('error', (err) => {
|
|
48
|
-
reject(err);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
this.proc.on('exit', (code) => {
|
|
52
|
-
for (const [id, entry] of this.pending) {
|
|
53
|
-
clearTimeout(entry.timer);
|
|
54
|
-
entry.reject(new Error(`Server exited with code ${code}`));
|
|
55
|
-
}
|
|
56
|
-
this.pending.clear();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Give server a moment to start
|
|
60
|
-
setTimeout(() => resolve(), 500);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
_processBuffer() {
|
|
65
|
-
// Newline-delimited JSON: each message is a single line terminated by \n
|
|
66
|
-
while (true) {
|
|
67
|
-
const nlIndex = this.buffer.indexOf('\n');
|
|
68
|
-
if (nlIndex === -1) break;
|
|
69
|
-
|
|
70
|
-
const line = this.buffer.substring(0, nlIndex).replace(/\r$/, '');
|
|
71
|
-
this.buffer = this.buffer.substring(nlIndex + 1);
|
|
72
|
-
|
|
73
|
-
if (!line.trim()) continue;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const msg = JSON.parse(line);
|
|
77
|
-
this._handleMessage(msg);
|
|
78
|
-
} catch (e) {
|
|
79
|
-
console.error('Failed to parse JSON-RPC message:', e.message, 'line:', line.substring(0, 100));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
_handleMessage(msg) {
|
|
85
|
-
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
86
|
-
const entry = this.pending.get(msg.id);
|
|
87
|
-
clearTimeout(entry.timer);
|
|
88
|
-
this.pending.delete(msg.id);
|
|
89
|
-
if (msg.error) {
|
|
90
|
-
entry.resolve({ error: msg.error });
|
|
91
|
-
} else {
|
|
92
|
-
entry.resolve({ result: msg.result });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Notifications (no id) are ignored
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
send(method, params) {
|
|
99
|
-
const id = ++this.requestId;
|
|
100
|
-
return new Promise((resolve, reject) => {
|
|
101
|
-
const timer = setTimeout(() => {
|
|
102
|
-
this.pending.delete(id);
|
|
103
|
-
reject(new Error('TIMEOUT'));
|
|
104
|
-
}, TIMEOUT_MS);
|
|
105
|
-
|
|
106
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
107
|
-
|
|
108
|
-
const message = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
|
109
|
-
this.proc.stdin.write(message);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
notify(method, params) {
|
|
114
|
-
const message = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
|
|
115
|
-
this.proc.stdin.write(message);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async initialize() {
|
|
119
|
-
const res = await this.send('initialize', {
|
|
120
|
-
protocolVersion: '2025-03-26',
|
|
121
|
-
capabilities: {},
|
|
122
|
-
clientInfo: { name: 'test-client', version: '1.0.0' }
|
|
123
|
-
});
|
|
124
|
-
this.notify('notifications/initialized', {});
|
|
125
|
-
return res;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async callTool(toolName, args) {
|
|
129
|
-
return this.send('tools/call', { name: toolName, arguments: args });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
stop() {
|
|
133
|
-
if (this.proc) {
|
|
134
|
-
this.proc.stdin.end();
|
|
135
|
-
this.proc.kill();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ============================================================================
|
|
141
|
-
// Test definitions
|
|
142
|
-
// ============================================================================
|
|
143
|
-
|
|
144
|
-
const tests = [
|
|
145
|
-
// ========================================================================
|
|
146
|
-
// CATEGORY 1: Null/Crash Safety (nonexistent symbols/files)
|
|
147
|
-
// ========================================================================
|
|
148
|
-
{
|
|
149
|
-
category: 'Null/Crash Safety',
|
|
150
|
-
tool: 'ucn',
|
|
151
|
-
desc: 'about - nonexistent symbol',
|
|
152
|
-
args: { command: 'about', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
category: 'Null/Crash Safety',
|
|
156
|
-
tool: 'ucn',
|
|
157
|
-
desc: 'context - nonexistent symbol',
|
|
158
|
-
args: { command: 'context', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
category: 'Null/Crash Safety',
|
|
162
|
-
tool: 'ucn',
|
|
163
|
-
desc: 'impact - nonexistent symbol',
|
|
164
|
-
args: { command: 'impact', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
category: 'Null/Crash Safety',
|
|
168
|
-
tool: 'ucn',
|
|
169
|
-
desc: 'smart - nonexistent symbol',
|
|
170
|
-
args: { command: 'smart', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
category: 'Null/Crash Safety',
|
|
174
|
-
tool: 'ucn',
|
|
175
|
-
desc: 'trace - nonexistent symbol',
|
|
176
|
-
args: { command: 'trace', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
category: 'Null/Crash Safety',
|
|
180
|
-
tool: 'ucn',
|
|
181
|
-
desc: 'verify - nonexistent symbol',
|
|
182
|
-
args: { command: 'verify', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
category: 'Null/Crash Safety',
|
|
186
|
-
tool: 'ucn',
|
|
187
|
-
desc: 'related - nonexistent symbol',
|
|
188
|
-
args: { command: 'related', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
category: 'Null/Crash Safety',
|
|
192
|
-
tool: 'ucn',
|
|
193
|
-
desc: 'example - nonexistent symbol',
|
|
194
|
-
args: { command: 'example', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
category: 'Null/Crash Safety',
|
|
198
|
-
tool: 'ucn',
|
|
199
|
-
desc: 'fn - nonexistent function',
|
|
200
|
-
args: { command: 'fn', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_function_xyz' }
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
category: 'Null/Crash Safety',
|
|
204
|
-
tool: 'ucn',
|
|
205
|
-
desc: 'class - nonexistent class',
|
|
206
|
-
args: { command: 'class', project_dir: PROJECT_DIR, name: 'ZzzNonexistentClassXyz' }
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
category: 'Null/Crash Safety',
|
|
210
|
-
tool: 'ucn',
|
|
211
|
-
desc: 'tests - nonexistent name',
|
|
212
|
-
args: { command: 'tests', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_test_xyz' }
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
category: 'Null/Crash Safety',
|
|
216
|
-
tool: 'ucn',
|
|
217
|
-
desc: 'typedef - nonexistent type',
|
|
218
|
-
args: { command: 'typedef', project_dir: PROJECT_DIR, name: 'ZzzNonexistentTypeXyz' }
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
category: 'Null/Crash Safety',
|
|
222
|
-
tool: 'ucn',
|
|
223
|
-
desc: 'graph - nonexistent file',
|
|
224
|
-
args: { command: 'graph', project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
category: 'Null/Crash Safety',
|
|
228
|
-
tool: 'ucn',
|
|
229
|
-
desc: 'file_exports - nonexistent file',
|
|
230
|
-
args: { command: 'file_exports', project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
category: 'Null/Crash Safety',
|
|
234
|
-
tool: 'ucn',
|
|
235
|
-
desc: 'imports - nonexistent file',
|
|
236
|
-
args: { command: 'imports', project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
category: 'Null/Crash Safety',
|
|
240
|
-
tool: 'ucn',
|
|
241
|
-
desc: 'exporters - nonexistent file',
|
|
242
|
-
args: { command: 'exporters', project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
{
|
|
246
|
-
category: 'Null/Crash Safety',
|
|
247
|
-
tool: 'ucn',
|
|
248
|
-
desc: 'api - nonexistent file',
|
|
249
|
-
args: { command: 'api', project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
category: 'Null/Crash Safety',
|
|
253
|
-
tool: 'ucn',
|
|
254
|
-
desc: 'lines - nonexistent file',
|
|
255
|
-
args: { command: 'lines', project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js', range: '1-10' }
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
category: 'Null/Crash Safety',
|
|
259
|
-
tool: 'ucn',
|
|
260
|
-
desc: 'expand - no prior context call',
|
|
261
|
-
args: { command: 'expand', project_dir: PROJECT_DIR, item: 1 }
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
// ========================================================================
|
|
265
|
-
// CATEGORY 2: Input Validation
|
|
266
|
-
// ========================================================================
|
|
267
|
-
{
|
|
268
|
-
category: 'Input Validation',
|
|
269
|
-
tool: 'ucn',
|
|
270
|
-
desc: 'find - whitespace-only name',
|
|
271
|
-
args: { command: 'find', project_dir: PROJECT_DIR, name: ' ' }
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
category: 'Input Validation',
|
|
275
|
-
tool: 'ucn',
|
|
276
|
-
desc: 'about - name with special chars "foo()"',
|
|
277
|
-
args: { command: 'about', project_dir: PROJECT_DIR, name: 'foo()' }
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
category: 'Input Validation',
|
|
281
|
-
tool: 'ucn',
|
|
282
|
-
desc: 'search - regex special chars "[test"',
|
|
283
|
-
args: { command: 'search', project_dir: PROJECT_DIR, term: '[test' }
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
category: 'Input Validation',
|
|
287
|
-
tool: 'ucn',
|
|
288
|
-
desc: 'plan - no operation specified',
|
|
289
|
-
args: { command: 'plan', project_dir: PROJECT_DIR, name: 'getIndex' }
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
category: 'Input Validation',
|
|
293
|
-
tool: 'ucn',
|
|
294
|
-
desc: 'stacktrace - empty stack',
|
|
295
|
-
args: { command: 'stacktrace', project_dir: PROJECT_DIR, stack: '' }
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
category: 'Input Validation',
|
|
299
|
-
tool: 'ucn',
|
|
300
|
-
desc: 'toc - nonexistent project_dir',
|
|
301
|
-
args: { command: 'toc', project_dir: '/nonexistent/fake/directory/abc123' }
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
category: 'Input Validation',
|
|
305
|
-
tool: 'ucn',
|
|
306
|
-
desc: 'find - top=0',
|
|
307
|
-
args: { command: 'find', project_dir: PROJECT_DIR, name: 'getIndex', top: 0 }
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
category: 'Input Validation',
|
|
311
|
-
tool: 'ucn',
|
|
312
|
-
desc: 'find - top=-1',
|
|
313
|
-
args: { command: 'find', project_dir: PROJECT_DIR, name: 'getIndex', top: -1 }
|
|
314
|
-
},
|
|
315
|
-
|
|
316
|
-
// ========================================================================
|
|
317
|
-
// CATEGORY 3: Normal Operations (verify no crash)
|
|
318
|
-
// ========================================================================
|
|
319
|
-
{
|
|
320
|
-
category: 'Normal Operations',
|
|
321
|
-
tool: 'ucn',
|
|
322
|
-
desc: 'toc - project overview',
|
|
323
|
-
args: { command: 'toc', project_dir: PROJECT_DIR }
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
category: 'Normal Operations',
|
|
327
|
-
tool: 'ucn',
|
|
328
|
-
desc: 'find - find "getIndex"',
|
|
329
|
-
args: { command: 'find', project_dir: PROJECT_DIR, name: 'getIndex' }
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
category: 'Normal Operations',
|
|
333
|
-
tool: 'ucn',
|
|
334
|
-
desc: 'deadcode - find dead code',
|
|
335
|
-
args: { command: 'deadcode', project_dir: PROJECT_DIR }
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
category: 'Normal Operations',
|
|
339
|
-
tool: 'ucn',
|
|
340
|
-
desc: 'search - search for "TODO"',
|
|
341
|
-
args: { command: 'search', project_dir: PROJECT_DIR, term: 'TODO' }
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
category: 'Normal Operations',
|
|
345
|
-
tool: 'ucn',
|
|
346
|
-
desc: 'api - project API',
|
|
347
|
-
args: { command: 'api', project_dir: PROJECT_DIR }
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
category: 'Normal Operations',
|
|
351
|
-
tool: 'ucn',
|
|
352
|
-
desc: 'stats - project stats',
|
|
353
|
-
args: { command: 'stats', project_dir: PROJECT_DIR }
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
category: 'Normal Operations',
|
|
357
|
-
tool: 'ucn',
|
|
358
|
-
desc: 'lines - extract lines 1-5 from discovery.js',
|
|
359
|
-
args: { command: 'lines', project_dir: PROJECT_DIR, file: 'core/discovery.js', range: '1-5' }
|
|
360
|
-
},
|
|
361
|
-
|
|
362
|
-
// ========================================================================
|
|
363
|
-
// Correctness Assertions
|
|
364
|
-
// ========================================================================
|
|
365
|
-
{
|
|
366
|
-
category: 'Correctness',
|
|
367
|
-
tool: 'ucn',
|
|
368
|
-
desc: 'api(file=nonexistent) returns isError',
|
|
369
|
-
args: { command: 'api', project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' },
|
|
370
|
-
assert: (res, text, isError) => isError === true || 'Expected isError: true for nonexistent file'
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
category: 'Correctness',
|
|
374
|
-
tool: 'ucn',
|
|
375
|
-
desc: 'api(file=nonexistent) message contains "not found"',
|
|
376
|
-
args: { command: 'api', project_dir: PROJECT_DIR, file: 'nonexistent.js' },
|
|
377
|
-
assert: (res, text, isError) => (isError && /not found/i.test(text)) || 'Expected file-not-found error message'
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
category: 'Correctness',
|
|
381
|
-
tool: 'ucn',
|
|
382
|
-
desc: 'smart(nonexistent) returns "not found" message',
|
|
383
|
-
args: { command: 'smart', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
384
|
-
assert: (res, text, isError) => (!isError && /not found/i.test(text)) || 'Expected "not found" message for nonexistent smart target'
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
category: 'Correctness',
|
|
388
|
-
tool: 'ucn',
|
|
389
|
-
desc: 'context(nonexistent) returns "not found" message',
|
|
390
|
-
args: { command: 'context', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
391
|
-
assert: (res, text, isError) => (!isError && /not found/i.test(text)) || 'Expected "not found" message for nonexistent context target'
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
category: 'Correctness',
|
|
395
|
-
tool: 'ucn',
|
|
396
|
-
desc: 'example(nonexistent) returns "no examples" message',
|
|
397
|
-
args: { command: 'example', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
398
|
-
assert: (res, text, isError) => (!isError && /no .* examples found|not found/i.test(text)) || 'Expected "no examples found" message for nonexistent example target'
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
category: 'Correctness',
|
|
402
|
-
tool: 'ucn',
|
|
403
|
-
desc: 'related(nonexistent) returns "not found" message',
|
|
404
|
-
args: { command: 'related', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
405
|
-
assert: (res, text, isError) => (!isError && /not found/i.test(text)) || 'Expected "not found" message for nonexistent related target'
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
category: 'Correctness',
|
|
409
|
-
tool: 'ucn',
|
|
410
|
-
desc: 'lines(range="5-0") returns validation error',
|
|
411
|
-
args: { command: 'lines', project_dir: PROJECT_DIR, file: 'core/discovery.js', range: '5-0' },
|
|
412
|
-
assert: (res, text, isError) => isError === true || 'Expected isError: true for invalid range'
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
category: 'Correctness',
|
|
416
|
-
tool: 'ucn',
|
|
417
|
-
desc: 'class(max_lines=-1) returns validation error',
|
|
418
|
-
args: { command: 'class', project_dir: PROJECT_DIR, name: 'ProjectIndex', max_lines: -1 },
|
|
419
|
-
assert: (res, text, isError) => isError === true || 'Expected isError: true for negative max_lines'
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
category: 'Correctness',
|
|
423
|
-
tool: 'ucn',
|
|
424
|
-
desc: 'lines - unique partial file resolves successfully',
|
|
425
|
-
args: { command: 'lines', project_dir: PROJECT_DIR, file: 'core/discovery.js', range: '1-3' },
|
|
426
|
-
assert: (res, text, isError) => isError === false || 'Expected success for unique partial file'
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
category: 'Correctness',
|
|
430
|
-
tool: 'ucn',
|
|
431
|
-
desc: 'file_exports(file=utils.js) returns ambiguity error',
|
|
432
|
-
args: { command: 'file_exports', project_dir: PROJECT_DIR, file: 'utils.js' },
|
|
433
|
-
assert: (res, text, isError) => (isError && /ambiguous/i.test(text)) || 'Expected file-ambiguous error for utils.js'
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
category: 'Correctness',
|
|
437
|
-
tool: 'ucn',
|
|
438
|
-
desc: 'imports(file=utils.js) returns ambiguity error',
|
|
439
|
-
args: { command: 'imports', project_dir: PROJECT_DIR, file: 'utils.js' },
|
|
440
|
-
assert: (res, text, isError) => (isError && /ambiguous/i.test(text)) || 'Expected file-ambiguous error for utils.js'
|
|
441
|
-
},
|
|
442
|
-
|
|
443
|
-
// ========================================================================
|
|
444
|
-
// CATEGORY 3: Security (path traversal, argument injection)
|
|
445
|
-
// ========================================================================
|
|
446
|
-
{
|
|
447
|
-
category: 'Security',
|
|
448
|
-
tool: 'ucn',
|
|
449
|
-
desc: 'lines rejects path traversal (../../../../etc/passwd)',
|
|
450
|
-
args: { command: 'lines', project_dir: PROJECT_DIR, file: '../../../../etc/passwd', range: '1-5' },
|
|
451
|
-
assert: (res, text, isError) => (isError && (/not found/i.test(text) || /outside project/i.test(text))) || 'Expected error for path traversal'
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
category: 'Security',
|
|
455
|
-
tool: 'ucn',
|
|
456
|
-
desc: 'lines rejects path traversal (../../other-project/secret.js)',
|
|
457
|
-
args: { command: 'lines', project_dir: PROJECT_DIR, file: '../../other-project/secret.js', range: '1-5' },
|
|
458
|
-
assert: (res, text, isError) => (isError && (/not found/i.test(text) || /outside project/i.test(text))) || 'Expected error for path traversal'
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
category: 'Security',
|
|
462
|
-
tool: 'ucn',
|
|
463
|
-
desc: 'lines works with valid file',
|
|
464
|
-
args: { command: 'lines', project_dir: PROJECT_DIR, file: 'core/discovery.js', range: '1-3' },
|
|
465
|
-
assert: (res, text, isError) => (!isError && text.length > 0) || 'Expected valid output for core/discovery.js'
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
category: 'Security',
|
|
469
|
-
tool: 'ucn',
|
|
470
|
-
desc: 'diff_impact rejects --config argument injection',
|
|
471
|
-
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: '--config=malicious' },
|
|
472
|
-
assert: (res, text, isError) => (isError && /invalid git ref/i.test(text)) || 'Expected error for argument injection in base'
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
category: 'Security',
|
|
476
|
-
tool: 'ucn',
|
|
477
|
-
desc: 'diff_impact rejects -o flag injection',
|
|
478
|
-
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: '-o /tmp/evil' },
|
|
479
|
-
assert: (res, text, isError) => (isError && /invalid git ref/i.test(text)) || 'Expected error for flag injection in base'
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
category: 'Security',
|
|
483
|
-
tool: 'ucn',
|
|
484
|
-
desc: 'diff_impact accepts valid ref HEAD~3',
|
|
485
|
-
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: 'HEAD~3' },
|
|
486
|
-
assert: (res, text, isError) => true // Should not error on valid ref format
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
category: 'Security',
|
|
490
|
-
tool: 'ucn',
|
|
491
|
-
desc: 'diff_impact accepts valid ref origin/main',
|
|
492
|
-
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: 'origin/main' },
|
|
493
|
-
assert: (res, text, isError) => true // Should not error on valid ref format
|
|
494
|
-
},
|
|
495
|
-
];
|
|
496
|
-
|
|
497
|
-
// ============================================================================
|
|
498
|
-
// Test runner
|
|
499
|
-
// ============================================================================
|
|
500
|
-
|
|
501
|
-
async function run() {
|
|
502
|
-
const client = new McpClient();
|
|
503
|
-
const results = [];
|
|
504
|
-
|
|
505
|
-
console.log('Starting MCP server...');
|
|
506
|
-
await client.start();
|
|
507
|
-
|
|
508
|
-
console.log('Sending initialize...');
|
|
509
|
-
const initRes = await client.initialize();
|
|
510
|
-
if (initRes.error) {
|
|
511
|
-
console.error('Initialize failed:', JSON.stringify(initRes.error));
|
|
512
|
-
client.stop();
|
|
513
|
-
process.exit(1);
|
|
514
|
-
}
|
|
515
|
-
console.log('Server initialized successfully.\n');
|
|
516
|
-
console.log('Running ' + tests.length + ' edge case tests...\n');
|
|
517
|
-
|
|
518
|
-
// Run tests sequentially
|
|
519
|
-
for (let i = 0; i < tests.length; i++) {
|
|
520
|
-
const t = tests[i];
|
|
521
|
-
const label = `[${i + 1}/${tests.length}] ${t.tool} ${t.args.command} - ${t.desc}`;
|
|
522
|
-
process.stdout.write(` ${label} ... `);
|
|
523
|
-
|
|
524
|
-
const startTime = Date.now();
|
|
525
|
-
let status = 'FAIL';
|
|
526
|
-
let detail = '';
|
|
527
|
-
|
|
528
|
-
try {
|
|
529
|
-
const res = await client.callTool(t.tool, t.args);
|
|
530
|
-
const elapsed = Date.now() - startTime;
|
|
531
|
-
|
|
532
|
-
if (res.error) {
|
|
533
|
-
// JSON-RPC level error - server responded, not a crash
|
|
534
|
-
status = 'PASS';
|
|
535
|
-
detail = `RPC error: ${res.error.message || JSON.stringify(res.error)} (${elapsed}ms)`;
|
|
536
|
-
} else if (res.result) {
|
|
537
|
-
const content = res.result.content;
|
|
538
|
-
const isError = res.result.isError === true;
|
|
539
|
-
const text = content && content[0] && content[0].text || '';
|
|
540
|
-
const preview = text.substring(0, 120).replace(/\n/g, '\\n');
|
|
541
|
-
status = 'PASS';
|
|
542
|
-
detail = `${isError ? 'ERROR response' : 'OK'}: "${preview}" (${elapsed}ms)`;
|
|
543
|
-
|
|
544
|
-
// Run assertion if provided
|
|
545
|
-
if (t.assert && status === 'PASS') {
|
|
546
|
-
const assertResult = t.assert(res, text, isError);
|
|
547
|
-
if (assertResult !== true) {
|
|
548
|
-
status = 'FAIL';
|
|
549
|
-
detail = `ASSERTION: ${assertResult} (${elapsed}ms)`;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
} else {
|
|
553
|
-
status = 'PASS';
|
|
554
|
-
detail = `Empty result (${elapsed}ms)`;
|
|
555
|
-
}
|
|
556
|
-
} catch (e) {
|
|
557
|
-
const elapsed = Date.now() - startTime;
|
|
558
|
-
if (e.message === 'TIMEOUT') {
|
|
559
|
-
status = 'FAIL';
|
|
560
|
-
detail = `TIMEOUT after ${TIMEOUT_MS}ms`;
|
|
561
|
-
} else {
|
|
562
|
-
status = 'FAIL';
|
|
563
|
-
detail = `CRASH: ${e.message} (${elapsed}ms)`;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
console.log(status);
|
|
568
|
-
results.push({
|
|
569
|
-
num: i + 1,
|
|
570
|
-
category: t.category,
|
|
571
|
-
tool: t.tool,
|
|
572
|
-
command: t.args.command,
|
|
573
|
-
desc: t.desc,
|
|
574
|
-
status,
|
|
575
|
-
detail
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
client.stop();
|
|
580
|
-
|
|
581
|
-
// ========================================================================
|
|
582
|
-
// Summary table
|
|
583
|
-
// ========================================================================
|
|
584
|
-
console.log('\n' + '='.repeat(140));
|
|
585
|
-
console.log('SUMMARY');
|
|
586
|
-
console.log('='.repeat(140));
|
|
587
|
-
|
|
588
|
-
const categories = [...new Set(results.map(r => r.category))];
|
|
589
|
-
|
|
590
|
-
for (const cat of categories) {
|
|
591
|
-
console.log(`\n--- ${cat} ---`);
|
|
592
|
-
console.log(
|
|
593
|
-
'#'.padEnd(5) +
|
|
594
|
-
'Command'.padEnd(22) +
|
|
595
|
-
'Description'.padEnd(42) +
|
|
596
|
-
'Status'.padEnd(8) +
|
|
597
|
-
'Detail'
|
|
598
|
-
);
|
|
599
|
-
console.log('-'.repeat(140));
|
|
600
|
-
|
|
601
|
-
const catResults = results.filter(r => r.category === cat);
|
|
602
|
-
for (const r of catResults) {
|
|
603
|
-
console.log(
|
|
604
|
-
String(r.num).padEnd(5) +
|
|
605
|
-
r.command.padEnd(22) +
|
|
606
|
-
r.desc.padEnd(42) +
|
|
607
|
-
r.status.padEnd(8) +
|
|
608
|
-
r.detail.substring(0, 100)
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const passed = results.filter(r => r.status === 'PASS').length;
|
|
614
|
-
const failed = results.filter(r => r.status === 'FAIL').length;
|
|
615
|
-
const total = results.length;
|
|
616
|
-
|
|
617
|
-
console.log('\n' + '='.repeat(140));
|
|
618
|
-
console.log(`TOTAL: ${total} tests | PASS: ${passed} | FAIL: ${failed}`);
|
|
619
|
-
console.log('='.repeat(140));
|
|
620
|
-
|
|
621
|
-
if (failed > 0) {
|
|
622
|
-
console.log('\nFailed tests:');
|
|
623
|
-
for (const r of results.filter(r => r.status === 'FAIL')) {
|
|
624
|
-
console.log(` ${r.num}. ${r.command} - ${r.desc}: ${r.detail}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
process.exit(failed > 0 ? 1 : 0);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
run().catch(e => {
|
|
632
|
-
console.error('Test runner crashed:', e);
|
|
633
|
-
process.exit(2);
|
|
634
|
-
});
|