ucn 3.4.5 → 3.4.6
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/README.md +82 -115
- package/core/project.js +6 -7
- package/mcp/server.js +0 -0
- package/package.json +1 -1
- package/test/mcp-edge-cases.js +39 -2
- package/test/parser.test.js +239 -0
- package/test/reliability-test-prompt.md +0 -58
package/README.md
CHANGED
|
@@ -155,125 +155,11 @@ ucn verify the_function # Did all call sites survive?
|
|
|
155
155
|
ucn deadcode --exclude=test # What can be deleted?
|
|
156
156
|
ucn toc # Project overview
|
|
157
157
|
```
|
|
158
|
+
|
|
158
159
|
## Supported Languages
|
|
159
160
|
|
|
160
161
|
JavaScript, TypeScript, Python, Go, Rust, Java
|
|
161
162
|
|
|
162
|
-
## Install
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
npm install -g ucn
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### MCP Server
|
|
169
|
-
|
|
170
|
-
UCN includes a built-in [MCP](https://modelcontextprotocol.io) server, so any MCP-compatible AI client can use it as a tool.
|
|
171
|
-
|
|
172
|
-
**Claude Code** (`~/.claude/mcp-config.json`):
|
|
173
|
-
```json
|
|
174
|
-
{
|
|
175
|
-
"mcpServers": {
|
|
176
|
-
"ucn": {
|
|
177
|
-
"command": "npx",
|
|
178
|
-
"args": ["-y", "ucn", "--mcp"]
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
**Claude Desktop** (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
185
|
-
```json
|
|
186
|
-
{
|
|
187
|
-
"mcpServers": {
|
|
188
|
-
"ucn": {
|
|
189
|
-
"command": "npx",
|
|
190
|
-
"args": ["-y", "ucn", "--mcp"]
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
**Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json` in project):
|
|
197
|
-
```json
|
|
198
|
-
{
|
|
199
|
-
"mcpServers": {
|
|
200
|
-
"ucn": {
|
|
201
|
-
"command": "npx",
|
|
202
|
-
"args": ["-y", "ucn", "--mcp"]
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**Windsurf** (`~/.codeium/windsurf/mcp_config.json`):
|
|
209
|
-
```json
|
|
210
|
-
{
|
|
211
|
-
"mcpServers": {
|
|
212
|
-
"ucn": {
|
|
213
|
-
"command": "npx",
|
|
214
|
-
"args": ["-y", "ucn", "--mcp"]
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
**VS Code Copilot** (`.vscode/mcp.json`):
|
|
221
|
-
```json
|
|
222
|
-
{
|
|
223
|
-
"servers": {
|
|
224
|
-
"ucn": {
|
|
225
|
-
"type": "stdio",
|
|
226
|
-
"command": "npx",
|
|
227
|
-
"args": ["-y", "ucn", "--mcp"]
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
**Zed** (Settings > `settings.json`):
|
|
234
|
-
```json
|
|
235
|
-
{
|
|
236
|
-
"context_servers": {
|
|
237
|
-
"ucn": {
|
|
238
|
-
"command": "npx",
|
|
239
|
-
"args": ["-y", "ucn", "--mcp"]
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
The MCP server exposes 27 tools: `ucn_about`, `ucn_context`, `ucn_impact`, `ucn_smart`, `ucn_trace`, `ucn_find`, `ucn_usages`, `ucn_toc`, `ucn_deadcode`, `ucn_fn`, `ucn_class`, `ucn_verify`, `ucn_imports`, `ucn_exporters`, `ucn_tests`, `ucn_related`, `ucn_graph`, `ucn_file_exports`, `ucn_search`, `ucn_plan`, `ucn_typedef`, `ucn_stacktrace`, `ucn_example`, `ucn_expand`, `ucn_lines`, `ucn_api`, `ucn_stats`.
|
|
246
|
-
|
|
247
|
-
### Claude Code Skill (alternative)
|
|
248
|
-
|
|
249
|
-
To use UCN as a skill in Claude Code (alternative to MCP):
|
|
250
|
-
|
|
251
|
-
```bash
|
|
252
|
-
mkdir -p ~/.claude/skills
|
|
253
|
-
|
|
254
|
-
# If installed via npm:
|
|
255
|
-
cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.claude/skills/
|
|
256
|
-
|
|
257
|
-
# If cloned from git:
|
|
258
|
-
git clone https://github.com/mleoca/ucn.git
|
|
259
|
-
cp -r ucn/.claude/skills/ucn ~/.claude/skills/
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### Codex (optional)
|
|
263
|
-
|
|
264
|
-
To use UCN as a skill in OpenAI Codex:
|
|
265
|
-
|
|
266
|
-
```bash
|
|
267
|
-
mkdir -p ~/.agents/skills
|
|
268
|
-
|
|
269
|
-
# If installed via npm:
|
|
270
|
-
cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.agents/skills/
|
|
271
|
-
|
|
272
|
-
# If cloned from git:
|
|
273
|
-
git clone https://github.com/mleoca/ucn.git
|
|
274
|
-
cp -r ucn/.claude/skills/ucn ~/.agents/skills/
|
|
275
|
-
```
|
|
276
|
-
|
|
277
163
|
## Usage
|
|
278
164
|
|
|
279
165
|
```
|
|
@@ -368,6 +254,87 @@ Quick Start:
|
|
|
368
254
|
ucn --interactive # Multiple queries
|
|
369
255
|
```
|
|
370
256
|
|
|
257
|
+
## Install
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npm install -g ucn
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### MCP Server
|
|
264
|
+
|
|
265
|
+
UCN includes a built-in [MCP](https://modelcontextprotocol.io) server, so any MCP-compatible AI client can use it as a tool. It exposes 27 tools (`ucn_about`, `ucn_context`, `ucn_impact`, `ucn_smart`, `ucn_trace`, `ucn_find`, `ucn_usages`, `ucn_toc`, `ucn_deadcode`, `ucn_fn`, `ucn_class`, `ucn_verify`, `ucn_imports`, `ucn_exporters`, `ucn_tests`, `ucn_related`, `ucn_graph`, `ucn_file_exports`, `ucn_search`, `ucn_plan`, `ucn_typedef`, `ucn_stacktrace`, `ucn_example`, `ucn_expand`, `ucn_lines`, `ucn_api`, `ucn_stats`).
|
|
266
|
+
|
|
267
|
+
**One-line setup** (for clients that support it):
|
|
268
|
+
|
|
269
|
+
| Client | Command |
|
|
270
|
+
|--------|---------|
|
|
271
|
+
| Claude Code | `claude mcp add ucn -- npx -y ucn --mcp` |
|
|
272
|
+
| OpenAI Codex CLI | `codex mcp add ucn -- npx -y ucn --mcp` |
|
|
273
|
+
| VS Code Copilot | `code --add-mcp '{"name":"ucn","command":"npx","args":["-y","ucn","--mcp"]}'` |
|
|
274
|
+
|
|
275
|
+
**Manual config** — add to the appropriate config file for your client:
|
|
276
|
+
|
|
277
|
+
| Client | Config file |
|
|
278
|
+
|--------|-------------|
|
|
279
|
+
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) |
|
|
280
|
+
| Cursor | `~/.cursor/mcp.json` or `.cursor/mcp.json` in project |
|
|
281
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
282
|
+
| Cline | VS Code sidebar > MCP Servers > Configure |
|
|
283
|
+
| Claude Code | `~/.claude/mcp-config.json` |
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"mcpServers": {
|
|
288
|
+
"ucn": {
|
|
289
|
+
"command": "npx",
|
|
290
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
<details>
|
|
297
|
+
<summary>VS Code Copilot uses a slightly different format (<code>.vscode/mcp.json</code>)</summary>
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"servers": {
|
|
302
|
+
"ucn": {
|
|
303
|
+
"type": "stdio",
|
|
304
|
+
"command": "npx",
|
|
305
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
</details>
|
|
311
|
+
|
|
312
|
+
### Claude Code Skill (alternative to MCP)
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
mkdir -p ~/.claude/skills
|
|
316
|
+
|
|
317
|
+
# If installed via npm:
|
|
318
|
+
cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.claude/skills/
|
|
319
|
+
|
|
320
|
+
# If cloned from git:
|
|
321
|
+
git clone https://github.com/mleoca/ucn.git
|
|
322
|
+
cp -r ucn/.claude/skills/ucn ~/.claude/skills/
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Codex Skill (alternative to MCP)
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
mkdir -p ~/.agents/skills
|
|
329
|
+
|
|
330
|
+
# If installed via npm:
|
|
331
|
+
cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.agents/skills/
|
|
332
|
+
|
|
333
|
+
# If cloned from git:
|
|
334
|
+
git clone https://github.com/mleoca/ucn.git
|
|
335
|
+
cp -r ucn/.claude/skills/ucn ~/.agents/skills/
|
|
336
|
+
```
|
|
337
|
+
|
|
371
338
|
## License
|
|
372
339
|
|
|
373
340
|
MIT
|
package/core/project.js
CHANGED
|
@@ -1803,7 +1803,7 @@ class ProjectIndex {
|
|
|
1803
1803
|
// Find all test files
|
|
1804
1804
|
const testFiles = [];
|
|
1805
1805
|
for (const [filePath, fileEntry] of this.files) {
|
|
1806
|
-
if (isTestFile(
|
|
1806
|
+
if (isTestFile(fileEntry.relativePath, fileEntry.language)) {
|
|
1807
1807
|
testFiles.push({ path: filePath, entry: fileEntry });
|
|
1808
1808
|
}
|
|
1809
1809
|
}
|
|
@@ -2108,14 +2108,13 @@ class ProjectIndex {
|
|
|
2108
2108
|
continue;
|
|
2109
2109
|
}
|
|
2110
2110
|
|
|
2111
|
+
const fileEntry = this.files.get(symbol.file);
|
|
2112
|
+
const lang = fileEntry?.language;
|
|
2113
|
+
|
|
2111
2114
|
// Skip test files unless requested
|
|
2112
|
-
if (!options.includeTests && isTestFile(symbol.
|
|
2115
|
+
if (!options.includeTests && isTestFile(symbol.relativePath, lang)) {
|
|
2113
2116
|
continue;
|
|
2114
2117
|
}
|
|
2115
|
-
|
|
2116
|
-
// Check if exported
|
|
2117
|
-
const fileEntry = this.files.get(symbol.file);
|
|
2118
|
-
const lang = fileEntry?.language;
|
|
2119
2118
|
const mods = symbol.modifiers || [];
|
|
2120
2119
|
|
|
2121
2120
|
// Language-specific entry points (called by runtime, no AST-visible callers)
|
|
@@ -3653,7 +3652,7 @@ class ProjectIndex {
|
|
|
3653
3652
|
totalState += state.length;
|
|
3654
3653
|
totalLines += fileEntry.lines;
|
|
3655
3654
|
totalDynamic += fileEntry.dynamicImports || 0;
|
|
3656
|
-
if (isTestFile(
|
|
3655
|
+
if (isTestFile(fileEntry.relativePath, fileEntry.language)) totalTests += 1;
|
|
3657
3656
|
|
|
3658
3657
|
const entry = {
|
|
3659
3658
|
file: fileEntry.relativePath,
|
package/mcp/server.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/test/mcp-edge-cases.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* MCP Server Edge Case Test Suite
|
|
5
5
|
*
|
|
6
|
-
* Tests
|
|
6
|
+
* Tests UCN MCP tools with null/crash safety, input validation,
|
|
7
7
|
* and normal operation edge cases.
|
|
8
8
|
*
|
|
9
9
|
* Communicates with the MCP server over stdio using newline-delimited JSON-RPC.
|
|
@@ -13,7 +13,7 @@ const { spawn } = require('child_process');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
|
|
15
15
|
const SERVER_PATH = path.join(__dirname, '..', 'mcp', 'server.js');
|
|
16
|
-
const PROJECT_DIR = '
|
|
16
|
+
const PROJECT_DIR = path.resolve(__dirname, '..');
|
|
17
17
|
const TIMEOUT_MS = 30000;
|
|
18
18
|
|
|
19
19
|
// ============================================================================
|
|
@@ -242,6 +242,25 @@ const tests = [
|
|
|
242
242
|
args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
243
243
|
},
|
|
244
244
|
|
|
245
|
+
{
|
|
246
|
+
category: 'Null/Crash Safety',
|
|
247
|
+
tool: 'ucn_api',
|
|
248
|
+
desc: 'nonexistent file',
|
|
249
|
+
args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
category: 'Null/Crash Safety',
|
|
253
|
+
tool: 'ucn_lines',
|
|
254
|
+
desc: 'nonexistent file',
|
|
255
|
+
args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js', range: '1-10' }
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
category: 'Null/Crash Safety',
|
|
259
|
+
tool: 'ucn_expand',
|
|
260
|
+
desc: 'no prior context call',
|
|
261
|
+
args: { project_dir: PROJECT_DIR, number: 1 }
|
|
262
|
+
},
|
|
263
|
+
|
|
245
264
|
// ========================================================================
|
|
246
265
|
// CATEGORY 2: Input Validation
|
|
247
266
|
// ========================================================================
|
|
@@ -321,6 +340,24 @@ const tests = [
|
|
|
321
340
|
desc: 'search for "TODO"',
|
|
322
341
|
args: { project_dir: PROJECT_DIR, term: 'TODO' }
|
|
323
342
|
},
|
|
343
|
+
{
|
|
344
|
+
category: 'Normal Operations',
|
|
345
|
+
tool: 'ucn_api',
|
|
346
|
+
desc: 'project API',
|
|
347
|
+
args: { project_dir: PROJECT_DIR }
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
category: 'Normal Operations',
|
|
351
|
+
tool: 'ucn_stats',
|
|
352
|
+
desc: 'project stats',
|
|
353
|
+
args: { project_dir: PROJECT_DIR }
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
category: 'Normal Operations',
|
|
357
|
+
tool: 'ucn_lines',
|
|
358
|
+
desc: 'extract lines 1-5 from discovery.js',
|
|
359
|
+
args: { project_dir: PROJECT_DIR, file: 'core/discovery.js', range: '1-5' }
|
|
360
|
+
},
|
|
324
361
|
];
|
|
325
362
|
|
|
326
363
|
// ============================================================================
|
package/test/parser.test.js
CHANGED
|
@@ -6392,5 +6392,244 @@ class DataService:
|
|
|
6392
6392
|
});
|
|
6393
6393
|
});
|
|
6394
6394
|
|
|
6395
|
+
// Regression: isTestFile should use relative paths, not absolute paths
|
|
6396
|
+
// Bug: When project lived at /Users/x/test/project/, the /test/ in the parent
|
|
6397
|
+
// path matched the Python test pattern /\/tests?\//, marking ALL files as test files.
|
|
6398
|
+
// This caused deadcode to either miss real dead code or produce false positives.
|
|
6399
|
+
describe('Regression: deadcode uses relative paths for isTestFile', () => {
|
|
6400
|
+
it('should not treat non-test files as test files when project is inside a /test/ directory', () => {
|
|
6401
|
+
// Simulate a project inside a directory named "test"
|
|
6402
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-relpath-${Date.now()}`, 'test', 'myproject');
|
|
6403
|
+
const toolsDir = path.join(tmpDir, 'tools');
|
|
6404
|
+
fs.mkdirSync(toolsDir, { recursive: true });
|
|
6405
|
+
|
|
6406
|
+
try {
|
|
6407
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
6408
|
+
fs.writeFileSync(path.join(toolsDir, '__init__.py'), '');
|
|
6409
|
+
fs.writeFileSync(path.join(toolsDir, 'helper.py'), `
|
|
6410
|
+
def unused_helper():
|
|
6411
|
+
return 42
|
|
6412
|
+
|
|
6413
|
+
def used_helper():
|
|
6414
|
+
return 1
|
|
6415
|
+
`);
|
|
6416
|
+
fs.writeFileSync(path.join(tmpDir, 'main.py'), `
|
|
6417
|
+
from tools.helper import used_helper
|
|
6418
|
+
|
|
6419
|
+
def main():
|
|
6420
|
+
print(used_helper())
|
|
6421
|
+
`);
|
|
6422
|
+
|
|
6423
|
+
const index = new ProjectIndex(tmpDir);
|
|
6424
|
+
index.build(null, { quiet: true });
|
|
6425
|
+
|
|
6426
|
+
const dead = index.deadcode();
|
|
6427
|
+
const deadNames = dead.map(d => d.name);
|
|
6428
|
+
|
|
6429
|
+
// unused_helper should be flagged as dead code
|
|
6430
|
+
assert.ok(deadNames.includes('unused_helper'),
|
|
6431
|
+
`unused_helper should be flagged as dead code, got: ${deadNames.join(', ')}`);
|
|
6432
|
+
|
|
6433
|
+
// used_helper should NOT be flagged
|
|
6434
|
+
assert.ok(!deadNames.includes('used_helper'),
|
|
6435
|
+
`used_helper should not be flagged as dead code`);
|
|
6436
|
+
} finally {
|
|
6437
|
+
fs.rmSync(path.join(os.tmpdir(), `ucn-test-relpath-${Date.now()}`), { recursive: true, force: true });
|
|
6438
|
+
// Clean up the created dir tree
|
|
6439
|
+
const topDir = tmpDir.split('/test/myproject')[0];
|
|
6440
|
+
if (topDir.includes('ucn-test-relpath')) {
|
|
6441
|
+
fs.rmSync(topDir, { recursive: true, force: true });
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
});
|
|
6445
|
+
|
|
6446
|
+
it('should correctly filter test files even when project is inside /test/ directory', () => {
|
|
6447
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-relpath2-${Date.now()}`, 'test', 'myproject');
|
|
6448
|
+
const testsDir = path.join(tmpDir, 'tests');
|
|
6449
|
+
fs.mkdirSync(testsDir, { recursive: true });
|
|
6450
|
+
|
|
6451
|
+
try {
|
|
6452
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
6453
|
+
fs.writeFileSync(path.join(tmpDir, 'app.py'), `
|
|
6454
|
+
def exported_func():
|
|
6455
|
+
return 42
|
|
6456
|
+
|
|
6457
|
+
def unused_func():
|
|
6458
|
+
return 0
|
|
6459
|
+
`);
|
|
6460
|
+
fs.writeFileSync(path.join(testsDir, 'test_app.py'), `
|
|
6461
|
+
from app import exported_func
|
|
6462
|
+
|
|
6463
|
+
def test_exported():
|
|
6464
|
+
assert exported_func() == 42
|
|
6465
|
+
|
|
6466
|
+
def _helper_in_test():
|
|
6467
|
+
return 'setup'
|
|
6468
|
+
`);
|
|
6469
|
+
|
|
6470
|
+
const index = new ProjectIndex(tmpDir);
|
|
6471
|
+
index.build(null, { quiet: true });
|
|
6472
|
+
|
|
6473
|
+
// Default: test files excluded
|
|
6474
|
+
const deadDefault = index.deadcode();
|
|
6475
|
+
const deadDefaultNames = deadDefault.map(d => d.name);
|
|
6476
|
+
|
|
6477
|
+
// unused_func from app.py should appear
|
|
6478
|
+
assert.ok(deadDefaultNames.includes('unused_func'),
|
|
6479
|
+
`unused_func should be in deadcode results`);
|
|
6480
|
+
|
|
6481
|
+
// _helper_in_test from test file should NOT appear (test files excluded by default)
|
|
6482
|
+
assert.ok(!deadDefaultNames.includes('_helper_in_test'),
|
|
6483
|
+
`_helper_in_test should not appear without --include-tests`);
|
|
6484
|
+
|
|
6485
|
+
// With --include-tests: test file symbols should appear
|
|
6486
|
+
const deadWithTests = index.deadcode({ includeTests: true });
|
|
6487
|
+
const deadWithTestsNames = deadWithTests.map(d => d.name);
|
|
6488
|
+
|
|
6489
|
+
assert.ok(deadWithTestsNames.includes('_helper_in_test'),
|
|
6490
|
+
`_helper_in_test should appear with --include-tests`);
|
|
6491
|
+
|
|
6492
|
+
// test_* functions should still be excluded (they're entry points)
|
|
6493
|
+
assert.ok(!deadWithTestsNames.includes('test_exported'),
|
|
6494
|
+
`test_exported should not be flagged (entry point)`);
|
|
6495
|
+
} finally {
|
|
6496
|
+
const topDir = tmpDir.split('/test/myproject')[0];
|
|
6497
|
+
if (topDir.includes('ucn-test-relpath2')) {
|
|
6498
|
+
fs.rmSync(topDir, { recursive: true, force: true });
|
|
6499
|
+
}
|
|
6500
|
+
}
|
|
6501
|
+
});
|
|
6502
|
+
});
|
|
6503
|
+
|
|
6504
|
+
// Regression: deadcode --include-exported should not include test file symbols
|
|
6505
|
+
// unless --include-tests is also specified
|
|
6506
|
+
describe('Regression: deadcode --include-exported respects test file filtering', () => {
|
|
6507
|
+
it('should not show test methods when only --include-exported is set', () => {
|
|
6508
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-exported-${Date.now()}`);
|
|
6509
|
+
const testsDir = path.join(tmpDir, 'tests');
|
|
6510
|
+
fs.mkdirSync(testsDir, { recursive: true });
|
|
6511
|
+
|
|
6512
|
+
try {
|
|
6513
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
6514
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.py'), `
|
|
6515
|
+
def public_func():
|
|
6516
|
+
return 42
|
|
6517
|
+
`);
|
|
6518
|
+
fs.writeFileSync(path.join(testsDir, 'test_lib.py'), `
|
|
6519
|
+
from lib import public_func
|
|
6520
|
+
|
|
6521
|
+
class TestLib:
|
|
6522
|
+
def test_public_func(self):
|
|
6523
|
+
assert public_func() == 42
|
|
6524
|
+
|
|
6525
|
+
def test_another(self):
|
|
6526
|
+
assert True
|
|
6527
|
+
`);
|
|
6528
|
+
|
|
6529
|
+
const index = new ProjectIndex(tmpDir);
|
|
6530
|
+
index.build(null, { quiet: true });
|
|
6531
|
+
|
|
6532
|
+
// --include-exported but NOT --include-tests
|
|
6533
|
+
const dead = index.deadcode({ includeExported: true, includeTests: false });
|
|
6534
|
+
const deadNames = dead.map(d => d.name);
|
|
6535
|
+
|
|
6536
|
+
// Test methods should NOT appear
|
|
6537
|
+
assert.ok(!deadNames.includes('test_public_func'),
|
|
6538
|
+
`test methods should not appear with only --include-exported`);
|
|
6539
|
+
assert.ok(!deadNames.includes('test_another'),
|
|
6540
|
+
`test methods should not appear with only --include-exported`);
|
|
6541
|
+
} finally {
|
|
6542
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6543
|
+
}
|
|
6544
|
+
});
|
|
6545
|
+
});
|
|
6546
|
+
|
|
6547
|
+
// Regression: isTestFile relative path fix applies to all languages (not just Python)
|
|
6548
|
+
// Rust has /\/tests\// pattern that could match parent directories
|
|
6549
|
+
describe('Regression: deadcode relative path fix works for Rust projects', () => {
|
|
6550
|
+
it('should not treat non-test Rust files as test files when project is inside /tests/ directory', () => {
|
|
6551
|
+
// Project lives inside a directory called "tests"
|
|
6552
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-rust-relpath-${Date.now()}`, 'tests', 'myproject');
|
|
6553
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
6554
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
6555
|
+
|
|
6556
|
+
try {
|
|
6557
|
+
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"');
|
|
6558
|
+
fs.writeFileSync(path.join(srcDir, 'lib.rs'), `
|
|
6559
|
+
fn unused_helper() -> i32 {
|
|
6560
|
+
42
|
|
6561
|
+
}
|
|
6562
|
+
|
|
6563
|
+
pub fn used_func() -> i32 {
|
|
6564
|
+
unused_helper()
|
|
6565
|
+
}
|
|
6566
|
+
`);
|
|
6567
|
+
|
|
6568
|
+
const index = new ProjectIndex(tmpDir);
|
|
6569
|
+
index.build(null, { quiet: true });
|
|
6570
|
+
|
|
6571
|
+
const dead = index.deadcode();
|
|
6572
|
+
const deadNames = dead.map(d => d.name);
|
|
6573
|
+
|
|
6574
|
+
// unused_helper should be flagged (it has a caller but let's check it's not filtered)
|
|
6575
|
+
// The key assertion: src/lib.rs should NOT be treated as a test file
|
|
6576
|
+
const { isTestFile } = require('../core/discovery');
|
|
6577
|
+
assert.ok(!isTestFile('src/lib.rs', 'rust'),
|
|
6578
|
+
'src/lib.rs should not be a test file');
|
|
6579
|
+
|
|
6580
|
+
// Verify the old bug: absolute path WOULD falsely match
|
|
6581
|
+
const absPath = path.join(tmpDir, 'src', 'lib.rs');
|
|
6582
|
+
// The absolute path contains /tests/ from parent dir
|
|
6583
|
+
assert.ok(absPath.includes('/tests/'),
|
|
6584
|
+
'Absolute path should contain /tests/ from parent directory');
|
|
6585
|
+
} finally {
|
|
6586
|
+
const topDir = tmpDir.split('/tests/myproject')[0];
|
|
6587
|
+
if (topDir.includes('ucn-test-rust-relpath')) {
|
|
6588
|
+
fs.rmSync(topDir, { recursive: true, force: true });
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
});
|
|
6592
|
+
});
|
|
6593
|
+
|
|
6594
|
+
// Regression: deadcode relative path fix works for JS projects with __tests__ in parent
|
|
6595
|
+
describe('Regression: deadcode relative path fix works for JS projects', () => {
|
|
6596
|
+
it('should not treat non-test JS files as test files when project is inside /__tests__/ directory', () => {
|
|
6597
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-js-relpath-${Date.now()}`, '__tests__', 'myproject');
|
|
6598
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
6599
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
6600
|
+
|
|
6601
|
+
try {
|
|
6602
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
6603
|
+
fs.writeFileSync(path.join(srcDir, 'utils.js'), `
|
|
6604
|
+
function unusedUtil() {
|
|
6605
|
+
return 42;
|
|
6606
|
+
}
|
|
6607
|
+
|
|
6608
|
+
function usedUtil() {
|
|
6609
|
+
return unusedUtil();
|
|
6610
|
+
}
|
|
6611
|
+
|
|
6612
|
+
module.exports = { usedUtil };
|
|
6613
|
+
`);
|
|
6614
|
+
|
|
6615
|
+
const index = new ProjectIndex(tmpDir);
|
|
6616
|
+
index.build(null, { quiet: true });
|
|
6617
|
+
|
|
6618
|
+
const dead = index.deadcode();
|
|
6619
|
+
const deadNames = dead.map(d => d.name);
|
|
6620
|
+
|
|
6621
|
+
// src/utils.js should NOT be treated as a test file
|
|
6622
|
+
const { isTestFile } = require('../core/discovery');
|
|
6623
|
+
assert.ok(!isTestFile('src/utils.js', 'javascript'),
|
|
6624
|
+
'src/utils.js should not be a test file');
|
|
6625
|
+
} finally {
|
|
6626
|
+
const topDir = tmpDir.split('/__tests__/myproject')[0];
|
|
6627
|
+
if (topDir.includes('ucn-test-js-relpath')) {
|
|
6628
|
+
fs.rmSync(topDir, { recursive: true, force: true });
|
|
6629
|
+
}
|
|
6630
|
+
}
|
|
6631
|
+
});
|
|
6632
|
+
});
|
|
6633
|
+
|
|
6395
6634
|
console.log('UCN v3 Test Suite');
|
|
6396
6635
|
console.log('Run with: node --test test/parser.test.js');
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
Run UCN reliability tests on 5 real-world projects. IMPORTANT: use node /Users/mihail/ucn/cli/index.js (NOT the global ucn) to test the current dev version. Use --clear-cache on every command.
|
|
2
|
-
|
|
3
|
-
First run the unit tests: node --test test/parser.test.js — should be 212 pass, 0 fail.
|
|
4
|
-
|
|
5
|
-
1. Clone these repos to /tmp/ucn-reliability-test-2/ (shallow clone --depth 1):
|
|
6
|
-
- fastify: https://github.com/fastify/fastify.git (JavaScript)
|
|
7
|
-
- httpx: https://github.com/encode/httpx.git (Python)
|
|
8
|
-
- hugo: https://github.com/gohugoio/hugo.git (Go)
|
|
9
|
-
- alacritty: https://github.com/alacritty/alacritty.git (Rust)
|
|
10
|
-
- spring-boot: https://github.com/spring-projects/spring-boot.git (Java — use spring-boot-project/spring-boot/src/main/java/org/springframework/boot only, pass that as the target path)
|
|
11
|
-
|
|
12
|
-
2. Launch 5 parallel agents (one per project). Each agent should run ALL of these commands with --clear-cache and 30s timeout:
|
|
13
|
-
|
|
14
|
-
toc, toc --detailed, find <sym1>, find <sym2>, about <sym1>,
|
|
15
|
-
context <class>, context <function>, impact <sym1>, smart <sym1>,
|
|
16
|
-
trace <sym1> --depth=2, usages <sym2>, deadcode, fn <method>,
|
|
17
|
-
imports <file>, exporters <file>, search "<term>", verify <sym1>,
|
|
18
|
-
find nonexistentXYZ (edge case), context "" (edge case)
|
|
19
|
-
|
|
20
|
-
3. Key symbols per project:
|
|
21
|
-
- fastify: route, FastifyInstance, inject, Reply, register
|
|
22
|
-
- httpx: get, Client, request, Response, __init__
|
|
23
|
-
- hugo: Build, Site, Execute, Page, New
|
|
24
|
-
- alacritty: main, update, Display, build, new
|
|
25
|
-
- spring-boot: run, SpringApplication, main, ApplicationContext, refresh
|
|
26
|
-
|
|
27
|
-
For `fn` command, specifically test class methods (Python __init__, Java overloaded methods) — these were broken before and fixed in this version.
|
|
28
|
-
|
|
29
|
-
4. Focus areas for EVERY command — check ALL of the following (AST tree-sitter reliability is the main focus):
|
|
30
|
-
- toc shows correct file/function/class counts, no crashes on large projects
|
|
31
|
-
- toc --detailed lists all symbols with line ranges, no truncation errors
|
|
32
|
-
- find returns ranked results by usage count, no duplicates
|
|
33
|
-
- about prefers lib/src/core definitions over test/example files, shows usages/callers/callees/code
|
|
34
|
-
- context <class> shows class name (not "undefined"), lists methods with signatures
|
|
35
|
-
- context <function> shows callers and callees with correct counts and weights
|
|
36
|
-
- context for non-existent symbols returns "Symbol not found" (NOT empty callers/callees)
|
|
37
|
-
- impact groups call sites by file with code context, shows argument patterns
|
|
38
|
-
- smart shows function code + inlined dependency code, overloads show ALL overload callees
|
|
39
|
-
- trace builds correct call tree with depth levels, weights, and multiplicity
|
|
40
|
-
- usages categorizes by type (definition/call/import/reference), no false positives
|
|
41
|
-
- deadcode completes within timeout, correctly excludes entry points (main, init, __init__, #[test])
|
|
42
|
-
- fn extracts correct code for BOTH top-level functions AND class methods (previously broken, now fixed)
|
|
43
|
-
- fn auto-resolves best definition (prefers lib/src over test)
|
|
44
|
-
- imports shows internal + external imports with resolved paths
|
|
45
|
-
- exporters finds importers for the file (Python relative imports work, Java package imports resolve)
|
|
46
|
-
- search returns matches with file grouping, respects project ignores
|
|
47
|
-
- verify checks call signatures, totalCalls equals valid+mismatches+uncertain (no inflated counts from filtered method calls)
|
|
48
|
-
- Edge cases: find nonexistentXYZ returns clean "not found", context "" returns usage error (no crash)
|
|
49
|
-
|
|
50
|
-
5. Each agent reports: command, status (OK/ERROR/CRASH/UNEXPECTED), notes.
|
|
51
|
-
At the end: total pass/fail, issues by severity, patterns.
|
|
52
|
-
|
|
53
|
-
6. After all agents finish, compile a cross-project summary comparing results to the previous run (which had 85/95 = 89.5% before fixes, target is 95%+ with 0 crashes). The fixes applied were:
|
|
54
|
-
- fn command now uses symbol index startLine/endLine directly (fixes class method extraction)
|
|
55
|
-
- verify totalCalls now computed as valid+mismatches+uncertain (fixes inflated counts)
|
|
56
|
-
- context returns null for undefined symbols (CLI shows "Symbol not found")
|
|
57
|
-
|
|
58
|
-
All code uses tree-sitter AST parsing — reliability and correctness of AST-based analysis is the main focus. Any crash, incorrect result, or hang is a bug.
|