Skip to content

Commit 2f9730a

Browse files
fix: statsData fully filters test nodes/edges, add MCP hotspots no_tests
- statsData now filters nodes and edges by excluding all nodes belonging to test files (previously only file counts and hotspots were filtered) - Add no_tests schema property and handler passthrough to MCP hotspots tool - Add 7 integration tests verifying noTests filtering behavior across queryNameData, statsData, impactAnalysisData, fileDepsData, moduleMapData
1 parent c519552 commit 2f9730a

3 files changed

Lines changed: 127 additions & 2 deletions

File tree

src/mcp.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ const BASE_TOOLS = [
286286
description: 'Rank files or directories',
287287
},
288288
limit: { type: 'number', description: 'Number of results to return', default: 10 },
289+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
289290
},
290291
},
291292
},
@@ -540,6 +541,7 @@ export async function startMCPServer(customDbPath, options = {}) {
540541
metric: args.metric,
541542
level: args.level,
542543
limit: args.limit,
544+
noTests: args.no_tests,
543545
});
544546
break;
545547
}

src/queries.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -739,8 +739,36 @@ export function statsData(customDbPath, opts = {}) {
739739
const db = openReadonlyOrFail(customDbPath);
740740
const noTests = opts.noTests || false;
741741

742+
// Build set of test file IDs for filtering nodes and edges
743+
let testFileIds = null;
744+
if (noTests) {
745+
const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
746+
testFileIds = new Set();
747+
const testFiles = new Set();
748+
for (const n of allFileNodes) {
749+
if (isTestFile(n.file)) {
750+
testFileIds.add(n.id);
751+
testFiles.add(n.file);
752+
}
753+
}
754+
// Also collect non-file node IDs that belong to test files
755+
const allNodes = db.prepare('SELECT id, file FROM nodes').all();
756+
for (const n of allNodes) {
757+
if (testFiles.has(n.file)) testFileIds.add(n.id);
758+
}
759+
}
760+
742761
// Node breakdown by kind
743-
const nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
762+
let nodeRows;
763+
if (noTests) {
764+
const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
765+
const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
766+
const counts = {};
767+
for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
768+
nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
769+
} else {
770+
nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
771+
}
744772
const nodesByKind = {};
745773
let totalNodes = 0;
746774
for (const r of nodeRows) {
@@ -749,7 +777,18 @@ export function statsData(customDbPath, opts = {}) {
749777
}
750778

751779
// Edge breakdown by kind
752-
const edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
780+
let edgeRows;
781+
if (noTests) {
782+
const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
783+
const filtered = allEdges.filter(
784+
(e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
785+
);
786+
const counts = {};
787+
for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
788+
edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
789+
} else {
790+
edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
791+
}
753792
const edgesByKind = {};
754793
let totalEdges = 0;
755794
for (const r of edgeRows) {

tests/integration/queries.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
impactAnalysisData,
3535
moduleMapData,
3636
queryNameData,
37+
statsData,
3738
whereData,
3839
} from '../../src/queries.js';
3940

@@ -470,3 +471,86 @@ describe('whereData', () => {
470471
expect(data.results).toHaveLength(0);
471472
});
472473
});
474+
475+
// ─── noTests filtering ───────────────────────────────────────────────
476+
477+
describe('noTests filtering', () => {
478+
test('queryNameData excludes test file nodes and callers', () => {
479+
const all = queryNameData('authenticate', dbPath);
480+
const filtered = queryNameData('authenticate', dbPath, { noTests: true });
481+
482+
const allFn = all.results.find((r) => r.name === 'authenticate' && r.kind === 'function');
483+
const filteredFn = filtered.results.find(
484+
(r) => r.name === 'authenticate' && r.kind === 'function',
485+
);
486+
487+
// testAuthenticate should be in callers without filter
488+
expect(allFn.callers.map((c) => c.name)).toContain('testAuthenticate');
489+
// testAuthenticate should be excluded with noTests
490+
expect(filteredFn.callers.map((c) => c.name)).not.toContain('testAuthenticate');
491+
});
492+
493+
test('queryNameData excludes test file results', () => {
494+
const all = queryNameData('testAuthenticate', dbPath);
495+
const filtered = queryNameData('testAuthenticate', dbPath, { noTests: true });
496+
497+
expect(all.results).toHaveLength(1);
498+
expect(filtered.results).toHaveLength(0);
499+
});
500+
501+
test('statsData excludes test files from counts', () => {
502+
const all = statsData(dbPath);
503+
const filtered = statsData(dbPath, { noTests: true });
504+
505+
// File count should be lower
506+
expect(filtered.files.total).toBeLessThan(all.files.total);
507+
// Node count should be lower (test file + testAuthenticate removed)
508+
expect(filtered.nodes.total).toBeLessThan(all.nodes.total);
509+
// Edge count should be lower (test import + test call edge removed)
510+
expect(filtered.edges.total).toBeLessThan(all.edges.total);
511+
});
512+
513+
test('statsData hotspots exclude test files', () => {
514+
const filtered = statsData(dbPath, { noTests: true });
515+
for (const h of filtered.hotspots) {
516+
expect(h.file).not.toMatch(/\.test\./);
517+
}
518+
});
519+
520+
test('impactAnalysisData excludes test dependents', () => {
521+
const all = impactAnalysisData('auth.js', dbPath);
522+
const filtered = impactAnalysisData('auth.js', dbPath, { noTests: true });
523+
524+
const allFiles = Object.values(all.levels)
525+
.flat()
526+
.map((f) => f.file);
527+
const filteredFiles = Object.values(filtered.levels)
528+
.flat()
529+
.map((f) => f.file);
530+
531+
expect(allFiles).toContain('auth.test.js');
532+
expect(filteredFiles).not.toContain('auth.test.js');
533+
});
534+
535+
test('fileDepsData excludes test importers', () => {
536+
const all = fileDepsData('auth.js', dbPath);
537+
const filtered = fileDepsData('auth.js', dbPath, { noTests: true });
538+
539+
const allImportedBy = all.results[0].importedBy.map((i) => i.file);
540+
const filteredImportedBy = filtered.results[0].importedBy.map((i) => i.file);
541+
542+
expect(allImportedBy).toContain('auth.test.js');
543+
expect(filteredImportedBy).not.toContain('auth.test.js');
544+
});
545+
546+
test('moduleMapData excludes test files', () => {
547+
const all = moduleMapData(dbPath, 20);
548+
const filtered = moduleMapData(dbPath, 20, { noTests: true });
549+
550+
const allFiles = all.topNodes.map((n) => n.file);
551+
const filteredFiles = filtered.topNodes.map((n) => n.file);
552+
553+
expect(allFiles).toContain('auth.test.js');
554+
expect(filteredFiles).not.toContain('auth.test.js');
555+
});
556+
});

0 commit comments

Comments
 (0)