Files
diegosouzapw--omniroute/tests/unit/memory-reindex-batch.test.ts
2026-07-13 13:39:12 +08:00

174 lines
6.7 KiB
TypeScript

/**
* 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<typeof core.getDbInstance>,
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");
});