174 lines
6.7 KiB
TypeScript
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");
|
|
});
|