Files
memind/memory-v2-pgvector.test.mjs
T
john dcf83e9b6f fix(memory-v2): improve pgvector recall and WeChat memory injection
Add keyword fallback and dedupe for pgvector resolve, score personal vs episodic memories by relevance, record WeChat recall product events, and disable broken chatrecall on Postgres session storage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 22:26:47 +08:00

251 lines
7.5 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createMemoryV2 } from './memory-v2.mjs';
import {
createPgvectorMemoryBackend,
pgvectorMemoryBackendInternals,
} from './memory-v2-pgvector.mjs';
test('pgvector backend is disabled by default and does not query storage', async () => {
let queried = false;
const backend = createPgvectorMemoryBackend({
pool: {
async query() {
queried = true;
return { rows: [] };
},
},
});
assert.equal(backend.isAvailable(), false);
assert.deepEqual(await backend.resolve({
userId: 'user-1',
embedding: [0.1, 0.2],
}), {
memories: [],
semanticMemories: [],
});
assert.equal(queried, false);
});
test('pgvector backend returns empty semantic result when embedding is unavailable', async () => {
let queried = false;
const backend = createPgvectorMemoryBackend({
enabled: true,
pool: {
async query() {
queried = true;
return { rows: [] };
},
},
});
const result = await backend.resolve({
userId: 'user-1',
query: 'memory-chain',
});
assert.deepEqual(result, {
memories: [],
semanticMemories: [],
});
assert.equal(queried, false);
});
test('pgvector backend performs parameterized vector lookup when explicitly enabled', async () => {
const queries = [];
const backend = createPgvectorMemoryBackend({
enabled: true,
tableName: 'memory_embeddings',
pool: {
async query(sql, params) {
queries.push({ sql, params });
return {
rows: [
{
id: 7,
content: '用户关注 Memory V2 的 facade 边界',
type: 'fact',
score: '0.87',
created_at: '2026-07-02T00:00:00.000Z',
},
],
};
},
},
embedQuery: async (query) => {
assert.equal(query, 'memory-chain');
return [0.25, 0.5, 0.75];
},
});
assert.equal(backend.isAvailable(), true);
const result = await backend.resolve({
userId: 'user-1',
query: 'memory-chain',
limit: 5,
});
assert.equal(queries.length, 2);
assert.match(queries[0].sql, /FROM memory_embeddings/);
assert.match(queries[0].sql, /WITH vector_candidates/);
assert.match(queries[0].sql, /recent_candidates/);
assert.deepEqual(queries[0].params, ['user-1', '[0.25,0.5,0.75]', 50]);
assert.match(queries[1].sql, /ILIKE/);
assert.deepEqual(result.semanticMemories, ['用户关注 Memory V2 的 facade 边界']);
assert.deepEqual(result.memories, [
{
id: '7',
label: 'fact',
text: '用户关注 Memory V2 的 facade 边界',
score: 0.87,
createdAt: '2026-07-02T00:00:00.000Z',
updatedAt: '2026-07-02T00:00:00.000Z',
},
]);
});
test('pgvector hybrid ranking recovers a recent Chinese memory missed by vector top-k', async () => {
const marker = 'MEM-RECALL-NEW';
const backend = createPgvectorMemoryBackend({
enabled: true,
pool: {
async query() {
return {
rows: [
{
id: 1,
content: '用户以前关注贵州旅游攻略',
type: 'interest',
score: 0.91,
created_at: '2026-06-01T00:00:00.000Z',
updated_at: '2026-06-01T00:00:00.000Z',
},
{
id: 2,
content: `用户的记忆召回灰度测试代号是 ${marker}`,
type: 'fact',
score: -0.49,
created_at: '2026-07-22T00:00:00.000Z',
updated_at: '2026-07-22T00:00:00.000Z',
},
{
id: 3,
content: '用户偏好简洁回答',
type: 'preference',
score: 0.3,
created_at: '2026-07-21T00:00:00.000Z',
updated_at: '2026-07-21T00:00:00.000Z',
},
],
};
},
},
embedQuery: async () => [0.25, 0.5, 0.75],
});
const result = await backend.resolve({
userId: 'user-1',
query: '我之前让你记住的记忆召回灰度测试代号是什么?请只回答完整代号。',
limit: 3,
});
assert.match(result.memories[0].text, new RegExp(marker));
});
test('pgvector hybrid ranking keeps vector order when query has no lexical overlap', () => {
const ranked = pgvectorMemoryBackendInternals.rankHybridCandidates([
{ id: 1, content: 'alpha', score: 0.2 },
{ id: 2, content: 'beta', score: 0.8 },
], '完全无关的中文查询', 2);
assert.equal(ranked[0].id, '2');
assert.equal(ranked[1].id, '1');
});
test('pgvector keyword fallback retrieves topic memories missed by vector top-k', async () => {
const queries = [];
const backend = createPgvectorMemoryBackend({
enabled: true,
pool: {
async query(sql, params) {
queries.push({ sql, params });
if (String(sql).includes('ILIKE')) {
return {
rows: [{
id: 901,
content: '用户对日本战国人物德川家康感兴趣,并希望继续深入讨论',
type: 'interest',
score: 1,
created_at: '2026-08-01T13:57:00.000Z',
updated_at: '2026-08-01T13:57:00.000Z',
}],
};
}
return {
rows: [{
id: 1,
content: '用户以后只要说“帮我搜索今天国际国内热门新闻和小知识,做成页面”',
type: 'preference',
score: 0.91,
created_at: '2026-07-31T00:00:00.000Z',
updated_at: '2026-07-31T00:00:00.000Z',
}],
};
},
},
embedQuery: async () => [0.25, 0.5, 0.75],
});
const result = await backend.resolve({
userId: 'user-tang',
query: '我们继续聊聊德川家康',
limit: 2,
});
assert.match(result.memories[0].text, /德川家康/);
assert.equal(queries.some((entry) => String(entry.sql).includes('ILIKE')), true);
});
test('extractKeywordTerms keeps topic phrases and drops recall boilerplate', () => {
const terms = pgvectorMemoryBackendInternals.extractKeywordTerms('我们之前有聊过,你记得吗');
assert.equal(terms.includes('记得'), false);
assert.equal(terms.includes('我们'), false);
const topicTerms = pgvectorMemoryBackendInternals.extractKeywordTerms('我们继续聊聊德川家康');
assert.equal(topicTerms.some((term) => term.includes('德川')), true);
});
test('pgvector backend validates table names before building SQL', () => {
assert.throws(
() => createPgvectorMemoryBackend({ tableName: 'memory_embeddings;DROP TABLE users' }),
/Invalid pgvector table name/,
);
});
test('Memory V2 can expose pgvector as unavailable plugin without selecting it', async () => {
const memory = createMemoryV2({
logger: { warn() {} },
backends: [
createPgvectorMemoryBackend({ enabled: false }),
{
name: 'legacy-conversation-memory',
async resolve() {
return { memories: [{ label: 'fact', text: 'legacy survives' }] };
},
},
],
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
MEMORY_VECTOR_ENABLED: '1',
},
});
const status = memory.getStatus();
const result = await memory.resolve({ userId: 'user-1', query: 'memory-chain' });
assert.equal(status.vectorEnabled, true);
assert.equal(status.selectedBackend, 'legacy-conversation-memory');
assert.equal(status.backends.find((item) => item.name === 'pgvector')?.available, false);
assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy survives' }]);
});