import { afterEach, describe, expect, it, vi } from 'vitest'; const makeErrnoError = (code: TCode, message: string) => Object.assign(new Error(message), { code }); /** Stub file handle returned by mocked `fs.open` for the init lock. */ const makeOpenMock = () => vi.fn(async () => ({ writeFile: vi.fn(async () => {}), close: vi.fn(async () => {}), })); /** Mock prepared statement shape for executePrepared/prepare+execute paths. */ const makePreparedStatement = (sql: string) => ({ sql, isSuccess: () => true, getErrorMessage: () => '', }); /** Mock connection supporting both query() and prepare/execute() call paths. */ const makeConn = (runQuery: (sql: string) => Promise) => { const query = vi.fn(runQuery); return { query, prepare: vi.fn(async (sql: string) => makePreparedStatement(sql)), execute: vi.fn(async (statement: { sql: string }) => query(statement.sql)), close: vi.fn(async () => {}), }; }; /** Standard `fs/promises` mock for tests that only need doInitLbug to succeed. */ const mockFsForInit = (dbPath: string) => { const ENOENT_ERROR = makeErrnoError( 'ENOENT', `ENOENT: no such file or directory, lstat '${dbPath}'`, ); vi.doMock('fs/promises', () => ({ default: { lstat: vi.fn(async () => { throw ENOENT_ERROR; }), access: vi.fn(async () => { throw ENOENT_ERROR; }), unlink: vi.fn(async () => {}), mkdir: vi.fn(async () => {}), open: makeOpenMock(), readdir: vi.fn(async () => []), }, })); }; describe('lbug adapter CHECKPOINT lifecycle', () => { afterEach(() => { vi.doUnmock('fs/promises'); vi.doUnmock('../../src/core/lbug/lbug-config.js'); vi.doUnmock('../../src/core/lbug/extension-loader.js'); vi.doUnmock('../../src/core/logger.js'); vi.resetModules(); vi.clearAllMocks(); }); it('removes orphan sidecars when main DB file is missing before opening LadybugDB', async () => { vi.resetModules(); const dbPath = '/tmp/gitnexus-lbug-orphan-sidecar/lbug'; const ENOENT_ERROR = makeErrnoError( 'ENOENT', `ENOENT: no such file or directory, access '${dbPath}'`, ); const queryResult = { getAll: vi.fn(async () => []), close: vi.fn() }; const conn = makeConn(async () => queryResult); const db = { close: vi.fn(async () => {}) }; const unlinkMock = vi.fn(async () => {}); const accessMock = vi.fn(async () => { throw ENOENT_ERROR; }); vi.doMock('fs/promises', () => ({ default: { lstat: vi.fn(async () => { throw ENOENT_ERROR; }), access: accessMock, unlink: unlinkMock, mkdir: vi.fn(async () => {}), open: makeOpenMock(), readdir: vi.fn(async () => []), }, })); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const warnMock = vi.fn(); vi.doMock('../../src/core/logger.js', () => ({ logger: { warn: warnMock, info: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug(dbPath); expect(accessMock).toHaveBeenCalledWith(dbPath); // Unlink called for: .shadow sidecar, .wal.checkpoint sidecar, init lock release expect(unlinkMock).toHaveBeenCalledWith(`${dbPath}.shadow`); expect(unlinkMock).toHaveBeenCalledWith(`${dbPath}.wal.checkpoint`); expect(unlinkMock).toHaveBeenCalledWith(`${dbPath}.init.lock`); expect(warnMock).toHaveBeenCalledTimes(2); expect(warnMock).toHaveBeenCalledWith( 'GitNexus: removed orphan sidecar lbug.shadow (no main DB file present)', ); expect(warnMock).toHaveBeenCalledWith( 'GitNexus: removed orphan sidecar lbug.wal.checkpoint (no main DB file present)', ); await adapter.closeLbug(); }); it('skips orphan sidecar cleanup when db access fails with non-ENOENT errors', async () => { vi.resetModules(); const dbPath = '/tmp/gitnexus-lbug-orphan-sidecar-eacces/lbug'; const ENOENT_ERROR = makeErrnoError( 'ENOENT', `ENOENT: no such file or directory, access '${dbPath}'`, ); const EACCES_ERROR = makeErrnoError('EACCES', `EACCES: permission denied, access '${dbPath}'`); const queryResult = { getAll: vi.fn(async () => []), close: vi.fn() }; const conn = makeConn(async () => queryResult); const db = { close: vi.fn(async () => {}) }; const accessMock = vi.fn(async () => { throw EACCES_ERROR; }); const unlinkMock = vi.fn(async () => {}); vi.doMock('fs/promises', () => ({ default: { lstat: vi.fn(async () => { throw ENOENT_ERROR; }), access: accessMock, unlink: unlinkMock, mkdir: vi.fn(async () => {}), open: makeOpenMock(), readdir: vi.fn(async () => []), }, })); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const warnMock = vi.fn(); vi.doMock('../../src/core/logger.js', () => ({ logger: { warn: warnMock, info: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug(dbPath); expect(accessMock).toHaveBeenCalledWith(dbPath); // Only the init lock release calls unlink — sidecar cleanup was skipped expect(unlinkMock).toHaveBeenCalledTimes(1); expect(unlinkMock).toHaveBeenCalledWith(`${dbPath}.init.lock`); expect(warnMock).toHaveBeenCalledTimes(1); expect(warnMock.mock.calls[0]?.[0]).toContain( 'GitNexus: unable to verify main DB file before orphan sidecar cleanup (EACCES); skipping cleanup:', ); await adapter.closeLbug(); }); it('does not remove sidecars when main db file is present', async () => { vi.resetModules(); const dbPath = '/tmp/gitnexus-lbug-present/lbug'; const ENOENT_ERROR = makeErrnoError( 'ENOENT', `ENOENT: no such file or directory, access '${dbPath}'`, ); const queryResult = { getAll: vi.fn(async () => []), close: vi.fn() }; const conn = makeConn(async () => queryResult); const db = { close: vi.fn(async () => {}) }; const accessMock = vi.fn(async () => {}); const unlinkMock = vi.fn(async () => {}); vi.doMock('fs/promises', () => ({ default: { lstat: vi.fn(async () => { throw ENOENT_ERROR; }), access: accessMock, unlink: unlinkMock, mkdir: vi.fn(async () => {}), open: makeOpenMock(), readdir: vi.fn(async () => []), }, })); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const warnMock = vi.fn(); vi.doMock('../../src/core/logger.js', () => ({ logger: { warn: warnMock, info: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug(dbPath); expect(accessMock).toHaveBeenCalledWith(dbPath); // Only the init lock release calls unlink — no sidecar cleanup needed expect(unlinkMock).toHaveBeenCalledTimes(1); expect(unlinkMock).toHaveBeenCalledWith(`${dbPath}.init.lock`); expect(warnMock).not.toHaveBeenCalled(); await adapter.closeLbug(); }); it.each([ { code: 'EPERM', message: 'operation not permitted', dbPath: '/tmp/gitnexus-lbug-lstat-eperm/lbug', }, { code: 'EACCES', message: 'permission denied', dbPath: '/tmp/gitnexus-lbug-lstat-eacces/lbug', }, ])('throws when db path lstat fails with non-ENOENT %s', async ({ code, message, dbPath }) => { vi.resetModules(); const LSTAT_ERROR = makeErrnoError(code, `${code}: ${message}, lstat '${dbPath}'`); const accessMock = vi.fn(async () => {}); const unlinkMock = vi.fn(async () => {}); vi.doMock('fs/promises', () => ({ default: { lstat: vi.fn(async () => { throw LSTAT_ERROR; }), access: accessMock, unlink: unlinkMock, mkdir: vi.fn(async () => {}), open: makeOpenMock(), readdir: vi.fn(async () => []), }, })); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => { throw new Error('should not be called'); }), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); vi.doMock('../../src/core/logger.js', () => ({ logger: { warn: vi.fn(), info: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await expect(adapter.initLbug(dbPath)).rejects.toThrow(new RegExp(message, 'i')); expect(accessMock).not.toHaveBeenCalled(); expect(unlinkMock).not.toHaveBeenCalled(); }); it('handles partial orphan sidecar state and removes only present sidecars', async () => { vi.resetModules(); const dbPath = '/tmp/gitnexus-lbug-partial-sidecar/lbug'; const ENOENT_ERROR = makeErrnoError( 'ENOENT', `ENOENT: no such file or directory, access '${dbPath}'`, ); const queryResult = { getAll: vi.fn(async () => []), close: vi.fn() }; const conn = makeConn(async () => queryResult); const db = { close: vi.fn(async () => {}) }; const accessMock = vi.fn(async () => { throw ENOENT_ERROR; }); const unlinkMock = vi.fn(async (target: string) => { if (target.endsWith('.shadow')) throw ENOENT_ERROR; }); vi.doMock('fs/promises', () => ({ default: { lstat: vi.fn(async () => { throw ENOENT_ERROR; }), access: accessMock, unlink: unlinkMock, mkdir: vi.fn(async () => {}), open: makeOpenMock(), readdir: vi.fn(async () => []), }, })); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const warnMock = vi.fn(); vi.doMock('../../src/core/logger.js', () => ({ logger: { warn: warnMock, info: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug(dbPath); expect(unlinkMock).toHaveBeenCalledWith(`${dbPath}.shadow`); expect(unlinkMock).toHaveBeenCalledWith(`${dbPath}.wal.checkpoint`); expect(warnMock).toHaveBeenCalledTimes(1); expect(warnMock).toHaveBeenCalledWith( 'GitNexus: removed orphan sidecar lbug.wal.checkpoint (no main DB file present)', ); await adapter.closeLbug(); }); it('proceeds to openLbugConnection when orphan sidecar unlink fails', async () => { vi.resetModules(); const dbPath = '/tmp/gitnexus-lbug-sidecar-unlink-fail/lbug'; const ENOENT_ERROR = makeErrnoError( 'ENOENT', `ENOENT: no such file or directory, access '${dbPath}'`, ); const EPERM_ERROR = makeErrnoError( 'EPERM', `EPERM: operation not permitted, unlink '${dbPath}.shadow'`, ); const queryResult = { getAll: vi.fn(async () => []), close: vi.fn() }; const conn = makeConn(async () => queryResult); const db = { close: vi.fn(async () => {}) }; const accessMock = vi.fn(async () => { throw ENOENT_ERROR; }); const unlinkMock = vi.fn(async () => { throw EPERM_ERROR; }); vi.doMock('fs/promises', () => ({ default: { lstat: vi.fn(async () => { throw ENOENT_ERROR; }), access: accessMock, unlink: unlinkMock, mkdir: vi.fn(async () => {}), open: makeOpenMock(), readdir: vi.fn(async () => []), }, })); const openLbugConnectionMock = vi.fn(async () => ({ db, conn })); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: openLbugConnectionMock, closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const warnMock = vi.fn(); vi.doMock('../../src/core/logger.js', () => ({ logger: { warn: warnMock, info: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug(dbPath); expect(unlinkMock).toHaveBeenCalledTimes(3); expect(warnMock).toHaveBeenCalledTimes(3); expect(warnMock.mock.calls[0]?.[0]).toContain( 'GitNexus: failed to remove orphan sidecar lbug.shadow (EPERM) while main DB file is missing; LadybugDB open may still fail:', ); expect(warnMock.mock.calls[1]?.[0]).toContain( 'GitNexus: failed to remove orphan sidecar lbug.wal.checkpoint (EPERM) while main DB file is missing; LadybugDB open may still fail:', ); expect(warnMock.mock.calls[2]?.[0]).toContain('GitNexus: failed to release init lock (EPERM)'); expect(openLbugConnectionMock).toHaveBeenCalledWith(expect.anything(), dbPath); await adapter.closeLbug(); }); it('drains and closes CHECKPOINT result before closing connection and database handles', async () => { vi.resetModules(); const events: string[] = []; const checkpointResult = { getAll: vi.fn(async () => { events.push('checkpoint:getAll'); return []; }), close: vi.fn(() => { events.push('checkpoint:close'); }), }; const genericResult = { getAll: vi.fn(async () => []), close: vi.fn(), }; const conn = makeConn(async (sql: string) => { if (sql === 'CHECKPOINT') { events.push('checkpoint:query'); return checkpointResult; } return genericResult; }); conn.close = vi.fn(async () => { events.push('conn:close'); }); const db = { close: vi.fn(async () => { events.push('db:close'); }), }; mockFsForInit('/tmp/gitnexus-lbug-checkpoint-lifecycle/lbug'); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug('/tmp/gitnexus-lbug-checkpoint-lifecycle/lbug'); events.length = 0; await adapter.closeLbug(); expect(events).toEqual([ 'checkpoint:query', 'checkpoint:getAll', 'checkpoint:close', 'conn:close', 'db:close', ]); }); it('closes normal query results after reading rows', async () => { vi.resetModules(); const events: string[] = []; const queryResult = { getAll: vi.fn(async () => { events.push('query:getAll'); return [{ id: 'file:a' }]; }), close: vi.fn(() => { events.push('query:close'); }), }; const genericResult = { getAll: vi.fn(async () => []), close: vi.fn(), }; const conn = makeConn(async (sql: string) => { if (sql === 'MATCH (n:File) RETURN n.id AS id') { events.push('query:run'); return queryResult; } return genericResult; }); const db = { close: vi.fn(async () => {}), }; mockFsForInit('/tmp/gitnexus-lbug-query-lifecycle/lbug'); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug('/tmp/gitnexus-lbug-query-lifecycle/lbug'); events.length = 0; await expect(adapter.executeQuery('MATCH (n:File) RETURN n.id AS id')).resolves.toEqual([ { id: 'file:a' }, ]); expect(events).toEqual(['query:run', 'query:getAll', 'query:close']); await adapter.closeLbug(); }); it('treats synchronous query result close errors as best-effort cleanup', async () => { vi.resetModules(); const queryResult = { getAll: vi.fn(async () => [{ id: 'file:a' }]), close: vi.fn(() => { throw new Error('close failed'); }), }; const genericResult = { getAll: vi.fn(async () => []), close: vi.fn(), }; const conn = makeConn(async (sql: string) => { if (sql === 'MATCH (n:File) RETURN n.id AS id') { return queryResult; } return genericResult; }); const db = { close: vi.fn(async () => {}), }; mockFsForInit('/tmp/gitnexus-lbug-sync-close-lifecycle/lbug'); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug('/tmp/gitnexus-lbug-sync-close-lifecycle/lbug'); await expect(adapter.executeQuery('MATCH (n:File) RETURN n.id AS id')).resolves.toEqual([ { id: 'file:a' }, ]); expect(queryResult.close).toHaveBeenCalledOnce(); await adapter.closeLbug(); }); it('closes later query results when an earlier array result fails to read', async () => { vi.resetModules(); const events: string[] = []; const firstResult = { getAll: vi.fn(async () => { events.push('first:getAll'); throw new Error('read failed'); }), close: vi.fn(() => { events.push('first:close'); }), }; const secondResult = { getAll: vi.fn(async () => { events.push('second:getAll'); return []; }), close: vi.fn(() => { events.push('second:close'); }), }; const genericResult = { getAll: vi.fn(async () => []), close: vi.fn(), }; const conn = makeConn(async (sql: string) => { if (sql === 'MATCH (n:File) RETURN n.id AS id') { return [firstResult, secondResult]; } return genericResult; }); const db = { close: vi.fn(async () => {}), }; mockFsForInit('/tmp/gitnexus-lbug-array-error-lifecycle/lbug'); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug('/tmp/gitnexus-lbug-array-error-lifecycle/lbug'); await expect(adapter.executeQuery('MATCH (n:File) RETURN n.id AS id')).rejects.toThrow( 'read failed', ); expect(events).toEqual(['first:getAll', 'first:close', 'second:getAll', 'second:close']); await adapter.closeLbug(); }); it('closes non-first stream query results when LadybugDB returns an array', async () => { vi.resetModules(); const events: string[] = []; const firstResult = { hasNext: vi .fn() .mockImplementationOnce(() => { events.push('first:hasNext:true'); return true; }) .mockImplementationOnce(() => { events.push('first:hasNext:false'); return false; }), getNext: vi.fn(async () => { events.push('first:getNext'); return { id: 'file:a' }; }), getAll: vi.fn(async () => { events.push('first:getAll'); return []; }), close: vi.fn(() => { events.push('first:close'); }), }; const secondResult = { getAll: vi.fn(async () => { events.push('second:getAll'); return []; }), close: vi.fn(() => { events.push('second:close'); }), }; const genericResult = { getAll: vi.fn(async () => []), close: vi.fn(), }; const conn = makeConn(async (sql: string) => { if (sql === 'MATCH (n:File) RETURN n.id AS id') { events.push('stream:query'); return [firstResult, secondResult]; } return genericResult; }); const db = { close: vi.fn(async () => {}), }; mockFsForInit('/tmp/gitnexus-lbug-stream-lifecycle/lbug'); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug('/tmp/gitnexus-lbug-stream-lifecycle/lbug'); const rows: unknown[] = []; events.length = 0; await expect( adapter.streamQuery('MATCH (n:File) RETURN n.id AS id', (row) => { rows.push(row); }), ).resolves.toBe(1); expect(rows).toEqual([{ id: 'file:a' }]); expect(events).toEqual([ 'stream:query', 'first:hasNext:true', 'first:getNext', 'first:hasNext:false', 'first:getAll', 'first:close', 'second:getAll', 'second:close', ]); await adapter.closeLbug(); }); it('drains stream query results when row handling fails before the result is exhausted', async () => { vi.resetModules(); const events: string[] = []; const queryResult = { hasNext: vi.fn(() => { events.push('stream:hasNext'); return true; }), getNext: vi.fn(async () => { events.push('stream:getNext'); return { id: 'file:a' }; }), getAll: vi.fn(async () => { events.push('stream:getAll'); return [{ id: 'file:b' }]; }), close: vi.fn(() => { events.push('stream:close'); }), }; const genericResult = { getAll: vi.fn(async () => []), close: vi.fn(), }; const conn = makeConn(async (sql: string) => { if (sql === 'MATCH (n:File) RETURN n.id AS id') { events.push('stream:query'); return queryResult; } return genericResult; }); const db = { close: vi.fn(async () => {}), }; mockFsForInit('/tmp/gitnexus-lbug-stream-error-lifecycle/lbug'); vi.doMock('../../src/core/lbug/lbug-config.js', () => ({ openLbugConnection: vi.fn(async () => ({ db, conn })), closeLbugConnection: vi.fn(async () => {}), isDbBusyError: vi.fn((err: unknown) => String(err).toLowerCase().includes('lock')), isOpenRetryExhausted: vi.fn(() => false), waitForWindowsHandleRelease: vi.fn(async () => true), isStorageVersionMismatchError: vi.fn(() => false), throwIfStorageVersionMismatch: vi.fn(), STORAGE_VERSION_MISMATCH_SUGGESTION: '', })); vi.doMock('../../src/core/lbug/extension-loader.js', () => ({ extensionManager: { ensure: vi.fn(async () => true), getCapabilities: vi.fn(() => []), reset: vi.fn(), }, })); const adapter = await import('../../src/core/lbug/lbug-adapter.js'); await adapter.initLbug('/tmp/gitnexus-lbug-stream-error-lifecycle/lbug'); await expect( adapter.streamQuery('MATCH (n:File) RETURN n.id AS id', () => { throw new Error('client disconnected'); }), ).rejects.toThrow('client disconnected'); expect(events).toEqual([ 'stream:query', 'stream:hasNext', 'stream:getNext', 'stream:getAll', 'stream:close', ]); await adapter.closeLbug(); }); });