From 54f27bd02c21553f41660704aa8966a29122c544 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 23:35:15 +0200 Subject: [PATCH] test(sf): lock embedding lifecycle hygiene contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new tests covering the embedding-cleanup paths shipped in 7bec2dc2d / 1b71ddd17 / 05a326a29: 1. updateMemoryContent → drops the existing memory_embeddings row (next backfill re-embeds the new content). 2. supersedeMemory → drops the superseded memory's embedding while preserving the live one's. 3. enforceMemoryCap → sweeps embeddings of newly-superseded memories so memory_embeddings stays aligned with active memories after a batch cap. Without these, a regression in the cleanup paths would silently leave orphaned vectors that loadAllEmbeddings's superseded_by filter masks at query time but bloats the table forever. 11 → 14 tests in memory-store.test.ts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../extensions/sf/tests/memory-store.test.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/resources/extensions/sf/tests/memory-store.test.ts b/src/resources/extensions/sf/tests/memory-store.test.ts index 16b89c54a..214111757 100644 --- a/src/resources/extensions/sf/tests/memory-store.test.ts +++ b/src/resources/extensions/sf/tests/memory-store.test.ts @@ -519,3 +519,113 @@ test("memory-store: schema includes memories table", () => { closeDatabase(); }); + +// ═══════════════════════════════════════════════════════════════════════════ +// memory-store: embedding lifecycle hygiene +// (locks the contract: updateMemoryContent invalidates, supersedeMemory drops, +// enforceMemoryCap sweeps. Without these, memory_embeddings accumulates dead +// rows and ranking returns stale vectors.) +// ═══════════════════════════════════════════════════════════════════════════ + +test("updateMemoryContent invalidates the embedding row", async () => { + closeDatabase(); + const { mkdtempSync, rmSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const { saveEmbedding } = await import("../memory-embeddings.ts"); + + const dir = mkdtempSync(join(tmpdir(), "sf-update-embed-")); + openDatabase(join(dir, "sf.db")); + const id = createMemory({ category: "architecture", content: "old" }); + assert.ok(id); + saveEmbedding(id, Float32Array.from([1, 2, 3]), "test-model"); + const adapter = _getAdapter()!; + const before = adapter + .prepare("SELECT count(*) as c FROM memory_embeddings WHERE memory_id = :id") + .get({ ":id": id }); + assert.equal(before?.["c"], 1); + + updateMemoryContent(id, "new content"); + + const after = adapter + .prepare("SELECT count(*) as c FROM memory_embeddings WHERE memory_id = :id") + .get({ ":id": id }); + assert.equal(after?.["c"], 0, "embedding row should be dropped after content update"); + + closeDatabase(); + rmSync(dir, { recursive: true, force: true }); +}); + +test("supersedeMemory drops the superseded memory's embedding row", async () => { + closeDatabase(); + const { mkdtempSync, rmSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const { saveEmbedding } = await import("../memory-embeddings.ts"); + + const dir = mkdtempSync(join(tmpdir(), "sf-supersede-embed-")); + openDatabase(join(dir, "sf.db")); + const oldId = createMemory({ category: "architecture", content: "old" }); + const newId = createMemory({ category: "architecture", content: "new" }); + assert.ok(oldId && newId); + saveEmbedding(oldId, Float32Array.from([1, 2, 3]), "test-model"); + saveEmbedding(newId, Float32Array.from([4, 5, 6]), "test-model"); + + supersedeMemory(oldId, newId); + + const adapter = _getAdapter()!; + const oldRow = adapter + .prepare("SELECT count(*) as c FROM memory_embeddings WHERE memory_id = :id") + .get({ ":id": oldId }); + const newRow = adapter + .prepare("SELECT count(*) as c FROM memory_embeddings WHERE memory_id = :id") + .get({ ":id": newId }); + assert.equal(oldRow?.["c"], 0, "superseded memory's embedding should be dropped"); + assert.equal(newRow?.["c"], 1, "live memory's embedding should be preserved"); + + closeDatabase(); + rmSync(dir, { recursive: true, force: true }); +}); + +test("enforceMemoryCap sweeps embeddings of newly-superseded memories", async () => { + closeDatabase(); + const { mkdtempSync, rmSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const { saveEmbedding } = await import("../memory-embeddings.ts"); + + const dir = mkdtempSync(join(tmpdir(), "sf-cap-embed-")); + openDatabase(join(dir, "sf.db")); + // Create 5 memories with descending confidence so the cap predictably + // keeps the top 3. + const ids: string[] = []; + for (let i = 0; i < 5; i++) { + const id = createMemory({ + category: "architecture", + content: `memory ${i}`, + confidence: 0.9 - i * 0.1, + }); + assert.ok(id); + ids.push(id); + saveEmbedding(id, Float32Array.from([i, i, i]), "test-model"); + } + const adapter = _getAdapter()!; + const beforeCount = adapter + .prepare("SELECT count(*) as c FROM memory_embeddings") + .get(); + assert.equal(beforeCount?.["c"], 5); + + enforceMemoryCap(3); + + const afterCount = adapter + .prepare("SELECT count(*) as c FROM memory_embeddings") + .get(); + assert.equal( + afterCount?.["c"], + 3, + "embeddings should align with the 3 active memories after cap enforcement", + ); + + closeDatabase(); + rmSync(dir, { recursive: true, force: true }); +});