Skip to content

Commit 7853fa5

Browse files
prosdevclaude
andcommitted
test(mcp): add adapter-level tests for dependsOn and connected components
Cover the two untested graph feature paths: RefsAdapter dependsOn (path tracing, hop count, no-path, missing indexer) and MapAdapter connected components (multi-cluster subsystems, single-cluster skip). Clean up stale scratchpad entries for already-wired features. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3dff35e commit 7853fa5

3 files changed

Lines changed: 208 additions & 5 deletions

File tree

.claude/scratchpad.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@
1212
## Future Work
1313

1414
- Antfly SDK: server-side path filter for `getDocsByFilePath` (eliminates 5k cap)
15-
- Wire `shortestPath` into `dev_refs` as a "trace path" feature (graph.ts is ready, adapter wiring is separate scope)
16-
- Wire `connectedComponents` into `dev_map` verbose output (graph.ts is ready)
1715
- Betweenness centrality — identifies bridge files between subsystems. Worth adding if agents need refactoring guidance. graphology (MIT, 1.6k stars) is the upgrade path if we need more than 3 hand-rolled algorithms.
1816
- **Connected components hub filtering** — widely-shared utility files (e.g., logger.ts imported by 50+ files) merge separate subsystems into one component. Filter out hub nodes (high in-degree) before computing components for better subsystem identification.
1917
- **PageRank at 10k+ nodes** — convergence tolerance 1e-6 may require all 100 iterations for large sparse graphs. Monitor performance. Consider reducing maxIterations or loosening tolerance for dev_map where approximate ranks are fine.
2018
- **getAll(limit: 10000) truncation** — medium-large monorepos may exceed 10k docs. Warning is logged but results are silently incomplete. Long-term: paginate or make limit configurable.
2119
- E2E tests in CI — blocked on Antfly memory requirements vs GitHub runner limits (7GB)
22-
- **Python language support** — plan written at `.claude/da-plans/core/phase-4-python-support/`. 4 parts: bundle WASM + queries, PythonScanner, pattern rules, test fixtures + docs.
2320
- Vue/Svelte SFC support — `.vue`/`.svelte` files have embedded `<script lang="ts">` blocks. Would need script block extraction before tree-sitter parsing. Lower priority — co-located `.ts` files in those projects already get full analysis.
2421
- Swap `WasmPatternMatcher` to `@ast-grep/napi` if bulk scanning perf becomes an issue (~4x faster native Rust). Interface is ready; implementation is mechanical.
2522

@@ -29,12 +26,10 @@
2926

3027
## Test Gaps
3128

32-
- **RefsAdapter integration test with `dependsOn`.** The `traceTo` path tracing feature is tested at the algorithm level (shortestPath in graph.test.ts) but not at the adapter level. Needs a test that constructs RefsAdapter with a mock indexer, calls `execute()` with `traceTo`, and verifies the path output format. Also needs a test for the error case when indexer is missing.
3329
- **InspectAdapter integration test with PatternMatcher.** The InspectAdapter test constructs without a `patternMatcher` — the AST path is never exercised through the MCP layer. Needs a test that constructs `InspectAdapter` with `createPatternMatcher()`, mocks the search service, calls `execute()`, and verifies AST-enhanced results flow through. Requires mock search service setup — larger integration test scope.
3430

3531
## Tech Debt
3632

37-
- **`isTestFile()` is hardcoded for JS/TS and Go.** Only checks for `.test.`, `.spec.` (JS/TS) and `_test.go` (Go). Python needs `test_*.py`, `*_test.py`, `conftest.py`. Future languages will need their own patterns. Should be refactored to a language-aware registry or pattern map instead of growing if/else chains. Tracked in Phase 4 Part 4.2.
3833

3934
## Notes
4035

packages/mcp-server/src/adapters/__tests__/map-adapter.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,122 @@ describe('MapAdapter', () => {
259259
});
260260
});
261261

262+
describe('Connected Components', () => {
263+
it('should include subsystems when docs have call graph edges', async () => {
264+
// Docs with callees forming two separate clusters
265+
const clusterDocs: SearchResult[] = [
266+
{
267+
id: 'packages/core/src/a.ts:fnA:1',
268+
score: 0.9,
269+
metadata: {
270+
path: 'packages/core/src/a.ts',
271+
type: 'function',
272+
name: 'fnA',
273+
exported: true,
274+
callees: [{ name: 'fnB', file: 'packages/core/src/b.ts', line: 1 }],
275+
},
276+
},
277+
{
278+
id: 'packages/core/src/b.ts:fnB:1',
279+
score: 0.9,
280+
metadata: {
281+
path: 'packages/core/src/b.ts',
282+
type: 'function',
283+
name: 'fnB',
284+
exported: true,
285+
callees: [],
286+
},
287+
},
288+
{
289+
id: 'packages/mcp/src/x.ts:fnX:1',
290+
score: 0.9,
291+
metadata: {
292+
path: 'packages/mcp/src/x.ts',
293+
type: 'function',
294+
name: 'fnX',
295+
exported: true,
296+
callees: [{ name: 'fnY', file: 'packages/mcp/src/y.ts', line: 1 }],
297+
},
298+
},
299+
{
300+
id: 'packages/mcp/src/y.ts:fnY:1',
301+
score: 0.9,
302+
metadata: {
303+
path: 'packages/mcp/src/y.ts',
304+
type: 'function',
305+
name: 'fnY',
306+
exported: true,
307+
callees: [],
308+
},
309+
},
310+
];
311+
312+
const clusterIndexer = {
313+
search: vi.fn().mockResolvedValue(clusterDocs),
314+
getAll: vi.fn().mockResolvedValue(clusterDocs),
315+
} as unknown as RepositoryIndexer;
316+
317+
const clusterAdapter = new MapAdapter({
318+
repositoryIndexer: clusterIndexer,
319+
defaultDepth: 3,
320+
defaultTokenBudget: 5000,
321+
});
322+
await clusterAdapter.initialize(context);
323+
324+
const result = await clusterAdapter.execute({ depth: 3 }, execContext);
325+
326+
expect(result.success).toBe(true);
327+
expect(result.data).toContain('Subsystems');
328+
expect(result.data).toContain('connected');
329+
});
330+
331+
it('should not show subsystems section when all docs are in one cluster', async () => {
332+
// All docs in same cluster (single connected component)
333+
const singleClusterDocs: SearchResult[] = [
334+
{
335+
id: 'src/a.ts:fnA:1',
336+
score: 0.9,
337+
metadata: {
338+
path: 'src/a.ts',
339+
type: 'function',
340+
name: 'fnA',
341+
exported: true,
342+
callees: [{ name: 'fnB', file: 'src/b.ts', line: 1 }],
343+
},
344+
},
345+
{
346+
id: 'src/b.ts:fnB:1',
347+
score: 0.9,
348+
metadata: {
349+
path: 'src/b.ts',
350+
type: 'function',
351+
name: 'fnB',
352+
exported: true,
353+
callees: [{ name: 'fnA', file: 'src/a.ts', line: 1 }],
354+
},
355+
},
356+
];
357+
358+
const singleIndexer = {
359+
search: vi.fn().mockResolvedValue(singleClusterDocs),
360+
getAll: vi.fn().mockResolvedValue(singleClusterDocs),
361+
} as unknown as RepositoryIndexer;
362+
363+
const singleAdapter = new MapAdapter({
364+
repositoryIndexer: singleIndexer,
365+
defaultDepth: 2,
366+
defaultTokenBudget: 5000,
367+
});
368+
await singleAdapter.initialize(context);
369+
370+
const result = await singleAdapter.execute({}, execContext);
371+
372+
expect(result.success).toBe(true);
373+
// Only 1 component — formatCodebaseMap skips the section when <= 1
374+
expect(result.data).not.toContain('Subsystems');
375+
});
376+
});
377+
262378
describe('Token Estimation', () => {
263379
it('should estimate tokens based on depth', () => {
264380
const shallow = adapter.estimateTokens({ depth: 1 });

packages/mcp-server/src/adapters/__tests__/refs-adapter.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,98 @@ describe('RefsAdapter', () => {
268268
});
269269
});
270270

271+
describe('Dependency Path Tracing (dependsOn)', () => {
272+
let adapterWithIndexer: RefsAdapter;
273+
274+
// Mock indexer that returns docs with callees forming a chain:
275+
// src/planner.ts → src/github.ts → src/api.ts
276+
const chainDocs: SearchResult[] = [
277+
{
278+
id: '1',
279+
score: 0.9,
280+
metadata: {
281+
path: 'src/planner.ts',
282+
callees: [{ name: 'fetchIssue', file: 'src/github.ts', line: 15 }],
283+
},
284+
},
285+
{
286+
id: '2',
287+
score: 0.9,
288+
metadata: {
289+
path: 'src/github.ts',
290+
callees: [{ name: 'httpGet', file: 'src/api.ts', line: 5 }],
291+
},
292+
},
293+
{
294+
id: '3',
295+
score: 0.9,
296+
metadata: {
297+
path: 'src/api.ts',
298+
callees: [],
299+
},
300+
},
301+
];
302+
303+
beforeEach(async () => {
304+
const mockIndexer = {
305+
getAll: vi.fn().mockResolvedValue(chainDocs),
306+
};
307+
308+
adapterWithIndexer = new RefsAdapter({
309+
searchService: mockSearchService,
310+
indexer: mockIndexer as unknown as import('@prosdevlab/dev-agent-core').RepositoryIndexer,
311+
defaultLimit: 20,
312+
});
313+
314+
await adapterWithIndexer.initialize(context);
315+
});
316+
317+
it('should trace dependency path between files', async () => {
318+
const result = await adapterWithIndexer.execute(
319+
{ name: 'createPlan', dependsOn: 'src/api.ts' },
320+
execContext
321+
);
322+
323+
expect(result.success).toBe(true);
324+
expect(result.data).toContain('Dependency Path');
325+
expect(result.data).toContain('src/planner.ts');
326+
expect(result.data).toContain('src/api.ts');
327+
expect(result.data).toContain('→');
328+
});
329+
330+
it('should report hop count', async () => {
331+
const result = await adapterWithIndexer.execute(
332+
{ name: 'createPlan', dependsOn: 'src/api.ts' },
333+
execContext
334+
);
335+
336+
expect(result.success).toBe(true);
337+
expect(result.data).toContain('2 hops');
338+
});
339+
340+
it('should report when no path exists', async () => {
341+
const result = await adapterWithIndexer.execute(
342+
{ name: 'createPlan', dependsOn: 'src/nonexistent.ts' },
343+
execContext
344+
);
345+
346+
expect(result.success).toBe(true);
347+
expect(result.data).toContain('No Path Found');
348+
expect(result.data).toContain('separate subsystems');
349+
});
350+
351+
it('should return error when indexer is not available', async () => {
352+
// The base adapter (no indexer) should fail for dependsOn
353+
const result = await adapter.execute(
354+
{ name: 'createPlan', dependsOn: 'src/api.ts' },
355+
execContext
356+
);
357+
358+
expect(result.success).toBe(false);
359+
expect(result.error?.code).toBe('INDEX_REQUIRED');
360+
});
361+
});
362+
271363
describe('Not Found', () => {
272364
it('should return error when function not found', async () => {
273365
// Mock empty results

0 commit comments

Comments
 (0)