From 2bb1b0ac6aa99fb6a09d546cbeeab1f0e264313f Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 18 Jun 2026 16:12:13 +0000 Subject: [PATCH] test(impact): cover calleesOfBlocks error-swallow graceful fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the slice-callees query (RETURN b.callees) fails, calleesOfBlocks logs and returns an empty set, so the bridge is not built and inter-procedural reach falls back to callgraph-equal — no error surfaces and no partial proven/unproven label is produced. Add a dispatch test that throws on the callees query (via vi.mocked, strictly typed) and asserts no bridge is passed to the BFS and no error surfaces. Addresses PR #2227 tri-review finding (P1-testing). Co-Authored-By: Claude Opus 4.8 (1M context) --- gitnexus/test/unit/calltool-dispatch.test.ts | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/gitnexus/test/unit/calltool-dispatch.test.ts b/gitnexus/test/unit/calltool-dispatch.test.ts index 535f761fd..15969200f 100644 --- a/gitnexus/test/unit/calltool-dispatch.test.ts +++ b/gitnexus/test/unit/calltool-dispatch.test.ts @@ -1718,6 +1718,44 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { expect(betterBridgeEvidence(unproven, unproven).evidence).toBe('unproven-bridge'); }); + it("mode:'pdg' degrades gracefully when the slice-callees query fails (no bridge, no throw)", async () => { + // calleesOfBlocks swallows a DB error and returns an empty set, so the bridge + // is not built and the inter-procedural reach falls back to callgraph-equal — + // never surfacing the error or producing a partial proven/unproven labeling. + resolveSingleTarget(); + // The slice-callees query (RETURN b.callees) throws; every other query (target + // resolution) returns the resolved symbol row. + vi.mocked(executeParameterized).mockImplementation(async (_repo, query) => { + if (query.includes('RETURN b.callees')) throw new Error('slice-callees query failed'); + return [{ id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' }]; + }); + // A line-seeded downstream slice so calleesOfBlocks is attempted. + vi.spyOn(backend as any, '_runImpactPDG').mockResolvedValueOnce({ + mode: 'pdg', + target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' }, + direction: 'downstream', + risk: 'UNKNOWN', + impactedCount: 0, + epistemic: 'pdg-intra-procedural', + reachableBlocks: ['BasicBlock:src/index.ts:8:0:1'], + seedBlocks: ['BasicBlock:src/index.ts:8:0:0'], + blockCount: 1, + affectedStatements: [{ line: 8, filePath: 'src/index.ts', text: 'callee()' }], + affectedStatementCount: 1, + criterionLine: 8, + }); + const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const result = await backend.callTool('impact', { + target: 'main', + direction: 'downstream', + mode: 'pdg', + line: 8, + }); + // The error was swallowed: no bridge passed to the BFS, and no error surfaced. + expect(result.error).toBeUndefined(); + expect(bfsSpy.mock.calls[0][4].pdgBridge).toBeUndefined(); + }); + it("mode:'pdg' + crossDepth → hard {error} (single-repo PDG impact)", async () => { resolveSingleTarget(); const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');