Files

251 lines
7.5 KiB
JavaScript
Raw Permalink Normal View History

2026-07-03 09:46:03 +08:00
import assert from 'node:assert/strict';
import test from 'node:test';
import { createMemoryV2 } from './memory-v2.mjs';
2026-07-22 11:15:00 +08:00
import {
createPgvectorMemoryBackend,
pgvectorMemoryBackendInternals,
} from './memory-v2-pgvector.mjs';
2026-07-03 09:46:03 +08:00
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);
2026-07-03 09:46:03 +08:00
assert.match(queries[0].sql, /FROM memory_embeddings/);
2026-07-22 11:15:00 +08:00
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/);
2026-07-03 09:46:03 +08:00
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',
2026-07-22 11:15:00 +08:00
updatedAt: '2026-07-02T00:00:00.000Z',
2026-07-03 09:46:03 +08:00
},
]);
});
2026-07-22 11:15:00 +08:00
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);
});
2026-07-03 09:46:03 +08:00
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' }]);
});