test(sf): lock embedding lifecycle hygiene contract

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) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-02 23:35:15 +02:00
parent 3b5e6588e9
commit 54f27bd02c

View file

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