GitNexus/gitnexus/test/integration/lbug-close-handle-release.test.ts
evolution 0daae93701
fix(lbug): drain checkpoint result before close (#1506)
* fix(lbug): drain checkpoint result before close

* test(lbug): cover checkpoint drain lifecycle

* fix(lbug): close query results after reads

* fix(lbug): close all stream query results

* fix(lbug): harden query result cleanup

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-12 14:03:45 +01:00

82 lines
3 KiB
TypeScript

/**
* Integration test: safeClose's Windows post-close handle-release wait.
*
* On Windows, libuv reports `db.close()` resolved before the kernel has
* released the file handle. A subsequent open of the same path can then
* race the release and surface "Could not set lock on file". `safeClose`
* probes the file with `fs.open` to force the residual lock to surface,
* absorbed by the open-time retry in `lbug-config.ts`.
*/
import path from 'path';
import { describe, expect, it } from 'vitest';
import { createTempDir } from '../helpers/test-db.js';
/**
* LadybugDB's native Windows file lock can outlive Database.close() for
* same-process close/reopen cycles. Keep true reopen coverage on POSIX and
* cover ordering deterministically in lbug-checkpoint-lifecycle.test.ts.
*/
const itLbugReopen = process.platform === 'win32' ? it.skip : it;
describe('safeClose — close + reopen does not surface lock errors', () => {
it('survives 10 sequential open/close/reopen cycles on the same path', async () => {
const tmp = await createTempDir('gitnexus-lbug-close-cycle-');
const dbPath = path.join(tmp.dbPath, 'lbug');
try {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
for (let i = 0; i < 10; i++) {
await adapter.initLbug(dbPath);
await adapter.closeLbug();
}
} finally {
await tmp.cleanup();
}
});
it('safeClose is idempotent — calling twice in a row does not throw', async () => {
const tmp = await createTempDir('gitnexus-lbug-idempotent-');
const dbPath = path.join(tmp.dbPath, 'lbug');
try {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
await adapter.initLbug(dbPath);
await adapter.closeLbug();
await adapter.closeLbug();
} finally {
await tmp.cleanup();
}
});
itLbugReopen('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();
}
});
});