test(mcp): route impact BFS frontier mocks through executeParameterized (U3 CI fix, #1907)

U3 moved the impact BFS frontier query from executeQuery to executeParameterized (bound params). Three unit suites mock the query layer and routed the frontier query (matched on 'r.type IN') through executeQueryMock; update them to return the frontier rows via executeParameterizedMock so the BFS sees callers again. Test-only — no production change. Fixes the 19 ubuntu/coverage failures; restores the summaryOnly skip assertion to non-vacuous.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-05-30 09:41:34 +00:00
parent eeafab80f2
commit 71a4e3a4f0
3 changed files with 133 additions and 95 deletions

View file

@ -649,19 +649,26 @@ describe('LocalBackend.callTool', () => {
it('impact byDepth items include a processes field (default empty when no processes)', async () => {
// Resolver returns target; BFS returns one frontier caller; no STEP_IN_PROCESS rows.
(executeParameterized as any).mockResolvedValue([
{ id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
]);
(executeQuery as any).mockResolvedValue([
{
id: 'func:caller',
name: 'caller',
type: 'Function',
filePath: 'src/uses-main.ts',
relType: 'CALLS',
confidence: 0.9,
},
]);
(executeParameterized as any).mockImplementation((_repoId: string, cypher: string) => {
// BFS frontier query is now parameterized (#1907 U3).
if (cypher.includes('r.type IN') && !cypher.includes('STEP_IN_PROCESS')) {
return Promise.resolve([
{
id: 'func:caller',
name: 'caller',
type: 'Function',
filePath: 'src/uses-main.ts',
relType: 'CALLS',
confidence: 0.9,
},
]);
}
// Symbol resolution.
return Promise.resolve([
{ id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
]);
});
(executeQuery as any).mockResolvedValue([]);
const result = await backend.callTool('impact', { target: 'main', direction: 'upstream' });
const d1 = result.byDepth?.[1] || result.byDepth?.['1'] || [];
@ -674,6 +681,19 @@ describe('LocalBackend.callTool', () => {
it('impact populates byDepth processes when STEP_IN_PROCESS rows exist', async () => {
(executeParameterized as any).mockImplementation((_repoId: string, cypher: string) => {
// BFS frontier query is now parameterized (#1907 U3).
if (cypher.includes('r.type IN') && !cypher.includes('STEP_IN_PROCESS')) {
return Promise.resolve([
{
id: 'func:caller',
name: 'caller',
type: 'Function',
filePath: 'src/uses-main.ts',
relType: 'CALLS',
confidence: 0.9,
},
]);
}
// Symbol resolver name-lookup
if (cypher.includes('WHERE n.name =')) {
return Promise.resolve([
@ -739,6 +759,20 @@ describe('LocalBackend.callTool', () => {
it('impact summaryOnly:true skips the per-symbol STEP_IN_PROCESS enrichment pass', async () => {
// Resolver returns target; BFS returns one caller; aggregation returns one process row.
(executeParameterized as any).mockImplementation((_repoId: string, cypher: string) => {
// BFS frontier query is now parameterized (#1907 U3) — return a caller so
// the per-symbol-skip assertion below is meaningful (not vacuous).
if (cypher.includes('r.type IN') && !cypher.includes('STEP_IN_PROCESS')) {
return Promise.resolve([
{
id: 'func:caller',
name: 'caller',
type: 'Function',
filePath: 'src/a.ts',
relType: 'CALLS',
confidence: 0.9,
},
]);
}
if (cypher.includes('WHERE n.name =')) {
return Promise.resolve([
{ id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
@ -811,6 +845,19 @@ describe('LocalBackend.callTool', () => {
await backend.init();
(executeParameterized as any).mockImplementation((_repoId: string, cypher: string) => {
// BFS frontier query is now parameterized (#1907 U3).
if (cypher.includes('r.type IN') && !cypher.includes('STEP_IN_PROCESS')) {
return Promise.resolve([
{
id: 'func:caller',
name: 'caller',
type: 'Function',
filePath: 'src/uses-main.ts',
relType: 'CALLS',
confidence: 0.9,
},
]);
}
// UID resolver
if (cypher.includes('WHERE n.id = $uid')) {
return Promise.resolve([

View file

@ -68,30 +68,9 @@ describe('impact: batching and grouping', () => {
const chunkSizes: number[] = [];
let chunkCallIndex = 0;
executeQueryMock.mockImplementation(async (...args: any[]) => {
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
// Depth traversal query (find related nodes) -- return 250 impacted ids
if (query.includes('r.type IN') && !query.includes('STEP_IN_PROCESS')) {
const res: any[] = [];
for (let i = 0; i < 250; i++) {
res.push({
id: `node-${i}`,
name: `n${i}`,
filePath: `file-${i}.js`,
relType: 'CALLS',
confidence: null,
});
}
return res;
}
// NOTE: process-chunk enrichment previously used executeQuery; our
// implementation now calls executeParameterized for those chunks. We
// still keep this branch to support any legacy calls, but primary
// chunk tracking will be handled via executeParameterizedMock below.
return [];
});
// BFS frontier query is now parameterized (#1907 U3) — handled in
// executeParameterizedMock below; executeQuery is unused by the impact path.
executeQueryMock.mockImplementation(async () => []);
// Handle parameterized calls (including chunked STEP_IN_PROCESS queries)
executeParameterizedMock.mockImplementation(async (...args: any[]) => {
@ -117,6 +96,20 @@ describe('impact: batching and grouping', () => {
},
];
}
// BFS frontier query (parameterized #1907 U3): return the 250 impacted ids.
if (query.includes('r.type IN') && !query.includes('STEP_IN_PROCESS')) {
const res: any[] = [];
for (let i = 0; i < 250; i++) {
res.push({
id: `node-${i}`,
name: `n${i}`,
filePath: `file-${i}.js`,
relType: 'CALLS',
confidence: null,
});
}
return res;
}
// Default target resolution
return [{ id: 'sym1', name: 'Target', filePath: 'f' }];
});
@ -151,6 +144,19 @@ describe('impact: batching and grouping', () => {
executeParameterizedMock.mockImplementation(async (...args: any[]) => {
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
// BFS frontier query (parameterized #1907 U3): return 6 impacted nodes.
if (query.includes('r.type IN') && !query.includes('STEP_IN_PROCESS')) {
const res: any[] = [];
for (let i = 0; i < 6; i++)
res.push({
id: `node-${i}`,
name: `n${i}`,
filePath: `file-${i}.js`,
relType: 'CALLS',
confidence: null,
});
return res;
}
if (!query.includes('STEP_IN_PROCESS'))
return [{ id: 'symA', name: 'TargetA', filePath: 'f' }];
// For STEP_IN_PROCESS in this test, return grouping rows
@ -190,25 +196,9 @@ describe('impact: batching and grouping', () => {
];
});
// Prepare impacted nodes: smaller set for clarity (6 nodes -> chunk size default 100 so single chunk)
executeQueryMock.mockImplementation(async (...args: any[]) => {
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
if (query.includes('r.type IN') && !query.includes('STEP_IN_PROCESS')) {
// return 6 nodes
const res: any[] = [];
for (let i = 0; i < 6; i++)
res.push({
id: `node-${i}`,
name: `n${i}`,
filePath: `file-${i}.js`,
relType: 'CALLS',
confidence: null,
});
return res;
}
return [];
});
// BFS frontier query is now parameterized (#1907 U3) — handled in
// executeParameterizedMock above; executeQuery is unused by the impact path.
executeQueryMock.mockImplementation(async () => []);
const params = { target: 'TargetA', direction: 'downstream', maxDepth: 1 } as any;
const res = await (backend as any)._impactImpl(repoHandle, params);
@ -243,23 +233,9 @@ describe('impact: batching and grouping', () => {
(backend as any).repos.set(repoHandle.id, repoHandle);
(backend as any).ensureInitialized = vi.fn().mockResolvedValue(undefined);
// Depth traversal returns 500 impacted nodes
executeQueryMock.mockImplementation(async (...args: any[]) => {
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
if (query.includes('r.type IN') && !query.includes('STEP_IN_PROCESS')) {
const res: any[] = [];
for (let i = 0; i < 500; i++)
res.push({
id: `node-${i}`,
name: `n${i}`,
filePath: `file-${i}.js`,
relType: 'CALLS',
confidence: null,
});
return res;
}
return [];
});
// BFS frontier query is now parameterized (#1907 U3) — handled in
// executeParameterizedMock below; executeQuery is unused by the impact path.
executeQueryMock.mockImplementation(async () => []);
const chunkSizes: number[] = [];
@ -294,6 +270,19 @@ describe('impact: batching and grouping', () => {
return [{ name: 'ModuleA' }];
}
// BFS frontier query (parameterized #1907 U3): return 500 impacted nodes.
if (query.includes('r.type IN') && !query.includes('STEP_IN_PROCESS')) {
const res: any[] = [];
for (let i = 0; i < 500; i++)
res.push({
id: `node-${i}`,
name: `n${i}`,
filePath: `file-${i}.js`,
relType: 'CALLS',
confidence: null,
});
return res;
}
// Default: target resolution
return [{ id: 'symX', name: 'TargetX', filePath: 'f' }];
});

View file

@ -46,30 +46,35 @@ function makeBackend() {
return { backend, repoHandle };
}
// The BFS frontier query is now parameterized (bound $frontierIds/$relTypes,
// #1907 U3), so the caller rows come back through executeParameterizedMock
// (matched on `r.type IN`) rather than executeQueryMock. Symbol resolution and
// the label-enrichment UNION still fall through to the default symbol row.
function setupMultiDepthHub(d1Count: number, d2Count: number) {
let depth = 0;
executeParameterizedMock.mockImplementation(async (...args: any[]) => {
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
if (query.includes('STEP_IN_PROCESS')) return [];
if (query.includes('MEMBER_OF')) return [];
if (query.includes('r.type IN')) {
depth++;
const count = depth === 1 ? d1Count : depth === 2 ? d2Count : 0;
const res: any[] = [];
for (let i = 0; i < count; i++) {
res.push({
id: `d${depth}-caller-${i}`,
name: `d${depth}caller${i}`,
filePath: `src/d${depth}-caller-${i}.ts`,
relType: 'CALLS',
confidence: null,
});
}
return res;
}
return [{ id: 'hub1', name: 'HubSymbol', filePath: 'hub.ts' }];
});
executeQueryMock.mockImplementation(async () => {
depth++;
const count = depth === 1 ? d1Count : depth === 2 ? d2Count : 0;
const res: any[] = [];
for (let i = 0; i < count; i++) {
res.push({
id: `d${depth}-caller-${i}`,
name: `d${depth}caller${i}`,
filePath: `src/d${depth}-caller-${i}.ts`,
relType: 'CALLS',
confidence: null,
});
}
return res;
});
executeQueryMock.mockImplementation(async () => []);
}
function setupHubSymbol(count: number) {
@ -77,12 +82,7 @@ function setupHubSymbol(count: number) {
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
if (query.includes('STEP_IN_PROCESS')) return [];
if (query.includes('MEMBER_OF')) return [];
return [{ id: 'hub1', name: 'HubSymbol', filePath: 'hub.ts' }];
});
executeQueryMock.mockImplementation(async (...args: any[]) => {
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
if (query.includes('r.type IN') && !query.includes('STEP_IN_PROCESS')) {
if (query.includes('r.type IN')) {
const res: any[] = [];
for (let i = 0; i < count; i++) {
res.push({
@ -95,8 +95,10 @@ function setupHubSymbol(count: number) {
}
return res;
}
return [];
return [{ id: 'hub1', name: 'HubSymbol', filePath: 'hub.ts' }];
});
executeQueryMock.mockImplementation(async () => []);
}
describe('impact: pagination and summaryOnly (#414)', () => {