ucn 3.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.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/.claude/skills/ucn/SKILL.md +77 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/cli/index.js +2437 -0
- package/core/discovery.js +513 -0
- package/core/imports.js +558 -0
- package/core/output.js +1274 -0
- package/core/parser.js +279 -0
- package/core/project.js +3261 -0
- package/index.js +52 -0
- package/languages/go.js +653 -0
- package/languages/index.js +267 -0
- package/languages/java.js +826 -0
- package/languages/javascript.js +1346 -0
- package/languages/python.js +667 -0
- package/languages/rust.js +950 -0
- package/languages/utils.js +457 -0
- package/package.json +42 -0
- package/test/fixtures/go/go.mod +3 -0
- package/test/fixtures/go/main.go +257 -0
- package/test/fixtures/go/service.go +187 -0
- package/test/fixtures/java/DataService.java +279 -0
- package/test/fixtures/java/Main.java +287 -0
- package/test/fixtures/java/Utils.java +199 -0
- package/test/fixtures/java/pom.xml +6 -0
- package/test/fixtures/javascript/main.js +109 -0
- package/test/fixtures/javascript/package.json +1 -0
- package/test/fixtures/javascript/service.js +88 -0
- package/test/fixtures/javascript/utils.js +67 -0
- package/test/fixtures/python/main.py +198 -0
- package/test/fixtures/python/pyproject.toml +3 -0
- package/test/fixtures/python/service.py +166 -0
- package/test/fixtures/python/utils.py +118 -0
- package/test/fixtures/rust/Cargo.toml +3 -0
- package/test/fixtures/rust/main.rs +253 -0
- package/test/fixtures/rust/service.rs +210 -0
- package/test/fixtures/rust/utils.rs +154 -0
- package/test/fixtures/typescript/main.ts +154 -0
- package/test/fixtures/typescript/package.json +1 -0
- package/test/fixtures/typescript/repository.ts +149 -0
- package/test/fixtures/typescript/types.ts +114 -0
- package/test/parser.test.js +3661 -0
- package/test/public-repos-test.js +477 -0
- package/test/systematic-test.js +619 -0
- package/ucn.js +8 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Systematic Test Script for UCN
|
|
5
|
+
*
|
|
6
|
+
* Tests all UCN commands across all supported languages to identify bugs.
|
|
7
|
+
* Run with: node test/systematic-test.js [--verbose] [--language=<lang>] [--command=<cmd>]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync, spawn } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
// Configuration
|
|
15
|
+
const UCN_PATH = path.join(__dirname, '..', 'ucn.js');
|
|
16
|
+
const FIXTURES_PATH = path.join(__dirname, 'fixtures');
|
|
17
|
+
const TIMEOUT = 30000; // 30 seconds
|
|
18
|
+
|
|
19
|
+
// Parse command line arguments
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
22
|
+
const filterLanguage = args.find(a => a.startsWith('--language='))?.split('=')[1];
|
|
23
|
+
const filterCommand = args.find(a => a.startsWith('--command='))?.split('=')[1];
|
|
24
|
+
|
|
25
|
+
// Colors for output
|
|
26
|
+
const colors = {
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
green: '\x1b[32m',
|
|
30
|
+
yellow: '\x1b[33m',
|
|
31
|
+
blue: '\x1b[34m',
|
|
32
|
+
cyan: '\x1b[36m',
|
|
33
|
+
dim: '\x1b[2m',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Test results
|
|
37
|
+
const results = {
|
|
38
|
+
passed: 0,
|
|
39
|
+
failed: 0,
|
|
40
|
+
skipped: 0,
|
|
41
|
+
bugs: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Language configurations
|
|
45
|
+
const languages = {
|
|
46
|
+
javascript: {
|
|
47
|
+
path: path.join(FIXTURES_PATH, 'javascript'),
|
|
48
|
+
extension: '.js',
|
|
49
|
+
symbols: {
|
|
50
|
+
functions: ['processData', 'validateInput', 'transformOutput', 'fetchAndProcess', 'helper', 'formatData'],
|
|
51
|
+
classes: ['DataProcessor', 'Service'],
|
|
52
|
+
methods: ['fetch', 'process', 'buildUrl'],
|
|
53
|
+
},
|
|
54
|
+
files: ['main.js', 'utils.js', 'service.js'],
|
|
55
|
+
},
|
|
56
|
+
typescript: {
|
|
57
|
+
path: path.join(FIXTURES_PATH, 'typescript'),
|
|
58
|
+
extension: '.ts',
|
|
59
|
+
symbols: {
|
|
60
|
+
functions: ['filterTasks', 'createTask', 'generateId', 'withRetry', 'processTask', 'createConfig'],
|
|
61
|
+
classes: ['TaskManager', 'Repository', 'DataService', 'Logger'],
|
|
62
|
+
interfaces: ['Task', 'Config', 'IRepository'],
|
|
63
|
+
enums: ['Status', 'LogLevel'],
|
|
64
|
+
},
|
|
65
|
+
files: ['main.ts', 'repository.ts', 'types.ts'],
|
|
66
|
+
},
|
|
67
|
+
python: {
|
|
68
|
+
path: path.join(FIXTURES_PATH, 'python'),
|
|
69
|
+
extension: '.py',
|
|
70
|
+
symbols: {
|
|
71
|
+
functions: ['create_task', 'filter_by_status', 'filter_by_priority', 'format_data', 'validate_input', 'deep_merge'],
|
|
72
|
+
classes: ['TaskManager', 'Task', 'DataService', 'CacheService', 'ApiClient'],
|
|
73
|
+
decorators: ['with_logging', 'with_retry'],
|
|
74
|
+
},
|
|
75
|
+
files: ['main.py', 'utils.py', 'service.py'],
|
|
76
|
+
},
|
|
77
|
+
go: {
|
|
78
|
+
path: path.join(FIXTURES_PATH, 'go'),
|
|
79
|
+
extension: '.go',
|
|
80
|
+
symbols: {
|
|
81
|
+
functions: ['NewTaskManager', 'ValidateTask', 'CreateTask', 'FilterByStatus', 'FormatTask', 'NewDataService'],
|
|
82
|
+
structs: ['Task', 'TaskManager', 'TaskProcessor', 'DataService', 'CacheService'],
|
|
83
|
+
methods: ['AddTask', 'GetTask', 'GetTasks', 'Save', 'Find'],
|
|
84
|
+
},
|
|
85
|
+
files: ['main.go', 'service.go'],
|
|
86
|
+
},
|
|
87
|
+
rust: {
|
|
88
|
+
path: path.join(FIXTURES_PATH, 'rust'),
|
|
89
|
+
extension: '.rs',
|
|
90
|
+
symbols: {
|
|
91
|
+
functions: ['validate_task', 'create_task', 'filter_by_status', 'format_task', 'format_data', 'snake_to_camel'],
|
|
92
|
+
structs: ['Task', 'TaskManager', 'TaskProcessor', 'DataService', 'CacheService', 'Config'],
|
|
93
|
+
traits: ['Entity', 'Repository'],
|
|
94
|
+
enums: ['Status'],
|
|
95
|
+
},
|
|
96
|
+
files: ['main.rs', 'service.rs', 'utils.rs'],
|
|
97
|
+
},
|
|
98
|
+
java: {
|
|
99
|
+
path: path.join(FIXTURES_PATH, 'java'),
|
|
100
|
+
extension: '.java',
|
|
101
|
+
symbols: {
|
|
102
|
+
functions: ['createTask', 'validateTask', 'filterByStatus', 'filterByPriority', 'formatTask', 'formatData'],
|
|
103
|
+
classes: ['Main', 'Task', 'TaskManager', 'TaskProcessor', 'DataService', 'CacheService', 'Utils'],
|
|
104
|
+
interfaces: ['Repository'],
|
|
105
|
+
enums: ['Status'],
|
|
106
|
+
},
|
|
107
|
+
files: ['Main.java', 'DataService.java', 'Utils.java'],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Commands to test with their expected behaviors
|
|
112
|
+
const commands = [
|
|
113
|
+
// Basic commands
|
|
114
|
+
{ name: 'toc', args: [], description: 'Table of contents', expectOutput: true },
|
|
115
|
+
{ name: 'stats', args: [], description: 'Project statistics', expectOutput: true },
|
|
116
|
+
|
|
117
|
+
// Find commands
|
|
118
|
+
{ name: 'find', args: ['$FUNCTION'], description: 'Find symbol definition', expectOutput: true },
|
|
119
|
+
{ name: 'usages', args: ['$FUNCTION'], description: 'Find symbol usages', expectOutput: true },
|
|
120
|
+
{ name: 'search', args: ['TODO'], description: 'Text search', expectOutput: false }, // May not have TODO
|
|
121
|
+
|
|
122
|
+
// Extraction commands
|
|
123
|
+
{ name: 'fn', args: ['$FUNCTION'], description: 'Extract function', expectOutput: true },
|
|
124
|
+
{ name: 'class', args: ['$CLASS'], description: 'Extract class', expectOutput: true },
|
|
125
|
+
{ name: 'lines', args: ['1-10'], description: 'Extract lines', expectOutput: true, fileMode: true },
|
|
126
|
+
|
|
127
|
+
// Analysis commands
|
|
128
|
+
{ name: 'context', args: ['$FUNCTION'], description: 'Show callers and callees', expectOutput: true },
|
|
129
|
+
{ name: 'about', args: ['$FUNCTION'], description: 'Full symbol information', expectOutput: true },
|
|
130
|
+
{ name: 'smart', args: ['$FUNCTION'], description: 'Function with dependencies', expectOutput: true },
|
|
131
|
+
{ name: 'impact', args: ['$FUNCTION'], description: 'Impact analysis', expectOutput: true },
|
|
132
|
+
{ name: 'trace', args: ['$FUNCTION', '--depth=2'], description: 'Call tree', expectOutput: true },
|
|
133
|
+
{ name: 'related', args: ['$FUNCTION'], description: 'Related functions', expectOutput: false }, // May not find related
|
|
134
|
+
|
|
135
|
+
// Additional analysis commands
|
|
136
|
+
{ name: 'typedef', args: ['$CLASS'], description: 'Find type definition', expectOutput: false }, // May not have types
|
|
137
|
+
{ name: 'example', args: ['$FUNCTION'], description: 'Best usage example', expectOutput: false }, // May not find example
|
|
138
|
+
{ name: 'verify', args: ['$FUNCTION'], description: 'Verify call sites', expectOutput: false }, // May not need verify
|
|
139
|
+
|
|
140
|
+
// Dependency commands
|
|
141
|
+
{ name: 'imports', args: ['$FILE'], description: 'File imports', expectOutput: false }, // May not have imports
|
|
142
|
+
{ name: 'exporters', args: ['$FILE'], description: 'Who imports file', expectOutput: false },
|
|
143
|
+
{ name: 'who-imports', args: ['$FILE'], description: 'Who imports (alias)', expectOutput: false },
|
|
144
|
+
{ name: 'graph', args: ['$FILE', '--depth=2'], description: 'Dependency graph', expectOutput: false },
|
|
145
|
+
|
|
146
|
+
// Refactoring commands
|
|
147
|
+
{ name: 'deadcode', args: [], description: 'Find unused functions', expectOutput: false },
|
|
148
|
+
{ name: 'api', args: [], description: 'Public API', expectOutput: true },
|
|
149
|
+
{ name: 'tests', args: ['$FUNCTION'], description: 'Find related tests', expectOutput: false },
|
|
150
|
+
|
|
151
|
+
// JSON output
|
|
152
|
+
{ name: 'find', args: ['$FUNCTION', '--json'], description: 'Find with JSON output', expectOutput: true, checkJson: true },
|
|
153
|
+
{ name: 'toc', args: ['--json'], description: 'TOC with JSON output', expectOutput: true, checkJson: true },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run a UCN command and capture output
|
|
158
|
+
*/
|
|
159
|
+
function runUcn(targetPath, command, args = [], options = {}) {
|
|
160
|
+
const fullArgs = ['node', UCN_PATH, targetPath, command, ...args];
|
|
161
|
+
const cmdStr = fullArgs.join(' ');
|
|
162
|
+
|
|
163
|
+
if (verbose) {
|
|
164
|
+
console.log(`${colors.dim} Running: ${cmdStr}${colors.reset}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const output = execSync(cmdStr, {
|
|
169
|
+
timeout: TIMEOUT,
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
172
|
+
cwd: __dirname,
|
|
173
|
+
});
|
|
174
|
+
return { success: true, output: output.trim(), command: cmdStr };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const stderr = error.stderr?.toString() || '';
|
|
177
|
+
const stdout = error.stdout?.toString() || '';
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
output: stdout,
|
|
181
|
+
error: stderr || error.message,
|
|
182
|
+
command: cmdStr,
|
|
183
|
+
exitCode: error.status,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Replace placeholders in command arguments
|
|
190
|
+
*/
|
|
191
|
+
function replaceArgs(args, symbols, files, lang) {
|
|
192
|
+
return args.map(arg => {
|
|
193
|
+
if (arg === '$FUNCTION') {
|
|
194
|
+
return symbols.functions?.[0] || symbols.methods?.[0] || 'main';
|
|
195
|
+
}
|
|
196
|
+
if (arg === '$CLASS') {
|
|
197
|
+
return symbols.classes?.[0] || symbols.structs?.[0] || 'Main';
|
|
198
|
+
}
|
|
199
|
+
if (arg === '$FILE') {
|
|
200
|
+
return files[0];
|
|
201
|
+
}
|
|
202
|
+
return arg;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Test a single command
|
|
208
|
+
*/
|
|
209
|
+
function testCommand(lang, langConfig, cmd) {
|
|
210
|
+
const testName = `${lang}/${cmd.name}`;
|
|
211
|
+
|
|
212
|
+
if (filterCommand && cmd.name !== filterCommand) {
|
|
213
|
+
return { skipped: true, name: testName };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const args = replaceArgs(cmd.args, langConfig.symbols, langConfig.files, lang);
|
|
217
|
+
const targetPath = cmd.fileMode
|
|
218
|
+
? path.join(langConfig.path, langConfig.files[0])
|
|
219
|
+
: langConfig.path;
|
|
220
|
+
|
|
221
|
+
const result = runUcn(targetPath, cmd.name, args);
|
|
222
|
+
|
|
223
|
+
// Check for success
|
|
224
|
+
if (!result.success) {
|
|
225
|
+
// Check if it's an expected failure (like no results found)
|
|
226
|
+
const isExpectedFailure =
|
|
227
|
+
result.error?.includes('No matches found') ||
|
|
228
|
+
result.error?.includes('not found') ||
|
|
229
|
+
result.error?.includes('No usages found') ||
|
|
230
|
+
result.error?.includes('No tests found') ||
|
|
231
|
+
result.error?.includes('No related') ||
|
|
232
|
+
result.error?.includes('No imports') ||
|
|
233
|
+
result.error?.includes('No deadcode');
|
|
234
|
+
|
|
235
|
+
if (isExpectedFailure && !cmd.expectOutput) {
|
|
236
|
+
return { passed: true, name: testName, note: 'Expected no results' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
passed: false,
|
|
241
|
+
name: testName,
|
|
242
|
+
error: result.error,
|
|
243
|
+
command: result.command,
|
|
244
|
+
exitCode: result.exitCode,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check for output if expected
|
|
249
|
+
if (cmd.expectOutput && !result.output) {
|
|
250
|
+
return {
|
|
251
|
+
passed: false,
|
|
252
|
+
name: testName,
|
|
253
|
+
error: 'Expected output but got none',
|
|
254
|
+
command: result.command,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check JSON validity if required
|
|
259
|
+
if (cmd.checkJson) {
|
|
260
|
+
try {
|
|
261
|
+
JSON.parse(result.output);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return {
|
|
264
|
+
passed: false,
|
|
265
|
+
name: testName,
|
|
266
|
+
error: `Invalid JSON output: ${e.message}`,
|
|
267
|
+
command: result.command,
|
|
268
|
+
output: result.output.substring(0, 200),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for error messages in output (potential bugs)
|
|
274
|
+
const errorPatterns = [
|
|
275
|
+
/undefined/i,
|
|
276
|
+
/null is not/i,
|
|
277
|
+
/cannot read property/i,
|
|
278
|
+
/is not a function/i,
|
|
279
|
+
/TypeError/,
|
|
280
|
+
/ReferenceError/,
|
|
281
|
+
/SyntaxError/,
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
for (const pattern of errorPatterns) {
|
|
285
|
+
if (pattern.test(result.output)) {
|
|
286
|
+
return {
|
|
287
|
+
passed: false,
|
|
288
|
+
name: testName,
|
|
289
|
+
error: `Output contains error pattern: ${pattern}`,
|
|
290
|
+
command: result.command,
|
|
291
|
+
output: result.output.substring(0, 500),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { passed: true, name: testName };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Test a language
|
|
301
|
+
*/
|
|
302
|
+
function testLanguage(lang, langConfig) {
|
|
303
|
+
console.log(`\n${colors.blue}Testing ${lang.toUpperCase()}${colors.reset}`);
|
|
304
|
+
console.log(`${colors.dim} Path: ${langConfig.path}${colors.reset}`);
|
|
305
|
+
|
|
306
|
+
// Check if fixtures exist
|
|
307
|
+
if (!fs.existsSync(langConfig.path)) {
|
|
308
|
+
console.log(`${colors.yellow} ⚠ Fixtures not found, skipping${colors.reset}`);
|
|
309
|
+
results.skipped += commands.length;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Test each command
|
|
314
|
+
for (const cmd of commands) {
|
|
315
|
+
const result = testCommand(lang, langConfig, cmd);
|
|
316
|
+
|
|
317
|
+
if (result.skipped) {
|
|
318
|
+
results.skipped++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (result.passed) {
|
|
323
|
+
results.passed++;
|
|
324
|
+
if (verbose) {
|
|
325
|
+
const note = result.note ? ` (${result.note})` : '';
|
|
326
|
+
console.log(` ${colors.green}✓${colors.reset} ${cmd.name}: ${cmd.description}${note}`);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
results.failed++;
|
|
330
|
+
results.bugs.push({
|
|
331
|
+
language: lang,
|
|
332
|
+
command: cmd.name,
|
|
333
|
+
description: cmd.description,
|
|
334
|
+
error: result.error,
|
|
335
|
+
fullCommand: result.command,
|
|
336
|
+
output: result.output,
|
|
337
|
+
exitCode: result.exitCode,
|
|
338
|
+
});
|
|
339
|
+
console.log(` ${colors.red}✗${colors.reset} ${cmd.name}: ${cmd.description}`);
|
|
340
|
+
if (verbose) {
|
|
341
|
+
console.log(` ${colors.red}Error: ${result.error}${colors.reset}`);
|
|
342
|
+
if (result.output) {
|
|
343
|
+
console.log(` ${colors.dim}Output: ${result.output.substring(0, 200)}${colors.reset}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Run additional edge case tests
|
|
352
|
+
*/
|
|
353
|
+
function testEdgeCases() {
|
|
354
|
+
console.log(`\n${colors.blue}Testing EDGE CASES${colors.reset}`);
|
|
355
|
+
|
|
356
|
+
const edgeCases = [
|
|
357
|
+
// Non-existent symbol
|
|
358
|
+
{
|
|
359
|
+
name: 'Non-existent symbol',
|
|
360
|
+
run: () => runUcn(languages.javascript.path, 'find', ['nonExistentSymbol12345']),
|
|
361
|
+
expect: (r) => !r.success || r.output.includes('No symbols found') || r.output.includes('No matches') || r.output === '',
|
|
362
|
+
},
|
|
363
|
+
// Empty arguments
|
|
364
|
+
{
|
|
365
|
+
name: 'Empty find argument',
|
|
366
|
+
run: () => runUcn(languages.javascript.path, 'find', ['']),
|
|
367
|
+
expect: (r) => true, // Should handle gracefully
|
|
368
|
+
},
|
|
369
|
+
// Special characters in search
|
|
370
|
+
{
|
|
371
|
+
name: 'Special chars in search',
|
|
372
|
+
run: () => runUcn(languages.javascript.path, 'search', ['[test]']),
|
|
373
|
+
expect: (r) => r.success || r.error?.includes('regex') || !r.error?.includes('SyntaxError'),
|
|
374
|
+
},
|
|
375
|
+
// Very long symbol name
|
|
376
|
+
{
|
|
377
|
+
name: 'Very long symbol name',
|
|
378
|
+
run: () => runUcn(languages.javascript.path, 'find', ['a'.repeat(1000)]),
|
|
379
|
+
expect: (r) => r.success || !r.error?.includes('Maximum call stack'),
|
|
380
|
+
},
|
|
381
|
+
// Invalid path
|
|
382
|
+
{
|
|
383
|
+
name: 'Invalid path',
|
|
384
|
+
run: () => runUcn('/nonexistent/path', 'toc', []),
|
|
385
|
+
expect: (r) => !r.success && (r.error?.includes('not found') || r.error?.includes('ENOENT') || r.error?.includes('No supported files')),
|
|
386
|
+
},
|
|
387
|
+
// Depth edge cases
|
|
388
|
+
{
|
|
389
|
+
name: 'Depth = 0',
|
|
390
|
+
run: () => runUcn(languages.javascript.path, 'trace', ['processData', '--depth=0']),
|
|
391
|
+
expect: (r) => r.success,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'Negative depth',
|
|
395
|
+
run: () => runUcn(languages.javascript.path, 'trace', ['processData', '--depth=-1']),
|
|
396
|
+
expect: (r) => r.success || !r.error?.includes('crash'),
|
|
397
|
+
},
|
|
398
|
+
// Unicode in search
|
|
399
|
+
{
|
|
400
|
+
name: 'Unicode in search',
|
|
401
|
+
run: () => runUcn(languages.javascript.path, 'search', ['日本語']),
|
|
402
|
+
expect: (r) => r.success || r.output === '',
|
|
403
|
+
},
|
|
404
|
+
// Multiple flags
|
|
405
|
+
{
|
|
406
|
+
name: 'Multiple flags combined',
|
|
407
|
+
run: () => runUcn(languages.javascript.path, 'find', ['processData', '--json', '--exact', '--top=1']),
|
|
408
|
+
expect: (r) => r.success,
|
|
409
|
+
},
|
|
410
|
+
// Flag variations
|
|
411
|
+
{
|
|
412
|
+
name: 'Exclude flag',
|
|
413
|
+
run: () => runUcn(languages.javascript.path, 'find', ['processData', '--exclude=test']),
|
|
414
|
+
expect: (r) => r.success,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'In flag (path filter)',
|
|
418
|
+
run: () => runUcn(languages.javascript.path, 'toc', ['--in=.']),
|
|
419
|
+
expect: (r) => r.success,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'Include tests flag',
|
|
423
|
+
run: () => runUcn(languages.javascript.path, 'usages', ['processData', '--include-tests']),
|
|
424
|
+
expect: (r) => r.success,
|
|
425
|
+
},
|
|
426
|
+
// Context output with expand
|
|
427
|
+
{
|
|
428
|
+
name: 'Context with expand flag',
|
|
429
|
+
run: () => runUcn(languages.javascript.path, 'context', ['processData', '--expand']),
|
|
430
|
+
expect: (r) => r.success,
|
|
431
|
+
},
|
|
432
|
+
// About with code-only flag
|
|
433
|
+
{
|
|
434
|
+
name: 'About with code-only',
|
|
435
|
+
run: () => runUcn(languages.javascript.path, 'about', ['processData', '--code-only']),
|
|
436
|
+
expect: (r) => r.success || r.output.includes('processData'),
|
|
437
|
+
},
|
|
438
|
+
// Smart with types
|
|
439
|
+
{
|
|
440
|
+
name: 'Smart with types',
|
|
441
|
+
run: () => runUcn(languages.typescript.path, 'smart', ['filterTasks', '--with-types']),
|
|
442
|
+
expect: (r) => r.success,
|
|
443
|
+
},
|
|
444
|
+
// File mode commands
|
|
445
|
+
{
|
|
446
|
+
name: 'Single file toc',
|
|
447
|
+
run: () => runUcn(path.join(languages.javascript.path, 'main.js'), 'toc', []),
|
|
448
|
+
expect: (r) => r.success && r.output.includes('processData'),
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: 'Single file find',
|
|
452
|
+
run: () => runUcn(path.join(languages.javascript.path, 'main.js'), 'find', ['processData']),
|
|
453
|
+
expect: (r) => r.success,
|
|
454
|
+
},
|
|
455
|
+
// Glob mode - note: path needs quoting to prevent shell expansion
|
|
456
|
+
{
|
|
457
|
+
name: 'Glob pattern find',
|
|
458
|
+
run: () => {
|
|
459
|
+
const globPath = `"${path.join(languages.javascript.path, '*.js')}"`;
|
|
460
|
+
return runUcn(globPath, 'find', ['helper']);
|
|
461
|
+
},
|
|
462
|
+
expect: (r) => r.success,
|
|
463
|
+
},
|
|
464
|
+
// Lines command edge cases
|
|
465
|
+
{
|
|
466
|
+
name: 'Lines with out-of-bounds range',
|
|
467
|
+
run: () => runUcn(path.join(languages.javascript.path, 'main.js'), 'lines', ['9999-10000']),
|
|
468
|
+
expect: (r) => !r.success && r.error?.includes('out of bounds'),
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'Lines with reversed range',
|
|
472
|
+
run: () => runUcn(path.join(languages.javascript.path, 'main.js'), 'lines', ['10-5']),
|
|
473
|
+
expect: (r) => r.success && r.output.includes('5'),
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'Lines with non-numeric range',
|
|
477
|
+
run: () => runUcn(path.join(languages.javascript.path, 'main.js'), 'lines', ['abc-def']),
|
|
478
|
+
expect: (r) => !r.success && r.error?.includes('Invalid line range'),
|
|
479
|
+
},
|
|
480
|
+
// Graph with negative depth
|
|
481
|
+
{
|
|
482
|
+
name: 'Graph with negative depth',
|
|
483
|
+
run: () => runUcn(languages.javascript.path, 'graph', ['main.js', '--depth=-5']),
|
|
484
|
+
expect: (r) => r.success && r.output.includes('main.js'),
|
|
485
|
+
},
|
|
486
|
+
// Double-dash separator
|
|
487
|
+
{
|
|
488
|
+
name: 'Double-dash flag separator',
|
|
489
|
+
run: () => runUcn(languages.javascript.path, 'find', ['--', '--test']),
|
|
490
|
+
expect: (r) => r.success && !r.error?.includes('Unknown flag'),
|
|
491
|
+
},
|
|
492
|
+
// Plan command
|
|
493
|
+
{
|
|
494
|
+
name: 'Plan with rename',
|
|
495
|
+
run: () => runUcn(languages.javascript.path, 'plan', ['processData', '--rename-to=processInput']),
|
|
496
|
+
expect: (r) => r.success && r.output.includes('Refactoring plan'),
|
|
497
|
+
},
|
|
498
|
+
// Verify command
|
|
499
|
+
{
|
|
500
|
+
name: 'Verify function calls',
|
|
501
|
+
run: () => runUcn(languages.javascript.path, 'verify', ['processData']),
|
|
502
|
+
expect: (r) => r.success,
|
|
503
|
+
},
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
for (const testCase of edgeCases) {
|
|
507
|
+
const result = testCase.run();
|
|
508
|
+
const passed = testCase.expect(result);
|
|
509
|
+
|
|
510
|
+
if (passed) {
|
|
511
|
+
results.passed++;
|
|
512
|
+
if (verbose) {
|
|
513
|
+
console.log(` ${colors.green}✓${colors.reset} ${testCase.name}`);
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
results.failed++;
|
|
517
|
+
const errorMsg = result.error || `Expected test to pass but got: ${JSON.stringify({ success: result.success, output: result.output?.substring(0, 100) })}`;
|
|
518
|
+
results.bugs.push({
|
|
519
|
+
language: 'edge-case',
|
|
520
|
+
command: testCase.name,
|
|
521
|
+
error: errorMsg,
|
|
522
|
+
fullCommand: result.command,
|
|
523
|
+
});
|
|
524
|
+
console.log(` ${colors.red}✗${colors.reset} ${testCase.name}`);
|
|
525
|
+
if (verbose) {
|
|
526
|
+
console.log(` ${colors.red}Error: ${errorMsg}${colors.reset}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Print summary report
|
|
534
|
+
*/
|
|
535
|
+
function printSummary() {
|
|
536
|
+
console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`);
|
|
537
|
+
console.log(`${colors.cyan}TEST SUMMARY${colors.reset}`);
|
|
538
|
+
console.log(`${colors.cyan}${'='.repeat(60)}${colors.reset}`);
|
|
539
|
+
|
|
540
|
+
console.log(`\n ${colors.green}Passed:${colors.reset} ${results.passed}`);
|
|
541
|
+
console.log(` ${colors.red}Failed:${colors.reset} ${results.failed}`);
|
|
542
|
+
console.log(` ${colors.yellow}Skipped:${colors.reset} ${results.skipped}`);
|
|
543
|
+
console.log(` Total: ${results.passed + results.failed + results.skipped}`);
|
|
544
|
+
|
|
545
|
+
if (results.bugs.length > 0) {
|
|
546
|
+
console.log(`\n${colors.red}BUGS FOUND (${results.bugs.length}):${colors.reset}`);
|
|
547
|
+
console.log(`${'-'.repeat(60)}`);
|
|
548
|
+
|
|
549
|
+
// Group bugs by language
|
|
550
|
+
const bugsByLang = {};
|
|
551
|
+
for (const bug of results.bugs) {
|
|
552
|
+
if (!bugsByLang[bug.language]) {
|
|
553
|
+
bugsByLang[bug.language] = [];
|
|
554
|
+
}
|
|
555
|
+
bugsByLang[bug.language].push(bug);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
for (const [lang, bugs] of Object.entries(bugsByLang)) {
|
|
559
|
+
console.log(`\n${colors.yellow}${lang.toUpperCase()}:${colors.reset}`);
|
|
560
|
+
for (const bug of bugs) {
|
|
561
|
+
console.log(` • ${bug.command}: ${bug.description || bug.error}`);
|
|
562
|
+
console.log(` ${colors.dim}Command: ${bug.fullCommand}${colors.reset}`);
|
|
563
|
+
if (bug.error && bug.error !== bug.description) {
|
|
564
|
+
console.log(` ${colors.red}Error: ${bug.error}${colors.reset}`);
|
|
565
|
+
}
|
|
566
|
+
if (bug.exitCode !== undefined) {
|
|
567
|
+
console.log(` ${colors.dim}Exit code: ${bug.exitCode}${colors.reset}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Save bugs to file
|
|
573
|
+
const bugsFile = path.join(__dirname, 'bugs-report.json');
|
|
574
|
+
fs.writeFileSync(bugsFile, JSON.stringify(results.bugs, null, 2));
|
|
575
|
+
console.log(`\n${colors.dim}Bug report saved to: ${bugsFile}${colors.reset}`);
|
|
576
|
+
} else {
|
|
577
|
+
console.log(`\n${colors.green}No bugs found! All tests passed.${colors.reset}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Main function
|
|
585
|
+
*/
|
|
586
|
+
function main() {
|
|
587
|
+
console.log(`${colors.cyan}UCN Systematic Test Suite${colors.reset}`);
|
|
588
|
+
console.log(`${colors.dim}Testing all commands across all supported languages${colors.reset}`);
|
|
589
|
+
console.log(`${colors.dim}UCN Path: ${UCN_PATH}${colors.reset}`);
|
|
590
|
+
console.log(`${colors.dim}Fixtures Path: ${FIXTURES_PATH}${colors.reset}`);
|
|
591
|
+
|
|
592
|
+
if (filterLanguage) {
|
|
593
|
+
console.log(`${colors.yellow}Filtering by language: ${filterLanguage}${colors.reset}`);
|
|
594
|
+
}
|
|
595
|
+
if (filterCommand) {
|
|
596
|
+
console.log(`${colors.yellow}Filtering by command: ${filterCommand}${colors.reset}`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Test each language
|
|
600
|
+
for (const [lang, config] of Object.entries(languages)) {
|
|
601
|
+
if (filterLanguage && lang !== filterLanguage) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
testLanguage(lang, config);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Test edge cases
|
|
608
|
+
if (!filterLanguage && !filterCommand) {
|
|
609
|
+
testEdgeCases();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Print summary
|
|
613
|
+
printSummary();
|
|
614
|
+
|
|
615
|
+
// Exit with error code if there were failures
|
|
616
|
+
process.exit(results.failed > 0 ? 1 : 0);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
main();
|