From ed7206855538d056330247e66812d48ca1ff0840 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 21 Jun 2026 06:08:29 +0000 Subject: [PATCH] fix(lbug): drain DELETE results in the deleteAll* helpers (#2264) deleteAllInterprocTaintPaths, deleteAllCallSummaries, and deleteAllCommunitiesAndProcesses awaited conn.query(...DELETE...) but dropped the returned QueryResult (only the count result was closed), leaking a native result handle and violating the helpers' own "query + drain inside the lock" contract. Close each delete result via closeQueryResults, matching the count handling. Adds a seeded drain test (closeQueryResults fires for the DELETE result). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JBJomjoTdBV2eveDVq4JMm --- gitnexus/src/core/lbug/lbug-adapter.ts | 10 +++-- .../lbug-conn-serialization.test.ts | 39 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/gitnexus/src/core/lbug/lbug-adapter.ts b/gitnexus/src/core/lbug/lbug-adapter.ts index abe2ed896..1a8ba18b2 100644 --- a/gitnexus/src/core/lbug/lbug-adapter.ts +++ b/gitnexus/src/core/lbug/lbug-adapter.ts @@ -2067,7 +2067,7 @@ export const deleteAllCommunitiesAndProcesses = async (): Promise<{ const rows = await result.getAll(); const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0); if (count > 0) { - await c.query(`MATCH (n:${label}) DETACH DELETE n`); + await closeQueryResults(await c.query(`MATCH (n:${label}) DETACH DELETE n`)); nodesDeleted += count; } } catch { @@ -2113,7 +2113,9 @@ export const deleteAllInterprocTaintPaths = async (): Promise<{ edgesDeleted: nu const rows = await result.getAll(); const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0); if (count > 0) { - await c.query(`MATCH ()-[r:CodeRelation]->() WHERE r.type = 'TAINT_PATH' DELETE r`); + await closeQueryResults( + await c.query(`MATCH ()-[r:CodeRelation]->() WHERE r.type = 'TAINT_PATH' DELETE r`), + ); edgesDeleted = count; } } catch (err) { @@ -2170,7 +2172,9 @@ export const deleteAllCallSummaries = async (): Promise<{ edgesDeleted: number } const rows = await result.getAll(); const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0); if (count > 0) { - await c.query(`MATCH ()-[r:CodeRelation]->() WHERE r.type = 'CALL_SUMMARY' DELETE r`); + await closeQueryResults( + await c.query(`MATCH ()-[r:CodeRelation]->() WHERE r.type = 'CALL_SUMMARY' DELETE r`), + ); edgesDeleted = count; } } catch (err) { diff --git a/gitnexus/test/integration/lbug-conn-serialization.test.ts b/gitnexus/test/integration/lbug-conn-serialization.test.ts index 726dee653..36951affe 100644 --- a/gitnexus/test/integration/lbug-conn-serialization.test.ts +++ b/gitnexus/test/integration/lbug-conn-serialization.test.ts @@ -27,6 +27,18 @@ vi.mock('../../src/core/lbug/conn-lock.js', async (importOriginal) => { import { withConnLock } from '../../src/core/lbug/conn-lock.js'; const lockSpy = vi.mocked(withConnLock); +// Spy `closeQueryResults` (call-through) to prove the deleteAll* helpers now +// drain/close their DELETE result, not just the count result (P2 #2264). +vi.mock('../../src/core/lbug/query-result-utils.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + closeQueryResults: vi.fn(actual.closeQueryResults) as typeof actual.closeQueryResults, + }; +}); +import { closeQueryResults } from '../../src/core/lbug/query-result-utils.js'; +const closeSpy = vi.mocked(closeQueryResults); + withTestLbugDB('conn-serialization', () => { describe('singleton-conn helpers acquire withConnLock (P1 #2264)', () => { beforeEach(() => { @@ -62,3 +74,30 @@ withTestLbugDB('conn-serialization', () => { }); }); }); + +withTestLbugDB( + 'conn-serialization-drain', + () => { + describe('deleteAll* drain their DELETE result (P2 #2264)', () => { + beforeEach(() => { + closeSpy.mockClear(); + }); + + it('U4: deleteAllCommunitiesAndProcesses closes the DETACH DELETE result', async () => { + const { deleteAllCommunitiesAndProcesses } = + await import('../../src/core/lbug/lbug-adapter.js'); + const result = await deleteAllCommunitiesAndProcesses(); + // Seed has 1 Community, 0 Process. With the drain fix, closeQueryResults + // fires for: Community count, Community DETACH DELETE, Process count = 3. + // Without the fix the delete result is dropped → only 2 closes. + expect(result).toMatchObject({ nodesDeleted: 1 }); + expect(closeSpy.mock.calls.length).toBeGreaterThanOrEqual(3); + }); + }); + }, + { + seed: [ + "CREATE (c:Community {id: 'comm:drain', label: 'Drain', heuristicLabel: 'Drain', keywords: ['x'], description: 'd', enrichedBy: 'heuristic', cohesion: 0.5, symbolCount: 1})", + ], + }, +);