ucn 3.7.4 → 3.7.5
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/cli/index.js +102 -1
- package/core/project.js +12 -4
- package/mcp/server.js +47 -7
- package/package.json +1 -1
- package/test/mcp-edge-cases.js +53 -0
- package/test/parser.test.js +54 -0
package/cli/index.js
CHANGED
|
@@ -1819,14 +1819,21 @@ Commands:
|
|
|
1819
1819
|
about <name> Everything about a symbol
|
|
1820
1820
|
usages <name> All usages grouped by type
|
|
1821
1821
|
context <name> Callers + callees
|
|
1822
|
+
expand <N> Show code for item N from context
|
|
1822
1823
|
smart <name> Function + dependencies
|
|
1823
1824
|
impact <name> What breaks if changed
|
|
1824
1825
|
trace <name> Call tree
|
|
1826
|
+
example <name> Best usage example
|
|
1827
|
+
related <name> Sibling functions
|
|
1825
1828
|
imports <file> What file imports
|
|
1826
1829
|
exporters <file> Who imports file
|
|
1827
1830
|
tests <name> Find tests
|
|
1828
1831
|
search <term> Text search
|
|
1829
1832
|
typedef <name> Find type definitions
|
|
1833
|
+
deadcode Find unused functions/classes
|
|
1834
|
+
verify <name> Check call sites match signature
|
|
1835
|
+
plan <name> Preview refactoring
|
|
1836
|
+
stacktrace <text> Parse a stack trace
|
|
1830
1837
|
api Show public symbols
|
|
1831
1838
|
diff-impact What changed and who's affected
|
|
1832
1839
|
stats Index statistics
|
|
@@ -1913,7 +1920,7 @@ function executeInteractiveCommand(index, command, arg) {
|
|
|
1913
1920
|
console.log('Usage: context <name>');
|
|
1914
1921
|
return;
|
|
1915
1922
|
}
|
|
1916
|
-
const ctx = index.context(arg, { includeUncertain: flags.includeUncertain });
|
|
1923
|
+
const ctx = index.context(arg, { includeUncertain: flags.includeUncertain, includeMethods: flags.includeMethods });
|
|
1917
1924
|
if (!ctx) {
|
|
1918
1925
|
console.log(`Symbol "${arg}" not found.`);
|
|
1919
1926
|
} else {
|
|
@@ -2036,6 +2043,100 @@ function executeInteractiveCommand(index, command, arg) {
|
|
|
2036
2043
|
break;
|
|
2037
2044
|
}
|
|
2038
2045
|
|
|
2046
|
+
case 'expand': {
|
|
2047
|
+
if (!arg) {
|
|
2048
|
+
console.log('Usage: expand <number>');
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const expandNum = parseInt(arg, 10);
|
|
2052
|
+
if (isNaN(expandNum)) {
|
|
2053
|
+
console.log(`Invalid item number: "${arg}"`);
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const cached = loadExpandableItems(index.root);
|
|
2057
|
+
if (!cached || !cached.items || cached.items.length === 0) {
|
|
2058
|
+
console.log('No expandable items. Run context first.');
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
const expandMatch = cached.items.find(i => i.num === expandNum);
|
|
2062
|
+
if (!expandMatch) {
|
|
2063
|
+
console.log(`Item ${expandNum} not found. Available: 1-${cached.items.length}`);
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
printExpandedItem(expandMatch, cached.root || index.root);
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
case 'deadcode': {
|
|
2071
|
+
const deadResult = index.deadcode({
|
|
2072
|
+
includeExported: flags.includeExported,
|
|
2073
|
+
includeDecorated: flags.includeDecorated,
|
|
2074
|
+
includeTests: flags.includeTests
|
|
2075
|
+
});
|
|
2076
|
+
console.log(output.formatDeadcode(deadResult));
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
case 'related': {
|
|
2081
|
+
if (!arg) {
|
|
2082
|
+
console.log('Usage: related <name>');
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const relResult = index.related(arg, { file: flags.file });
|
|
2086
|
+
console.log(output.formatRelated(relResult));
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
case 'example': {
|
|
2091
|
+
if (!arg) {
|
|
2092
|
+
console.log('Usage: example <name>');
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
console.log(output.formatExample(index.example(arg), arg));
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
case 'plan': {
|
|
2100
|
+
if (!arg) {
|
|
2101
|
+
console.log('Usage: plan <name> [--add-param=x] [--remove-param=x] [--rename-to=x]');
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (!flags.addParam && !flags.removeParam && !flags.renameTo) {
|
|
2105
|
+
console.log('Plan requires an operation: --add-param, --remove-param, or --rename-to');
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
const planResult = index.plan(arg, {
|
|
2109
|
+
addParam: flags.addParam,
|
|
2110
|
+
removeParam: flags.removeParam,
|
|
2111
|
+
renameTo: flags.renameTo,
|
|
2112
|
+
defaultValue: flags.defaultValue,
|
|
2113
|
+
file: flags.file
|
|
2114
|
+
});
|
|
2115
|
+
console.log(output.formatPlan(planResult));
|
|
2116
|
+
break;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
case 'verify': {
|
|
2120
|
+
if (!arg) {
|
|
2121
|
+
console.log('Usage: verify <name>');
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
const verifyResult = index.verify(arg, { file: flags.file });
|
|
2125
|
+
console.log(output.formatVerify(verifyResult));
|
|
2126
|
+
break;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
case 'stacktrace':
|
|
2130
|
+
case 'stack': {
|
|
2131
|
+
if (!arg) {
|
|
2132
|
+
console.log('Usage: stacktrace <stack text>');
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const stackResult = index.parseStackTrace(arg);
|
|
2136
|
+
console.log(output.formatStackTrace(stackResult));
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2039
2140
|
default:
|
|
2040
2141
|
console.log(`Unknown command: ${command}. Type "help" for available commands.`);
|
|
2041
2142
|
}
|
package/core/project.js
CHANGED
|
@@ -1412,7 +1412,8 @@ class ProjectIndex {
|
|
|
1412
1412
|
});
|
|
1413
1413
|
}
|
|
1414
1414
|
} catch (e) {
|
|
1415
|
-
//
|
|
1415
|
+
// Expected: minified files exceed tree-sitter buffer, binary files fail to parse.
|
|
1416
|
+
// These are not actionable errors — silently skip.
|
|
1416
1417
|
}
|
|
1417
1418
|
}
|
|
1418
1419
|
|
|
@@ -1848,6 +1849,8 @@ class ProjectIndex {
|
|
|
1848
1849
|
|
|
1849
1850
|
return result;
|
|
1850
1851
|
} catch (e) {
|
|
1852
|
+
// Expected: file read/parse failures (minified, binary, buffer exceeded).
|
|
1853
|
+
// Return empty callees rather than crashing the entire query.
|
|
1851
1854
|
return [];
|
|
1852
1855
|
}
|
|
1853
1856
|
}
|
|
@@ -1956,6 +1959,8 @@ class ProjectIndex {
|
|
|
1956
1959
|
cleanHtmlScriptTags(extracted, detectLanguage(symbol.file));
|
|
1957
1960
|
return extracted.join('\n');
|
|
1958
1961
|
} catch (e) {
|
|
1962
|
+
// Expected: file may have been deleted or become unreadable since indexing.
|
|
1963
|
+
// Return empty string rather than crashing.
|
|
1959
1964
|
return '';
|
|
1960
1965
|
}
|
|
1961
1966
|
}
|
|
@@ -2354,6 +2359,9 @@ class ProjectIndex {
|
|
|
2354
2359
|
// Note: no 'g' flag - we only need to test for presence per line
|
|
2355
2360
|
// The 'i' flag is kept for case-insensitive matching
|
|
2356
2361
|
const regex = new RegExp('\\b' + escapeRegExp(searchTerm) + '\\b', 'i');
|
|
2362
|
+
// Pre-compile patterns used inside per-line loop
|
|
2363
|
+
const callPattern = new RegExp(escapeRegExp(searchTerm) + '\\s*\\(');
|
|
2364
|
+
const strPattern = new RegExp("['\"`]" + escapeRegExp(searchTerm) + "['\"`]");
|
|
2357
2365
|
|
|
2358
2366
|
for (const { path: testPath, entry } of testFiles) {
|
|
2359
2367
|
try {
|
|
@@ -2368,12 +2376,11 @@ class ProjectIndex {
|
|
|
2368
2376
|
matchType = 'test-case';
|
|
2369
2377
|
} else if (/\b(import|require|from)\b/.test(line)) {
|
|
2370
2378
|
matchType = 'import';
|
|
2371
|
-
} else if (
|
|
2379
|
+
} else if (callPattern.test(line)) {
|
|
2372
2380
|
matchType = 'call';
|
|
2373
2381
|
}
|
|
2374
2382
|
// Detect if the match is inside a string literal (e.g., 'parseFile' or "parseFile")
|
|
2375
2383
|
if (matchType === 'reference' || matchType === 'call') {
|
|
2376
|
-
const strPattern = new RegExp("['\"`]" + escapeRegExp(searchTerm) + "['\"`]");
|
|
2377
2384
|
if (strPattern.test(line)) {
|
|
2378
2385
|
matchType = 'string-ref';
|
|
2379
2386
|
}
|
|
@@ -4409,7 +4416,8 @@ class ProjectIndex {
|
|
|
4409
4416
|
});
|
|
4410
4417
|
}
|
|
4411
4418
|
} catch (e) {
|
|
4412
|
-
//
|
|
4419
|
+
// Expected: binary/minified files fail to read or parse.
|
|
4420
|
+
// These are not actionable errors — silently skip.
|
|
4413
4421
|
}
|
|
4414
4422
|
}
|
|
4415
4423
|
|
package/mcp/server.js
CHANGED
|
@@ -158,6 +158,31 @@ function toolError(message) {
|
|
|
158
158
|
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a file path via index and validate it's within the project root.
|
|
163
|
+
* Returns the resolved absolute path string, or a toolError response.
|
|
164
|
+
*/
|
|
165
|
+
function resolveAndValidatePath(index, file) {
|
|
166
|
+
const resolved = index.resolveFilePathForQuery(file);
|
|
167
|
+
if (typeof resolved !== 'string') {
|
|
168
|
+
if (resolved.error === 'file-ambiguous') {
|
|
169
|
+
return toolError(`Ambiguous file "${file}". Candidates:\n${resolved.candidates.map(c => ' ' + c).join('\n')}`);
|
|
170
|
+
}
|
|
171
|
+
return toolError(`File not found: ${file}`);
|
|
172
|
+
}
|
|
173
|
+
// Path boundary check: ensure resolved path is within the project root
|
|
174
|
+
try {
|
|
175
|
+
const realPath = fs.realpathSync(resolved);
|
|
176
|
+
const realRoot = fs.realpathSync(index.root);
|
|
177
|
+
if (realPath !== realRoot && !realPath.startsWith(realRoot + path.sep)) {
|
|
178
|
+
return toolError(`File is outside project root: ${file}`);
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
return toolError(`Cannot resolve file path: ${file}`);
|
|
182
|
+
}
|
|
183
|
+
return resolved;
|
|
184
|
+
}
|
|
185
|
+
|
|
161
186
|
function requireName(name) {
|
|
162
187
|
if (!name || !name.trim()) {
|
|
163
188
|
return toolError('Symbol name is required.');
|
|
@@ -465,6 +490,9 @@ server.registerTool(
|
|
|
465
490
|
}
|
|
466
491
|
|
|
467
492
|
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
493
|
+
// Validate file is within project root
|
|
494
|
+
const fnPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
|
|
495
|
+
if (typeof fnPathCheck !== 'string') return fnPathCheck;
|
|
468
496
|
const code = fs.readFileSync(match.file, 'utf-8');
|
|
469
497
|
const codeLines = code.split('\n');
|
|
470
498
|
const fnCode = codeLines.slice(match.startLine - 1, match.endLine).join('\n');
|
|
@@ -490,6 +518,9 @@ server.registerTool(
|
|
|
490
518
|
}
|
|
491
519
|
|
|
492
520
|
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
521
|
+
// Validate file is within project root
|
|
522
|
+
const clsPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
|
|
523
|
+
if (typeof clsPathCheck !== 'string') return clsPathCheck;
|
|
493
524
|
|
|
494
525
|
const code = fs.readFileSync(match.file, 'utf-8');
|
|
495
526
|
const codeLines = code.split('\n');
|
|
@@ -542,13 +573,8 @@ server.registerTool(
|
|
|
542
573
|
return toolError('Line range is required (e.g. "10-20" or "15").');
|
|
543
574
|
}
|
|
544
575
|
const index = getIndex(project_dir);
|
|
545
|
-
const resolved = index
|
|
546
|
-
if (typeof resolved !== 'string')
|
|
547
|
-
if (resolved.error === 'file-ambiguous') {
|
|
548
|
-
return toolError(`Ambiguous file "${file}". Candidates:\n${resolved.candidates.map(c => ' ' + c).join('\n')}`);
|
|
549
|
-
}
|
|
550
|
-
return toolError(`File not found: ${file}`);
|
|
551
|
-
}
|
|
576
|
+
const resolved = resolveAndValidatePath(index, file);
|
|
577
|
+
if (typeof resolved !== 'string') return resolved; // toolError response
|
|
552
578
|
const filePath = resolved;
|
|
553
579
|
|
|
554
580
|
const parts = range.split('-');
|
|
@@ -631,6 +657,16 @@ server.registerTool(
|
|
|
631
657
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
632
658
|
return toolError(`Cannot locate file for ${match.name}`);
|
|
633
659
|
}
|
|
660
|
+
// Validate file is within project root
|
|
661
|
+
try {
|
|
662
|
+
const realPath = fs.realpathSync(filePath);
|
|
663
|
+
const realRoot = fs.realpathSync(index.root);
|
|
664
|
+
if (realPath !== realRoot && !realPath.startsWith(realRoot + path.sep)) {
|
|
665
|
+
return toolError(`File is outside project root: ${match.name}`);
|
|
666
|
+
}
|
|
667
|
+
} catch (e) {
|
|
668
|
+
return toolError(`Cannot resolve file path for ${match.name}`);
|
|
669
|
+
}
|
|
634
670
|
|
|
635
671
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
636
672
|
const fileLines = content.split('\n');
|
|
@@ -732,6 +768,10 @@ server.registerTool(
|
|
|
732
768
|
}
|
|
733
769
|
|
|
734
770
|
case 'diff_impact': {
|
|
771
|
+
// Validate git ref format to prevent argument injection
|
|
772
|
+
if (base && !/^[a-zA-Z0-9._\-~\/^@{}:]+$/.test(base)) {
|
|
773
|
+
return toolError(`Invalid git ref format: ${base}`);
|
|
774
|
+
}
|
|
735
775
|
const index = getIndex(project_dir);
|
|
736
776
|
const result = index.diffImpact({
|
|
737
777
|
base: base || 'HEAD',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.5",
|
|
4
4
|
"description": "Universal Code Navigator — AST-based call graph analysis for AI agents. Find callers, trace impact, detect dead code across JS/TS, Python, Go, Rust, Java, and HTML. CLI, MCP server, and agent skill.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/test/mcp-edge-cases.js
CHANGED
|
@@ -411,6 +411,59 @@ const tests = [
|
|
|
411
411
|
args: { command: 'imports', project_dir: PROJECT_DIR, file: 'utils.js' },
|
|
412
412
|
assert: (res, text, isError) => (isError && /ambiguous/i.test(text)) || 'Expected file-ambiguous error for utils.js'
|
|
413
413
|
},
|
|
414
|
+
|
|
415
|
+
// ========================================================================
|
|
416
|
+
// CATEGORY 3: Security (path traversal, argument injection)
|
|
417
|
+
// ========================================================================
|
|
418
|
+
{
|
|
419
|
+
category: 'Security',
|
|
420
|
+
tool: 'ucn',
|
|
421
|
+
desc: 'lines rejects path traversal (../../../../etc/passwd)',
|
|
422
|
+
args: { command: 'lines', project_dir: PROJECT_DIR, file: '../../../../etc/passwd', range: '1-5' },
|
|
423
|
+
assert: (res, text, isError) => (isError && (/not found/i.test(text) || /outside project/i.test(text))) || 'Expected error for path traversal'
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
category: 'Security',
|
|
427
|
+
tool: 'ucn',
|
|
428
|
+
desc: 'lines rejects path traversal (../../other-project/secret.js)',
|
|
429
|
+
args: { command: 'lines', project_dir: PROJECT_DIR, file: '../../other-project/secret.js', range: '1-5' },
|
|
430
|
+
assert: (res, text, isError) => (isError && (/not found/i.test(text) || /outside project/i.test(text))) || 'Expected error for path traversal'
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
category: 'Security',
|
|
434
|
+
tool: 'ucn',
|
|
435
|
+
desc: 'lines works with valid file',
|
|
436
|
+
args: { command: 'lines', project_dir: PROJECT_DIR, file: 'core/discovery.js', range: '1-3' },
|
|
437
|
+
assert: (res, text, isError) => (!isError && text.length > 0) || 'Expected valid output for core/discovery.js'
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
category: 'Security',
|
|
441
|
+
tool: 'ucn',
|
|
442
|
+
desc: 'diff_impact rejects --config argument injection',
|
|
443
|
+
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: '--config=malicious' },
|
|
444
|
+
assert: (res, text, isError) => (isError && /invalid git ref/i.test(text)) || 'Expected error for argument injection in base'
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
category: 'Security',
|
|
448
|
+
tool: 'ucn',
|
|
449
|
+
desc: 'diff_impact rejects -o flag injection',
|
|
450
|
+
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: '-o /tmp/evil' },
|
|
451
|
+
assert: (res, text, isError) => (isError && /invalid git ref/i.test(text)) || 'Expected error for flag injection in base'
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
category: 'Security',
|
|
455
|
+
tool: 'ucn',
|
|
456
|
+
desc: 'diff_impact accepts valid ref HEAD~3',
|
|
457
|
+
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: 'HEAD~3' },
|
|
458
|
+
assert: (res, text, isError) => true // Should not error on valid ref format
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
category: 'Security',
|
|
462
|
+
tool: 'ucn',
|
|
463
|
+
desc: 'diff_impact accepts valid ref origin/main',
|
|
464
|
+
args: { command: 'diff_impact', project_dir: PROJECT_DIR, base: 'origin/main' },
|
|
465
|
+
assert: (res, text, isError) => true // Should not error on valid ref format
|
|
466
|
+
},
|
|
414
467
|
];
|
|
415
468
|
|
|
416
469
|
// ============================================================================
|
package/test/parser.test.js
CHANGED
|
@@ -13025,5 +13025,59 @@ it('FIX 99 — parseDiff handles quoted paths with special characters', () => {
|
|
|
13025
13025
|
'Literal backslash-n in filename must be preserved, not converted to newline');
|
|
13026
13026
|
});
|
|
13027
13027
|
|
|
13028
|
+
// ============================================================================
|
|
13029
|
+
// Interactive Mode Tests (Fix #100 — missing commands)
|
|
13030
|
+
// ============================================================================
|
|
13031
|
+
|
|
13032
|
+
describe('Interactive Mode', () => {
|
|
13033
|
+
const { execFileSync } = require('child_process');
|
|
13034
|
+
const cliPath = path.join(__dirname, '..', 'cli', 'index.js');
|
|
13035
|
+
|
|
13036
|
+
it('supports all commands without errors', () => {
|
|
13037
|
+
// Test each previously-missing command by piping into interactive mode
|
|
13038
|
+
// Each should not crash and should produce some output (not "Unknown command")
|
|
13039
|
+
const commands = [
|
|
13040
|
+
'deadcode',
|
|
13041
|
+
'related processData',
|
|
13042
|
+
'example processData',
|
|
13043
|
+
'verify processData',
|
|
13044
|
+
'expand 1', // Will say "no expandable items" but won't crash
|
|
13045
|
+
];
|
|
13046
|
+
|
|
13047
|
+
const input = commands.join('\n') + '\nquit\n';
|
|
13048
|
+
|
|
13049
|
+
const result = execFileSync('node', [cliPath, '--interactive', '.'], {
|
|
13050
|
+
input,
|
|
13051
|
+
encoding: 'utf-8',
|
|
13052
|
+
cwd: path.join(__dirname, '..'),
|
|
13053
|
+
timeout: 30000,
|
|
13054
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
13055
|
+
});
|
|
13056
|
+
|
|
13057
|
+
// Verify no "Unknown command" errors for the previously-missing commands
|
|
13058
|
+
assert.ok(!result.includes('Unknown command: deadcode'), 'deadcode should be recognized in interactive mode');
|
|
13059
|
+
assert.ok(!result.includes('Unknown command: related'), 'related should be recognized in interactive mode');
|
|
13060
|
+
assert.ok(!result.includes('Unknown command: example'), 'example should be recognized in interactive mode');
|
|
13061
|
+
assert.ok(!result.includes('Unknown command: verify'), 'verify should be recognized in interactive mode');
|
|
13062
|
+
assert.ok(!result.includes('Unknown command: expand'), 'expand should be recognized in interactive mode');
|
|
13063
|
+
});
|
|
13064
|
+
|
|
13065
|
+
it('help lists all commands', () => {
|
|
13066
|
+
const result = execFileSync('node', [cliPath, '--interactive', '.'], {
|
|
13067
|
+
input: 'help\nquit\n',
|
|
13068
|
+
encoding: 'utf-8',
|
|
13069
|
+
cwd: path.join(__dirname, '..'),
|
|
13070
|
+
timeout: 30000,
|
|
13071
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
13072
|
+
});
|
|
13073
|
+
|
|
13074
|
+
// Verify the help text includes all commands (including newly added ones)
|
|
13075
|
+
const expectedCommands = ['expand', 'deadcode', 'related', 'example', 'verify', 'plan', 'stacktrace'];
|
|
13076
|
+
for (const cmd of expectedCommands) {
|
|
13077
|
+
assert.ok(result.includes(cmd), `Interactive help should list "${cmd}"`);
|
|
13078
|
+
}
|
|
13079
|
+
});
|
|
13080
|
+
});
|
|
13081
|
+
|
|
13028
13082
|
console.log('UCN v3 Test Suite');
|
|
13029
13083
|
console.log('Run with: node --test test/parser.test.js');
|