diff --git a/gitnexus/src/core/lbug/lbug-adapter.ts b/gitnexus/src/core/lbug/lbug-adapter.ts index 51af63b03..37cbd1bc6 100644 --- a/gitnexus/src/core/lbug/lbug-adapter.ts +++ b/gitnexus/src/core/lbug/lbug-adapter.ts @@ -2904,7 +2904,26 @@ export const queryFTS = async ( }; /** - * Drop an FTS index + * True for the two benign "nothing to drop" `DROP_FTS_INDEX` failures — + * both catalog/binder exceptions, LadybugDB's classes for "this name isn't + * bound to anything right now" (probe-verified end-to-end through + * `dropFTSIndex`'s real `conn.query()` path against @ladybugdb/core + * 0.18.x): the named index was never created (`Binder exception: Table + * doesn't have an index with name .`), or the FTS extension/function + * isn't registered at all (`Catalog exception: function DROP_FTS_INDEX is + * not defined...`). A real engine failure — e.g. the `Runtime exception: + * FTS index '' is inconsistent: ...` class from #2589 — is a + * DIFFERENT exception class (an execution-time failure, not a catalog/bind + * lookup miss), so this returns false for it. Pure string logic so it is + * unit-testable without a native LadybugDB connection. + */ +export const isBenignDropFtsIndexError = (message: string): boolean => + message.includes('Binder exception:') || message.includes('Catalog exception:'); + +/** + * Drop an FTS index. Tolerates only {@link isBenignDropFtsIndexError} — + * anything else rethrows instead of being silently masked, which previously + * let a corrupted index persist across analyze runs undetected. */ export const dropFTSIndex = async (tableName: string, indexName: string): Promise => { if (!conn) { @@ -2913,8 +2932,11 @@ export const dropFTSIndex = async (tableName: string, indexName: string): Promis try { await queryAndDrain(conn, `CALL DROP_FTS_INDEX('${tableName}', '${indexName}')`); - } catch { - // Index may not exist + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if (!isBenignDropFtsIndexError(msg)) { + throw e; + } } finally { ensuredFTSIndexes.delete(ftsIndexKey(tableName, indexName)); } diff --git a/gitnexus/test/unit/drop-fts-index-error-classification.test.ts b/gitnexus/test/unit/drop-fts-index-error-classification.test.ts new file mode 100644 index 000000000..54d6d8597 --- /dev/null +++ b/gitnexus/test/unit/drop-fts-index-error-classification.test.ts @@ -0,0 +1,58 @@ +/** + * #2589: `dropFTSIndex` must tolerate only benign "nothing to drop" + * `DROP_FTS_INDEX` failures and rethrow everything else — previously it + * swallowed every error unconditionally, which could mask a genuinely + * corrupted FTS index across analyze runs. + * + * `isBenignDropFtsIndexError` is pure string logic (no native connection + * needed), so the classification itself is unit-tested directly, including + * against the exact reported #2589 error text — a native repro of that + * specific engine failure was not achieved during investigation, but the + * classifier's behavior for it is still provable from the message alone. + */ +import { describe, expect, it } from 'vitest'; +import { isBenignDropFtsIndexError, dropFTSIndex } from '../../src/core/lbug/lbug-adapter.js'; +import { withTestLbugDB } from '../helpers/test-indexed-db.js'; + +describe('isBenignDropFtsIndexError', () => { + it('is true for the FTS-extension/function-not-registered catalog error (probe-verified text)', () => { + expect( + isBenignDropFtsIndexError( + "Catalog exception: function DROP_FTS_INDEX is not defined. This function exists in the FTS extension. You can install and load the extension by running 'INSTALL FTS; LOAD EXTENSION FTS;'.", + ), + ).toBe(true); + }); + + it('is true for the index-never-created binder error (probe-verified against the real dropFTSIndex path)', () => { + expect( + isBenignDropFtsIndexError( + "Binder exception: Table File doesn't have an index with name file_fts.", + ), + ).toBe(true); + }); + + it('is false for the #2589 runtime inconsistency error (must surface, not be swallowed)', () => { + expect( + isBenignDropFtsIndexError( + "Runtime exception: FTS index 'file_fts' is inconsistent: term 'wiki' is missing during delete.", + ), + ).toBe(false); + }); + + it('is false for an unrelated failure', () => { + expect(isBenignDropFtsIndexError('Connection Exception: database is closed')).toBe(false); + }); +}); + +withTestLbugDB('drop-fts-index-benign-cases', (handle) => { + describe('dropFTSIndex end-to-end benign cases (#2589)', () => { + it('resolves cleanly when the named index was never created', async () => { + void handle; + const { executeQuery } = await import('../../src/core/lbug/lbug-adapter.js'); + await executeQuery( + `CREATE NODE TABLE IF NOT EXISTS DropProbe (id STRING PRIMARY KEY, content STRING)`, + ); + await expect(dropFTSIndex('DropProbe', 'drop_probe_never_created')).resolves.toBeUndefined(); + }, 120_000); + }); +});