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, ); const runtimePolicy = resolveMemoryV2Policy({ env: {} }); assert.equal(runtimePolicy.agentResolveEnabled, false); assert.equal(runtimePolicy.agentInjectionMode, 'off'); assert.deepEqual(runtimePolicy.agentCanaryUserIds, []); assert.equal(runtimePolicy.agentResolveLimit, 3); assert.equal(runtimePolicy.promotionEnabled, false); }); 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, runtimeControl: { agentResolveEnabled: false, agentInjectionMode: 'off', agentCanaryUserIds: [], agentResolveLimit: 3, agentResolveTimeoutMs: 1200, promotionEnabled: false, compactionV2Enabled: false, reflectionEnabled: false, lifecycleWorkerEnabled: false, lifecycleRolloutMode: 'off', lifecycleRolloutUserIds: [], }, 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, }, }, ], }); });