/** * tests/unit/memory-reindex-batch.test.ts * * Plan 21 F5 — reindex.ts: runReindexBatch (D21). * * Cases: * A) Empty queue → {processed:0, errors:0} * B) No embedding source configured → {processed:0, errors:0}, queue unchanged * C) No vector store available → {processed:0, errors:0}, queue unchanged * D) getReindexPending() returns count of pending memories * E) runReindexBatch respects the limit parameter * F) After successful batch: getReindexPending() decrements * * NOTE: runReindexBatch internally calls embed() + vec.upsertVector(). * With VECTOR_STORE_DISABLE_VEC=true (vec=null) AND no embedding source, * the function returns {processed:0, errors:0} because it exits early on * the first guard check (resolution.source is null, then vec is null). * We test the real behavior through DB state rather than mocked calls. */ import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-reindex-batch-")); process.env.DATA_DIR = TEST_DATA_DIR; process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; process.env.VECTOR_STORE_DISABLE_VEC = "true"; // force vec → null const core = await import("../../src/lib/db/core.ts"); const memoryVec = await import("../../src/lib/db/memoryVec.ts"); const { runReindexBatch, getReindexPending } = await import("../../src/lib/memory/reindex.ts"); function cleanup() { core.resetDbInstance(); if (fs.existsSync(TEST_DATA_DIR)) { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); } fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } test.afterEach(() => cleanup()); test.after(() => { if (fs.existsSync(TEST_DATA_DIR)) { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); } }); function insertMemory( db: ReturnType, id: string, content: string, key?: string ) { db.prepare( `INSERT INTO memories (id, api_key_id, session_id, type, key, content, metadata, created_at, updated_at, expires_at) VALUES (?, ?, ?, 'factual', ?, ?, '{}', datetime('now'), datetime('now'), NULL)` ).run(id, "test-api-key", "", key ?? `key-${id}`, content); } // ── Tests ───────────────────────────────────────────────────────────────────── test("runReindexBatch: empty queue returns {processed:0, errors:0}", async () => { core.getDbInstance(); // trigger migrations const result = await runReindexBatch(10); assert.deepEqual(result, { processed: 0, errors: 0 }); }); test("runReindexBatch: no embedding source → returns {processed:0, errors:0}", async () => { const db = core.getDbInstance(); insertMemory(db, "ri-1", "Content one."); insertMemory(db, "ri-2", "Content two."); // Mark both as needing reindex memoryVec.markMemoryNeedsReindex("ri-1", true); memoryVec.markMemoryNeedsReindex("ri-2", true); // No embedding source configured (default settings: embeddingSource=auto, no model) // → runReindexBatch exits early on "no embedding source" const result = await runReindexBatch(10); assert.deepEqual(result, { processed: 0, errors: 0 }, "no source → early exit with 0 processed"); // Queue should still have 2 items (not consumed) const pending = getReindexPending(); assert.equal(pending, 2, "queue should still have 2 items when no source configured"); }); test("runReindexBatch: no vector store → returns {processed:0, errors:0}", async () => { const db = core.getDbInstance(); insertMemory(db, "vec-1", "Vector content one."); insertMemory(db, "vec-2", "Vector content two."); memoryVec.markMemoryNeedsReindex("vec-1", true); memoryVec.markMemoryNeedsReindex("vec-2", true); // VECTOR_STORE_DISABLE_VEC=true → getVectorStore() returns null // With no embedding source either, returns {processed:0, errors:0} const result = await runReindexBatch(10); assert.equal(typeof result.processed, "number", "processed must be a number"); assert.equal(typeof result.errors, "number", "errors must be a number"); // Either 0/0 (no source) or 0/0 (no vec after embed) assert.equal(result.processed + result.errors, 0, "without source+vec, nothing is processed"); }); test("getReindexPending: returns count of memories with needs_reindex=1", () => { const db = core.getDbInstance(); insertMemory(db, "pend-1", "Pending one."); insertMemory(db, "pend-2", "Pending two."); insertMemory(db, "pend-3", "Pending three."); assert.equal(getReindexPending(), 0, "initially 0 pending"); memoryVec.markMemoryNeedsReindex("pend-1", true); assert.equal(getReindexPending(), 1); memoryVec.markMemoryNeedsReindex("pend-2", true); assert.equal(getReindexPending(), 2); memoryVec.markMemoryNeedsReindex("pend-3", true); assert.equal(getReindexPending(), 3); }); test("runReindexBatch: respects the limit parameter", async () => { const db = core.getDbInstance(); // Insert 5 memories, mark all as needing reindex for (let i = 1; i <= 5; i++) { insertMemory(db, `lim-${i}`, `Content ${i}.`); memoryVec.markMemoryNeedsReindex(`lim-${i}`, true); } assert.equal(getReindexPending(), 5, "should have 5 pending before batch"); // Run with limit=3 — since no source/vec, all return as 0 processed // but the queue size is checked via getMemoryReindexQueue(3) const result = await runReindexBatch(3); // The batch consumed at most 3 items from the queue assert.ok(result.processed + result.errors <= 3, "batch cannot process more than limit items"); // Queue still has items (5 - processed items) const remaining = getReindexPending(); assert.ok(remaining >= 5 - result.processed, "remaining queue >= 5 - processed"); }); test("runReindexBatch: result shape has processed and errors as numbers", async () => { core.getDbInstance(); const result = await runReindexBatch(100); assert.ok(typeof result === "object" && result !== null, "result must be an object"); assert.ok("processed" in result, "result must have processed field"); assert.ok("errors" in result, "result must have errors field"); assert.equal(typeof result.processed, "number"); assert.equal(typeof result.errors, "number"); assert.ok(result.processed >= 0, "processed must be non-negative"); assert.ok(result.errors >= 0, "errors must be non-negative"); }); test("runReindexBatch: does not crash when called repeatedly on empty queue", async () => { core.getDbInstance(); await assert.doesNotReject(async () => { await runReindexBatch(10); await runReindexBatch(10); await runReindexBatch(10); }, "repeated calls on empty queue must not throw"); });