fix(lbug): drain checkpoint result before close

This commit is contained in:
evolution 2026-05-11 22:19:00 +08:00
parent 622f98ade5
commit 32d7e5b640
3 changed files with 58 additions and 2 deletions

View file

@ -218,6 +218,19 @@ const runWithSessionLock = async <T>(operation: () => Promise<T>): Promise<T> =>
const normalizeCopyPath = (filePath: string): string => filePath.replace(/\\/g, '/');
const drainQueryResult = async (
queryResult: lbug.QueryResult | lbug.QueryResult[],
): Promise<void> => {
const results = Array.isArray(queryResult) ? queryResult : [queryResult];
for (const result of results) {
try {
await result.getAll();
} finally {
await Promise.resolve(result.close()).catch(() => {});
}
}
};
export const initLbug = async (dbPath: string) => {
return runWithSessionLock(() => ensureLbugInitialized(dbPath));
};
@ -1060,7 +1073,8 @@ export const fetchExistingEmbeddingHashes = async (
export const flushWAL = async (): Promise<void> => {
if (!conn) return;
try {
await conn.query('CHECKPOINT');
const checkpointResult = await conn.query('CHECKPOINT');
await drainQueryResult(checkpointResult);
} catch {
/* ignore — older LadybugDB or schemaless DB may not accept it */
}

View file

@ -8,7 +8,7 @@
* absorbed by the open-time retry in `lbug-config.ts`.
*/
import path from 'path';
import { describe, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { createTempDir } from '../helpers/test-db.js';
describe('safeClose — close + reopen does not surface lock errors', () => {
@ -38,4 +38,38 @@ describe('safeClose — close + reopen does not surface lock errors', () => {
await tmp.cleanup();
}
});
it('flushes WAL when switching between two database paths in one process', async () => {
const repoA = await createTempDir('gitnexus-lbug-switch-a-');
const repoB = await createTempDir('gitnexus-lbug-switch-b-');
const dbPathA = path.join(repoA.dbPath, 'lbug');
const dbPathB = path.join(repoB.dbPath, 'lbug');
try {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
await adapter.withLbugDb(dbPathA, async () => {
await adapter.executeQuery(
"CREATE (:File {id: 'file:a', name: 'a.ts', filePath: 'a.ts', content: 'repo a'})",
);
});
await adapter.withLbugDb(dbPathB, async () => {
await adapter.executeQuery(
"CREATE (:File {id: 'file:b', name: 'b.ts', filePath: 'b.ts', content: 'repo b'})",
);
});
const rows = await adapter.withLbugDb(dbPathA, async () =>
adapter.executeQuery("MATCH (n:File {id: 'file:a'}) RETURN n.filePath AS filePath"),
);
expect(rows).toEqual([{ filePath: 'a.ts' }]);
} finally {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
await adapter.closeLbug().catch(() => {});
await repoA.cleanup();
await repoB.cleanup();
}
});
});

View file

@ -58,6 +58,14 @@ describe('flushWAL / safeClose — consolidation guard (#1376)', () => {
expect(matches.length).toBe(1);
});
it('flushWAL drains and closes the CHECKPOINT result before returning', () => {
const flushBody = adapterSource.slice(
adapterSource.indexOf('export const flushWAL'),
adapterSource.indexOf('export const safeClose'),
);
expect(flushBody).toMatch(/await drainQueryResult\(checkpointResult\)/);
});
it('conn.close() only appears inside safeClose (with eslint-disable)', () => {
// Every conn.close() in the adapter must live inside safeClose, guarded
// by the eslint-disable comment. Count occurrences to catch leaks.