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

471 lines
13 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
createLegacyMemoryBackend,
createMemoryV2,
resolveMemoryV2Policy,
} from './memory-v2.mjs';
function silentLogger() {
return { warn() {} };
}
test('resolveMemoryV2Policy uses legacy memory flag for backward compatibility', () => {
assert.equal(resolveMemoryV2Policy({ env: {} }).enabled, true);
assert.equal(
resolveMemoryV2Policy({
env: { USER_CONVERSATION_MEMORY_ENABLED: '0' },
}).enabled,
false,
);
assert.equal(
resolveMemoryV2Policy({
env: {
USER_CONVERSATION_MEMORY_ENABLED: '0',
MEMORY_ENABLED: 'true',
},
}).enabled,
true,
);
assert.equal(
resolveMemoryV2Policy({
env: { MEMORY_VECTOR_ENABLED: '1' },
}).vectorEnabled,
true,
);
});
test('legacy backend adapts existing conversation memory service', async () => {
const calls = [];
const backend = createLegacyMemoryBackend({
async listMemories(userId, options) {
calls.push(['list', userId, options]);
return [{ id: 'm1', label: 'preference', text: '用户喜欢简洁回答' }];
},
async saveAndAnalyze(sessionId, userId, messages) {
calls.push(['write', sessionId, userId, messages.length]);
return { saved: messages.length, analyzed: 1, memories: 1 };
},
async analyzeUser(userId) {
calls.push(['compact', userId]);
return { analyzed: 2, memories: 1 };
},
});
const resolved = await backend.resolve({ userId: 'u1', limit: 3 });
const written = await backend.write({
userId: 'u1',
sessionId: 's1',
messages: [{ role: 'user', content: [] }],
});
const compacted = await backend.compact({ userId: 'u1' });
assert.deepEqual(resolved.memories, [
{ id: 'm1', label: 'preference', text: '用户喜欢简洁回答' },
]);
assert.deepEqual(written, { saved: 1, analyzed: 1, memories: 1 });
assert.deepEqual(compacted, { analyzed: 2, memories: 1 });
assert.deepEqual(calls, [
['list', 'u1', { limit: 3 }],
['write', 's1', 'u1', 1],
['compact', 'u1'],
]);
});
test('Memory V2 resolve returns unified payload from legacy backend', async () => {
const memory = createMemoryV2({
logger: silentLogger(),
legacyMemoryService: {
async listMemories() {
return [{ id: 'm1', label: 'fact', text: '用户常做基础设施排障' }];
},
},
env: { MEMORY_ENABLED: '1' },
});
const result = await memory.resolve({ userId: 'u1', query: '帮我看 103' });
assert.equal(result.ok, true);
assert.equal(result.enabled, true);
assert.equal(result.source, 'legacy-conversation-memory');
assert.deepEqual(result.memories, [
{ id: 'm1', label: 'fact', text: '用户常做基础设施排障' },
]);
assert.deepEqual(result.semanticMemories, result.memories);
});
test('Memory V2 feature flags can disable reads and writes without touching legacy service', async () => {
let touched = false;
const memory = createMemoryV2({
logger: silentLogger(),
legacyMemoryService: {
async listMemories() {
touched = true;
return [];
},
async saveAndAnalyze() {
touched = true;
return { saved: 1, analyzed: 1, memories: 1 };
},
},
env: { MEMORY_ENABLED: '0' },
});
assert.deepEqual(await memory.resolve({ userId: 'u1' }), {
ok: true,
enabled: false,
skipped: true,
reason: 'disabled',
degraded: false,
source: null,
profile: null,
semanticMemories: [],
behaviorSummary: null,
activeGoals: [],
contextGoals: [],
memories: [],
});
assert.deepEqual(await memory.write({ userId: 'u1', sessionId: 's1' }), {
ok: true,
enabled: false,
skipped: true,
reason: 'disabled',
source: null,
saved: 0,
analyzed: 0,
memories: 0,
});
assert.equal(touched, false);
});
test('Memory V2 fails open when backend resolve fails', async () => {
const memory = createMemoryV2({
logger: silentLogger(),
backends: [
{
name: 'broken-backend',
async resolve() {
throw new Error('backend unavailable');
},
},
],
env: { MEMORY_ENABLED: '1' },
});
const result = await memory.resolve({ userId: 'u1' });
assert.equal(result.ok, false);
assert.equal(result.enabled, true);
assert.equal(result.degraded, true);
assert.equal(result.reason, 'backend_failure');
assert.deepEqual(result.memories, []);
});
test('Memory V2 write honors event log flag', async () => {
let touched = false;
const memory = createMemoryV2({
logger: silentLogger(),
legacyMemoryService: {
async saveAndAnalyze() {
touched = true;
return { saved: 1, analyzed: 1, memories: 1 };
},
},
env: {
MEMORY_ENABLED: '1',
MEMORY_EVENT_LOG_ENABLED: '0',
},
});
const result = await memory.write({ userId: 'u1', sessionId: 's1', messages: [] });
assert.equal(result.skipped, true);
assert.equal(result.reason, 'event_log_disabled');
assert.equal(touched, false);
});
test('Memory V2 selects configured pluggable backend when available', async () => {
const calls = [];
const memory = createMemoryV2({
logger: silentLogger(),
backends: [
{
name: 'legacy-conversation-memory',
async resolve() {
calls.push('legacy');
return { memories: [{ text: 'legacy' }] };
},
},
{
name: 'pgvector',
async resolve() {
calls.push('pgvector');
return {
profile: { responseStyle: 'concise' },
semanticMemories: ['用户关注 memory facade'],
memories: [{ label: 'fact', text: 'vector result' }],
};
},
},
],
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
MEMORY_PROFILE_ENABLED: '1',
},
});
const result = await memory.resolve({ userId: 'u1', query: 'memory-chain' });
assert.deepEqual(calls, ['pgvector']);
assert.equal(result.source, 'pgvector');
assert.deepEqual(result.profile, { responseStyle: 'concise' });
assert.deepEqual(result.semanticMemories, ['用户关注 memory facade']);
assert.deepEqual(result.memories, [{ label: 'fact', text: 'vector result' }]);
});
test('Memory V2 normalizes external resolve payloads into tool-safe context fields', async () => {
const memory = createMemoryV2({
logger: silentLogger(),
backends: [
{
name: 'langgraph',
async resolve() {
return {
tool: 'browser',
action: 'download',
url: 'https://example.invalid',
memories: [
{
id: 'm1',
label: 'fact',
text: 'user prefers cautious memory rollouts',
tool: 'download',
functionCall: { name: 'bad' },
url: 'https://example.invalid',
filePath: '/tmp/bad',
arguments: { value: 1 },
score: '0.7',
created_at: '2026-07-03T00:00:00Z',
},
],
semanticMemories: [
{ text: 'semantic safe', toolName: 'bad_tool' },
{ action: 'call_tool', arguments: {} },
'plain semantic',
],
behaviorSummary: 'Use memory as context only.',
activeGoals: [
{ text: 'preserve execution boundary', tool: 'bad' },
{ command: 'rm -rf /', arguments: [] },
'keep memory in context domain',
],
};
},
},
],
env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'langgraph' },
});
const result = await memory.resolve({ userId: 'u1', query: 'memory-chain' });
assert.equal(result.source, 'langgraph');
assert.deepEqual(result.memories, [
{
id: 'm1',
label: 'fact',
text: 'user prefers cautious memory rollouts',
createdAt: '2026-07-03T00:00:00Z',
score: 0.7,
},
]);
assert.deepEqual(result.semanticMemories, ['semantic safe', 'plain semantic']);
assert.equal(result.behaviorSummary, 'Use memory as context only.');
assert.deepEqual(result.contextGoals, [
'preserve execution boundary',
'keep memory in context domain',
]);
assert.deepEqual(result.activeGoals, result.contextGoals);
assert.equal(result.tool, undefined);
assert.equal(result.action, undefined);
assert.equal(result.url, undefined);
assert.equal(result.memories[0].tool, undefined);
assert.equal(result.memories[0].functionCall, undefined);
assert.equal(result.memories[0].url, undefined);
assert.equal(result.memories[0].filePath, undefined);
assert.equal(result.memories[0].arguments, undefined);
});
test('Memory V2 falls back to first available backend when preferred backend is unavailable', async () => {
const memory = createMemoryV2({
logger: silentLogger(),
backends: [
{
name: 'pgvector',
isAvailable() {
return false;
},
async resolve() {
throw new Error('should not use unavailable backend');
},
},
{
name: 'legacy-conversation-memory',
async resolve() {
return { memories: [{ label: 'fact', text: 'legacy fallback' }] };
},
},
],
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
},
});
const result = await memory.resolve({ userId: 'u1' });
assert.equal(result.source, 'legacy-conversation-memory');
assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy fallback' }]);
});
test('Memory V2 selects backends per operation so semantic read does not block legacy writes', async () => {
const calls = [];
const memory = createMemoryV2({
logger: silentLogger(),
backends: [
{
name: 'pgvector',
isAvailable() {
return true;
},
async resolve() {
calls.push('pgvector.resolve');
return { memories: [{ label: 'semantic', text: 'vector read' }] };
},
},
{
name: 'legacy-conversation-memory',
async resolve() {
calls.push('legacy.resolve');
return { memories: [{ label: 'fact', text: 'legacy read' }] };
},
async write() {
calls.push('legacy.write');
return { saved: 1, analyzed: 1, memories: 1 };
},
async compact() {
calls.push('legacy.compact');
return { analyzed: 2, memories: 1 };
},
},
],
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
MEMORY_EVENT_LOG_ENABLED: '1',
},
});
const resolved = await memory.resolve({ userId: 'u1', query: 'memory-chain' });
const written = await memory.write({ userId: 'u1', sessionId: 's1', messages: [] });
const compacted = await memory.compact({ userId: 'u1' });
assert.equal(resolved.source, 'pgvector');
assert.equal(written.source, 'legacy-conversation-memory');
assert.equal(compacted.source, 'legacy-conversation-memory');
assert.deepEqual(calls, ['pgvector.resolve', 'legacy.write', 'legacy.compact']);
});
test('Memory V2 can suppress profile payloads through policy', async () => {
const memory = createMemoryV2({
logger: silentLogger(),
backends: [
{
name: 'profile-backend',
async resolve() {
return {
profile: { responseStyle: 'detailed' },
memories: [{ label: 'preference', text: '用户喜欢完整回答' }],
};
},
},
],
env: {
MEMORY_ENABLED: '1',
MEMORY_PROFILE_ENABLED: '0',
},
});
const result = await memory.resolve({ userId: 'u1' });
assert.equal(result.profile, null);
assert.deepEqual(result.memories, [{ label: 'preference', text: '用户喜欢完整回答' }]);
});
test('Memory V2 getStatus exposes policy and backend contract details', () => {
const memory = createMemoryV2({
logger: silentLogger(),
backends: [
{
name: 'legacy-conversation-memory',
async resolve() {
return { memories: [] };
},
async write() {
return { saved: 0, analyzed: 0, memories: 0 };
},
async compact() {
return { analyzed: 0, memories: 0 };
},
},
{
name: 'pgvector',
isAvailable() {
return false;
},
async resolve() {
return { memories: [] };
},
},
],
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
MEMORY_PROFILE_ENABLED: '0',
MEMORY_EVENT_LOG_ENABLED: '1',
MEMORY_VECTOR_ENABLED: '1',
MEMORY_FAIL_OPEN: '1',
},
});
assert.deepEqual(memory.getStatus(), {
enabled: true,
backend: 'pgvector',
selectedBackend: 'legacy-conversation-memory',
profileEnabled: false,
eventLogEnabled: true,
vectorEnabled: true,
failOpen: true,
backends: [
{
name: 'legacy-conversation-memory',
available: true,
supports: {
resolve: true,
write: true,
compact: true,
},
},
{
name: 'pgvector',
available: false,
supports: {
resolve: true,
write: false,
compact: false,
},
},
],
});
});