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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBJomjoTdBV2eveDVq4JMm
This commit is contained in:
Gergo Magyar 2026-06-21 06:08:29 +00:00
parent f5fb6fc384
commit ed72068555
2 changed files with 46 additions and 3 deletions

View file

@ -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) {

View file

@ -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<typeof import('../../src/core/lbug/query-result-utils.js')>();
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})",
],
},
);