From 06fb9c0a6bf9efae5a70a41389159152d3674e3d Mon Sep 17 00:00:00 2001 From: perfectra1n Date: Fri, 20 Mar 2026 11:43:23 -0700 Subject: [PATCH] test(search): add FTS5 integration test --- apps/server/spec/fts5_search.spec.ts | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 apps/server/spec/fts5_search.spec.ts diff --git a/apps/server/spec/fts5_search.spec.ts b/apps/server/spec/fts5_search.spec.ts new file mode 100644 index 0000000000..7d6ed7483a --- /dev/null +++ b/apps/server/spec/fts5_search.spec.ts @@ -0,0 +1,76 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import config from "../src/services/config.js"; + +let app: Application; + +function timed(fn: () => T): [T, number] { + const start = performance.now(); + const result = fn(); + return [result, performance.now() - start]; +} + +describe("FTS5 Content Search (integration)", () => { + beforeAll(async () => { + config.General.noAuthentication = true; + const buildApp = (await import("../src/app.js")).default; + app = await buildApp(); + }); + + it("FTS5 index builds and searches correctly", async () => { + const sql = (await import("../src/services/sql.js")).default; + const becca = (await import("../src/becca/becca.js")).default; + const ftsIndex = (await import("../src/services/search/fts_index.js")).default; + const cls = (await import("../src/services/cls.js")).default; + + await new Promise((resolve) => { + cls.init(() => { + // Check if FTS table exists (migration may not have run on test DB) + const tableExists = sql.getValue( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='note_content_fts'" + ); + + if (!tableExists) { + // Create the table for testing + sql.execute(` + CREATE VIRTUAL TABLE IF NOT EXISTS note_content_fts USING fts5( + noteId UNINDEXED, + content, + tokenize='unicode61 remove_diacritics 2' + ) + `); + } + + const noteCount = Object.keys(becca.notes).length; + console.log(`\n Notes in becca: ${noteCount}`); + + // Build the index + ftsIndex.resetIndex(); + const [, buildMs] = timed(() => ftsIndex.buildIndex()); + console.log(` FTS index build: ${buildMs.toFixed(0)}ms`); + + // Verify index has content + const indexedCount = sql.getValue("SELECT COUNT(*) FROM note_content_fts"); + console.log(` Notes indexed: ${indexedCount}`); + expect(indexedCount).toBeGreaterThanOrEqual(0); + + // If we have indexed content, test search + if (indexedCount > 0) { + const [results, searchMs] = timed(() => ftsIndex.searchContent(["note"], "*=*")); + console.log(` FTS search "note": ${searchMs.toFixed(1)}ms (${results.length} results)`); + expect(results).toBeInstanceOf(Array); + } + + // Test update and remove don't throw + expect(() => ftsIndex.updateNote("nonexistent")).not.toThrow(); + expect(() => ftsIndex.removeNote("nonexistent")).not.toThrow(); + + // Clean up + sql.execute("DELETE FROM note_content_fts"); + ftsIndex.resetIndex(); + + resolve(); + }); + }); + }); +});