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 }); +});