Files
memind/memory-v2-pgvector-backfill.test.mjs
T
2026-07-03 09:46:03 +08:00

165 lines
4.4 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
backfillLegacyMemoriesToPgvector,
loadLegacyMemoryBackfillBatch,
} from './memory-v2-pgvector-backfill.mjs';
function createMysqlPool(rows) {
const calls = [];
return {
calls,
async query(sql, params) {
calls.push({ sql, params });
const [afterUpdatedAt, sameUpdatedAt, afterId, limit] = params;
const filtered = rows
.filter((row) => row.status === 'active')
.filter((row) => row.updated_at > afterUpdatedAt || (row.updated_at === sameUpdatedAt && row.id > afterId))
.sort((left, right) => left.updated_at - right.updated_at || left.id.localeCompare(right.id))
.slice(0, limit);
return [filtered];
},
};
}
const legacyRows = [
{
id: 'mem-1',
user_id: 'user-1',
label: 'fact',
memory_text: '用户关注 Memory V2 facade',
evidence_message_id: 'msg-1',
source_session_id: 'session-1',
confidence: '0.800',
status: 'active',
created_at: 1000,
updated_at: 2000,
},
{
id: 'mem-2',
user_id: 'user-1',
label: 'preference',
memory_text: '用户喜欢渐进式架构升级',
evidence_message_id: null,
source_session_id: 'session-2',
confidence: '0.700',
status: 'active',
created_at: 1100,
updated_at: 3000,
},
{
id: 'mem-archived',
user_id: 'user-1',
label: 'fact',
memory_text: 'archived should not move',
status: 'archived',
created_at: 1200,
updated_at: 4000,
},
];
test('loadLegacyMemoryBackfillBatch reads active memories with cursor ordering', async () => {
const mysqlPool = createMysqlPool(legacyRows);
const batch = await loadLegacyMemoryBackfillBatch(mysqlPool, {
cursor: { updatedAt: 2000, id: 'mem-1' },
limit: 10,
});
assert.equal(batch.memories.length, 1);
assert.equal(batch.memories[0].id, 'mem-2');
assert.deepEqual(batch.nextCursor, { updatedAt: 3000, id: 'mem-2' });
assert.equal(batch.hasMore, false);
assert.match(mysqlPool.calls[0].sql, /FROM h5_user_memory_items/);
assert.deepEqual(mysqlPool.calls[0].params, [2000, 2000, 'mem-1', 10]);
});
test('backfillLegacyMemoriesToPgvector dry-run does not embed or write', async () => {
const mysqlPool = createMysqlPool(legacyRows);
let embedded = false;
let written = false;
const result = await backfillLegacyMemoriesToPgvector({
mysqlPool,
pgPool: {
async query() {
written = true;
},
},
embedMemory: async () => {
embedded = true;
return [0.1, 0.2];
},
dryRun: true,
limit: 1,
});
assert.equal(result.mode, 'dry-run');
assert.equal(result.scanned, 1);
assert.equal(result.inserted, 0);
assert.equal(result.hasMore, true);
assert.equal(embedded, false);
assert.equal(written, false);
});
test('backfillLegacyMemoriesToPgvector apply embeds and upserts rows', async () => {
const mysqlPool = createMysqlPool(legacyRows);
const pgCalls = [];
const result = await backfillLegacyMemoriesToPgvector({
mysqlPool,
pgPool: {
async query(sql, params) {
pgCalls.push({ sql, params });
return { rows: [] };
},
},
embedMemory: async (memory) => {
assert.match(memory.text, /Memory V2|渐进式/);
return memory.id === 'mem-1' ? [0.1, 0.2] : [0.3, 0.4];
},
dryRun: false,
limit: 10,
});
assert.equal(result.mode, 'apply');
assert.equal(result.scanned, 2);
assert.equal(result.inserted, 2);
assert.equal(pgCalls.length, 2);
assert.match(pgCalls[0].sql, /INSERT INTO "memory_embeddings"/);
assert.match(pgCalls[0].sql, /ON CONFLICT \(source_memory_id\)/);
assert.deepEqual(pgCalls[0].params.slice(0, 8), [
'user-1',
'用户关注 Memory V2 facade',
'[0.1,0.2]',
'fact',
'mem-1',
'session-1',
'msg-1',
JSON.stringify({ confidence: 0.8 }),
]);
assert.ok(pgCalls[0].params[8] instanceof Date);
assert.ok(pgCalls[0].params[9] instanceof Date);
});
test('backfillLegacyMemoriesToPgvector apply requires pg pool and embedding function', async () => {
const mysqlPool = createMysqlPool(legacyRows);
await assert.rejects(
() => backfillLegacyMemoriesToPgvector({
mysqlPool,
dryRun: false,
embedMemory: async () => [0.1],
}),
/PostgreSQL pool/,
);
await assert.rejects(
() => backfillLegacyMemoriesToPgvector({
mysqlPool,
dryRun: false,
pgPool: { async query() {} },
}),
/embedMemory/,
);
});