From cc243a9d47a0547ee00aee99ca60d340bfb4942d Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 11:02:25 -0300 Subject: [PATCH] fix(memory): defensive markNeedsReindex + drain setImmediate in legacy tests - store.ts: wrap markMemoryNeedsReindex in safeMarkNeedsReindex helper that swallows errors when the DB is no longer available (e.g. test teardown after the parent promise resolved). Prevents fire-and-forget vector upserts from triggering unhandledRejection in tests. - memory-store.test.ts: drain setImmediate in afterEach/after hooks so pending vector upsert tasks settle before DATA_DIR is removed. - memory-settings.test.ts: extend deepEqual expected shape with the 7 new fields introduced by plan 21 F5 (embeddingSource, embeddingProviderModel, transformersEnabled, staticEnabled, rerankEnabled, rerankProviderModel, vectorStore). --- src/lib/memory/store.ts | 21 +++++++++++++++++---- tests/unit/memory-settings.test.ts | 8 ++++++++ tests/unit/memory-store.test.ts | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/lib/memory/store.ts b/src/lib/memory/store.ts index eb0fa1b570..6e9004595d 100644 --- a/src/lib/memory/store.ts +++ b/src/lib/memory/store.ts @@ -101,6 +101,19 @@ function findExistingMemory( * Fire-and-forget: generate embedding for a memory and upsert into sqlite-vec. * Errors are logged but never thrown — this must never block the SQLite write. */ +/** + * Best-effort: try to mark a memory needs_reindex. Swallows errors so that DB-closed + * states (e.g. test teardown after the parent promise resolved) never escape as + * unhandledRejection. Producing this side-effect is opportunistic by design. + */ +function safeMarkNeedsReindex(id: string, needs: boolean): void { + try { + markMemoryNeedsReindex(id, needs); + } catch { + // intentional swallow — DB may be closed (test teardown) or schema not yet ready + } +} + function scheduleVectorUpsert(id: string, content: string): void { setImmediate(async () => { try { @@ -115,25 +128,25 @@ function scheduleVectorUpsert(id: string, content: string): void { reason: embeddingResult.reason, message: sanitizeErrorMessage(embeddingResult.message), }); - markMemoryNeedsReindex(id, true); + safeMarkNeedsReindex(id, true); return; } const vec = getVectorStore(); if (!vec) { - markMemoryNeedsReindex(id, true); + safeMarkNeedsReindex(id, true); return; } await vec.ensureReady(resolution); await vec.upsertVector(id, embeddingResult.vector); - markMemoryNeedsReindex(id, false); + safeMarkNeedsReindex(id, false); } catch (err: unknown) { log.warn("memory.vec.upsert.fail", { id, error: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)), }); - markMemoryNeedsReindex(id, true); + safeMarkNeedsReindex(id, true); } }); } diff --git a/tests/unit/memory-settings.test.ts b/tests/unit/memory-settings.test.ts index f47f991b06..4f9eb2a25e 100644 --- a/tests/unit/memory-settings.test.ts +++ b/tests/unit/memory-settings.test.ts @@ -23,6 +23,14 @@ describe("memory settings helpers", () => { retentionDays: 1, strategy: DEFAULT_MEMORY_SETTINGS.strategy, skillsEnabled: true, + // Plan 21 — Memory Engine Redesign extended fields (default values) + embeddingSource: DEFAULT_MEMORY_SETTINGS.embeddingSource, + embeddingProviderModel: DEFAULT_MEMORY_SETTINGS.embeddingProviderModel, + transformersEnabled: DEFAULT_MEMORY_SETTINGS.transformersEnabled, + staticEnabled: DEFAULT_MEMORY_SETTINGS.staticEnabled, + rerankEnabled: DEFAULT_MEMORY_SETTINGS.rerankEnabled, + rerankProviderModel: DEFAULT_MEMORY_SETTINGS.rerankProviderModel, + vectorStore: DEFAULT_MEMORY_SETTINGS.vectorStore, }); }); diff --git a/tests/unit/memory-store.test.ts b/tests/unit/memory-store.test.ts index a629f064f1..1699955136 100644 --- a/tests/unit/memory-store.test.ts +++ b/tests/unit/memory-store.test.ts @@ -52,11 +52,29 @@ function insertMemoryRow({ ).run(id, apiKeyId, sessionId, type, key, content, metadata, createdAt, updatedAt, expiresAt); } +/** + * Drain pending setImmediate callbacks scheduled by store.createMemory/updateMemory + * (which fire-and-forget the vector upsert). Without this, the test runner may end + * before the async tasks resolve, causing "asynchronous activity after test ended" + * errors when the upsert later tries to touch the DB we already tore down. + */ +async function drainSetImmediate(rounds = 3): Promise { + for (let i = 0; i < rounds; i++) { + await new Promise((resolve) => setImmediate(resolve)); + } +} + test.beforeEach(async () => { await resetStorage(); }); +test.afterEach(async () => { + // Allow vector upsert fire-and-forget to settle before the next test resets DATA_DIR. + await drainSetImmediate(); +}); + test.after(async () => { + await drainSetImmediate(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); });