import { describe, it, expect, vi, beforeEach } from 'vitest'; /** * Unit coverage for probeFtsExtensionLoad (#2374, PR #2375): the doctor FTS * probe's outcomes without the real native module or network. @ladybugdb/core * is mocked so query behavior (resolve / reject / never-settle) is controlled * per test, and the timeout is exercised with a tiny injected budget. */ const h = vi.hoisted(() => ({ query: vi.fn<(sql: string) => Promise>(), connCtor: vi.fn<() => void>(), connClose: vi.fn<() => Promise>(async () => undefined), dbClose: vi.fn<() => Promise>(async () => undefined), })); vi.mock('@ladybugdb/core', () => { class Database { constructor(_path: string) {} close = h.dbClose; } class Connection { constructor(_db: unknown) { h.connCtor(); } query = h.query; close = h.connClose; } return { default: { Database, Connection } }; }); import { probeFtsExtensionLoad, probeVectorExtensionLoad, } from '../../src/core/lbug/native-check.js'; const closeable = () => ({ close: vi.fn() }); beforeEach(() => { h.query.mockReset(); h.connCtor.mockReset(); h.connClose.mockClear(); h.dbClose.mockClear(); }); describe('probeFtsExtensionLoad (#2374)', () => { it('reports loaded and closes every result when LOAD succeeds (array result)', async () => { const results = [closeable(), closeable()]; h.query.mockResolvedValue(results); await expect(probeFtsExtensionLoad()).resolves.toEqual({ loaded: true }); expect(results.map((r) => r.close.mock.calls.length)).toEqual([1, 1]); expect(h.connClose).toHaveBeenCalled(); expect(h.dbClose).toHaveBeenCalled(); }); it('reports loaded for a single (non-array) result', async () => { h.query.mockResolvedValue(closeable()); await expect(probeFtsExtensionLoad()).resolves.toEqual({ loaded: true }); }); it('reports the collapsed reason when LOAD fails', async () => { h.query.mockRejectedValue(new Error('IO exception:\n invalid ELF header')); await expect(probeFtsExtensionLoad()).resolves.toMatchObject({ loaded: false, reason: 'IO exception: invalid ELF header', }); }); it('times out instead of hanging when the native call never settles', async () => { h.query.mockReturnValue(new Promise(() => undefined)); await expect(probeFtsExtensionLoad(20)).resolves.toMatchObject({ loaded: false, reason: expect.stringContaining('timed out'), }); }); it('still closes the db when the Connection ctor throws', async () => { h.connCtor.mockImplementation(() => { throw new Error('connection ctor failed'); }); await expect(probeFtsExtensionLoad()).resolves.toMatchObject({ loaded: false }); expect(h.dbClose).toHaveBeenCalled(); }); it('reports loaded even when a result close() throws', async () => { h.query.mockResolvedValue({ close: () => { throw new Error('close boom'); }, }); await expect(probeFtsExtensionLoad()).resolves.toEqual({ loaded: true }); }); }); describe('probeVectorExtensionLoad (#2623 follow-up)', () => { it('issues LOAD EXTENSION vector and reports loaded on success — no platform short-circuit', async () => { h.query.mockResolvedValue(closeable()); await expect(probeVectorExtensionLoad()).resolves.toEqual({ loaded: true }); // The probe must really attempt the LOAD (the old code refused Windows // before ever touching the engine; the artifact ships for win_amd64 too). expect(h.query).toHaveBeenCalledWith('LOAD EXTENSION vector'); }); it('reports the collapsed reason when LOAD fails', async () => { h.query.mockRejectedValue(new Error('IO exception:\n extension file not found')); await expect(probeVectorExtensionLoad()).resolves.toMatchObject({ loaded: false, reason: 'IO exception: extension file not found', }); }); it('times out instead of hanging when the native call never settles', async () => { h.query.mockReturnValue(new Promise(() => undefined)); await expect(probeVectorExtensionLoad(20)).resolves.toMatchObject({ loaded: false, reason: expect.stringContaining('timed out'), }); }); });