import assert from 'node:assert/strict'; import test from 'node:test'; import { createManagedMemoryV2Runtime, createMemoryV2Runtime } from './memory-v2-runtime.mjs'; function silentLogger() { return { warnings: [], warn(message) { this.warnings.push(message); }, }; } function legacyService(memories = []) { return { async listMemories() { return memories; }, async saveAndAnalyze() { return { saved: 1, analyzed: 1, memories: 1 }; }, async analyzeUser() { return { analyzed: 1, memories: 1 }; }, }; } test('createMemoryV2Runtime keeps pgvector dormant without explicit pg URL', async () => { let importedPg = false; const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy memory' }]), env: { MEMORY_ENABLED: '1', MEMORY_VECTOR_ENABLED: '1', MEMORY_BACKEND: 'pgvector', }, importPg() { importedPg = true; throw new Error('should not import pg'); }, }); const result = await memory.resolve({ userId: 'u1', query: 'hello' }); const pgvector = memory.getStatus().backends.find((backend) => backend.name === 'pgvector'); assert.equal(importedPg, false); assert.equal(memory.getStatus().selectedBackend, 'legacy-conversation-memory'); assert.equal(pgvector.available, false); assert.equal(pgvector.reason, 'not_configured'); assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy memory' }]); }); test('createMemoryV2Runtime does not open pg pool when embedding module is missing', async () => { let importedPg = false; const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService(), env: { MEMORY_ENABLED: '1', MEMORY_VECTOR_ENABLED: '1', MEMORY_BACKEND: 'pgvector', MEMORY_PGVECTOR_DATABASE_URL: 'postgresql://local/memory', }, importPg() { importedPg = true; throw new Error('should not import pg without embedding module'); }, }); const pgvector = memory.getStatus().backends.find((backend) => backend.name === 'pgvector'); assert.equal(importedPg, false); assert.equal(memory.getStatus().selectedBackend, 'legacy-conversation-memory'); assert.equal(pgvector.available, false); assert.equal(pgvector.reason, 'embedding_module_not_configured'); }); test('createMemoryV2Runtime selects pgvector only when pool and embedding are configured', async () => { const queries = []; let poolEnded = false; class FakePool { constructor(options) { this.options = options; } async query(sql, params) { queries.push({ sql, params, options: this.options }); return { rows: [ { id: 1, type: 'fact', content: 'semantic memory', score: 0.91, created_at: '2026-07-02T00:00:00.000Z', }, ], }; } async end() { poolEnded = true; } } const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy memory' }]), env: { MEMORY_ENABLED: '1', MEMORY_VECTOR_ENABLED: '1', MEMORY_BACKEND: 'pgvector', MEMORY_PGVECTOR_DATABASE_URL: 'postgresql://local/memory', MEMORY_PGVECTOR_EMBEDDING_MODULE: './fake-embed.mjs', MEMORY_PGVECTOR_POOL_MAX: '2', }, async importPg(specifier) { assert.equal(specifier, 'pg'); return { Pool: FakePool }; }, async importModule(specifier) { assert.match(specifier, /fake-embed\.mjs$/); return { async embedQuery(query) { assert.equal(query, 'memory-chain'); return [0.1, 0.2, 0.3]; }, }; }, async fetchImpl(url, options) { assert.equal(url, 'http://127.0.0.1:6333/collections/memind_memory/points/search'); assert.equal(options.method, 'POST'); assert.deepEqual(JSON.parse(options.body), { vector: [0.1, 0.2, 0.3], limit: 4, with_payload: true, filter: { must: [{ key: 'user_id', match: { value: 'u1' }, }], }, }); return { ok: true, async json() { return { result: [{ id: 'q1', score: 0.88, payload: { content: 'qdrant memory', type: 'fact', }, }], }; }, }; }, }); const result = await memory.resolve({ userId: 'u1', query: 'memory-chain' }); assert.equal(memory.getStatus().selectedBackend, 'pgvector'); assert.equal(result.source, 'pgvector'); assert.deepEqual(result.semanticMemories, ['semantic memory']); assert.equal(queries.length, 1); assert.equal(queries[0].options.connectionString, 'postgresql://local/memory'); assert.equal(queries[0].options.max, 2); assert.deepEqual(queries[0].params, ['u1', '[0.1,0.2,0.3]', 8]); await memory.close(); assert.equal(poolEnded, true); }); test('createMemoryV2Runtime falls back to legacy when pgvector init fails', async () => { const logger = silentLogger(); const memory = await createMemoryV2Runtime({ logger, legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy fallback' }]), env: { MEMORY_ENABLED: '1', MEMORY_VECTOR_ENABLED: '1', MEMORY_BACKEND: 'pgvector', MEMORY_PGVECTOR_DATABASE_URL: 'postgresql://local/memory', MEMORY_PGVECTOR_EMBEDDING_MODULE: './bad-embed.mjs', }, async importModule() { return {}; }, }); const result = await memory.resolve({ userId: 'u1', query: 'hello' }); const pgvector = memory.getStatus().backends.find((backend) => backend.name === 'pgvector'); assert.equal(memory.getStatus().selectedBackend, 'legacy-conversation-memory'); assert.equal(pgvector.available, false); assert.equal(pgvector.reason, 'init_failed'); assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy fallback' }]); assert.match(logger.warnings[0], /pgvector backend unavailable/); }); test('createMemoryV2Runtime replaces qdrant placeholder without network wiring', async () => { const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy qdrant fallback' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'qdrant', MEMORY_QDRANT_ENABLED: '1', MEMORY_QDRANT_URL: 'http://127.0.0.1:6333', MEMORY_QDRANT_COLLECTION: 'memind_memory', }, }); const status = memory.getStatus(); const qdrant = status.backends.find((backend) => backend.name === 'qdrant'); const result = await memory.resolve({ userId: 'u1', query: 'memory-chain' }); assert.equal(status.selectedBackend, 'legacy-conversation-memory'); assert.equal(qdrant.available, false); assert.equal(qdrant.reason, 'client_not_configured'); assert.equal(qdrant.category, 'semantic'); assert.equal(qdrant.flag, 'MEMORY_QDRANT_ENABLED'); assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy qdrant fallback' }]); }); test('createMemoryV2Runtime can select qdrant read-only client when embedding module is configured', async () => { const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy qdrant fallback' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'qdrant', MEMORY_QDRANT_ENABLED: '1', MEMORY_QDRANT_URL: 'http://127.0.0.1:6333', MEMORY_QDRANT_COLLECTION: 'memind_memory', MEMORY_QDRANT_EMBEDDING_MODULE: './qdrant-embed.mjs', MEMORY_QDRANT_LIMIT: '4', }, async importModule(specifier) { assert.match(specifier, /qdrant-embed\.mjs$/); return { async embedQuery(query) { assert.equal(query, 'memory-chain'); return [0.1, 0.2, 0.3]; }, }; }, async fetchImpl(url, options) { assert.equal(url, 'http://127.0.0.1:6333/collections/memind_memory/points/search'); assert.equal(options.method, 'POST'); assert.deepEqual(JSON.parse(options.body), { vector: [0.1, 0.2, 0.3], limit: 4, with_payload: true, filter: { must: [{ key: 'user_id', match: { value: 'u1' }, }], }, }); return { ok: true, async json() { return { result: [{ id: 'q1', score: 0.88, payload: { content: 'qdrant memory', type: 'fact', }, }], }; }, }; }, }); const qdrant = memory.getStatus().backends.find((backend) => backend.name === 'qdrant'); const result = await memory.resolve({ userId: 'u1', query: 'memory-chain' }); assert.equal(memory.getStatus().selectedBackend, 'qdrant'); assert.equal(qdrant.available, true); assert.equal(result.source, 'qdrant'); assert.deepEqual(result.memories, [{ id: 'q1', label: 'fact', text: 'qdrant memory', score: 0.88, createdAt: null, }]); }); test('createManagedMemoryV2Runtime refreshes backend selection from admin config state', async () => { let state = { fingerprint: 'v1', overrides: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'legacy', }, }; const memory = await createManagedMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy managed' }]), configService: { async getRuntimeState() { return state; }, }, async importModule(specifier) { assert.match(specifier, /managed-embed\.mjs$/); return { async embedQuery() { return [0.1, 0.2, 0.3]; }, }; }, async importPg() { return { Pool: class FakePool { async query() { return { rows: [{ id: 1, type: 'fact', content: 'pgvector managed', score: 0.9, created_at: '2026-07-02T00:00:00.000Z', }], }; } async end() {} }, }; }, }); const legacyResult = await memory.resolve({ userId: 'u1', query: 'hello' }); assert.equal(legacyResult.source, 'legacy-conversation-memory'); state = { fingerprint: 'v2', overrides: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'pgvector', MEMORY_VECTOR_ENABLED: '1', MEMORY_PGVECTOR_ENABLED: '1', MEMORY_PGVECTOR_DATABASE_URL: 'postgresql://local/memory', MEMORY_PGVECTOR_EMBEDDING_MODULE: './managed-embed.mjs', }, }; const pgvectorResult = await memory.resolve({ userId: 'u1', query: 'hello' }); const status = await memory.getStatus(); assert.equal(pgvectorResult.source, 'pgvector'); assert.equal(status.selectedBackend, 'pgvector'); assert.equal(status.configSource, 'admin-db'); }); test('createMemoryV2Runtime reports qdrant init failure without blocking memory', async () => { const logger = silentLogger(); const memory = await createMemoryV2Runtime({ logger, legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy after qdrant error' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'qdrant', MEMORY_QDRANT_ENABLED: '1', MEMORY_QDRANT_URL: 'ftp://bad-qdrant', }, }); const status = memory.getStatus(); const qdrant = status.backends.find((backend) => backend.name === 'qdrant'); const result = await memory.resolve({ userId: 'u1' }); assert.equal(status.selectedBackend, 'legacy-conversation-memory'); assert.equal(qdrant.available, false); assert.equal(qdrant.reason, 'init_failed'); assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy after qdrant error' }]); assert.match(logger.warnings[0], /qdrant backend unavailable/); }); test('createMemoryV2Runtime keeps mem0 as fallback when api key is incomplete', async () => { const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy resolve with mem0' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'mem0', MEMORY_MEM0_ENABLED: '1', MEMORY_MEM0_PROJECT_ID: 'memind_project', }, }); const status = memory.getStatus(); const mem0 = status.backends.find((backend) => backend.name === 'mem0'); 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(status.selectedBackend, 'legacy-conversation-memory'); assert.equal(mem0.available, false); assert.equal(mem0.reason, 'api_key_not_configured'); assert.equal(mem0.category, 'extraction'); assert.equal(mem0.flag, 'MEMORY_MEM0_ENABLED'); assert.deepEqual(resolved.memories, [{ label: 'fact', text: 'legacy resolve with mem0' }]); assert.equal(written.source, 'legacy-conversation-memory'); assert.equal(compacted.source, 'legacy-conversation-memory'); }); test('createMemoryV2Runtime can write and compact through Mem0 HTTP client', async () => { const requests = []; const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy resolve with mem0' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'mem0', MEMORY_MEM0_ENABLED: '1', MEMORY_MEM0_API_KEY: 'secret', MEMORY_MEM0_PROJECT_ID: 'memind_project', MEMORY_MEM0_BASE_URL: 'https://mem0.local', }, async fetchImpl(url, options) { requests.push({ url, body: JSON.parse(options.body) }); return { ok: true, async json() { return { saved: 1, analyzed: 1, memories: 1 }; }, }; }, }); const resolved = await memory.resolve({ userId: 'u1', query: 'memory-chain' }); const written = await memory.write({ userId: 'u1', sessionId: 's1', messages: [{ text: 'hi' }] }); const compacted = await memory.compact({ userId: 'u1' }); assert.equal(memory.getStatus().selectedBackend, 'legacy-conversation-memory'); assert.deepEqual(resolved.memories, [{ label: 'fact', text: 'legacy resolve with mem0' }]); assert.equal(written.source, 'mem0'); assert.equal(compacted.source, 'mem0'); assert.deepEqual(requests.map((request) => request.url), [ 'https://mem0.local/v1/memories', 'https://mem0.local/v1/memories/compact', ]); }); test('createMemoryV2Runtime reports mem0 init failure without blocking memory', async () => { const logger = silentLogger(); const memory = await createMemoryV2Runtime({ logger, legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy after mem0 error' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'mem0', MEMORY_MEM0_ENABLED: '1', MEMORY_MEM0_API_KEY: 'secret', MEMORY_MEM0_PROJECT_ID: 'bad project id', }, }); const status = memory.getStatus(); const mem0 = status.backends.find((backend) => backend.name === 'mem0'); const result = await memory.resolve({ userId: 'u1' }); assert.equal(status.selectedBackend, 'legacy-conversation-memory'); assert.equal(mem0.available, false); assert.equal(mem0.reason, 'init_failed'); assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy after mem0 error' }]); assert.match(logger.warnings[0], /mem0 backend unavailable/); }); test('createMemoryV2Runtime keeps letta as fallback when agent id is incomplete', async () => { const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy resolve with letta' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'letta', MEMORY_LETTA_ENABLED: '1', MEMORY_LETTA_API_KEY: 'secret', MEMORY_LETTA_PROJECT_ID: 'memind_project', }, }); const status = memory.getStatus(); const letta = status.backends.find((backend) => backend.name === 'letta'); 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(status.selectedBackend, 'legacy-conversation-memory'); assert.equal(letta.available, false); assert.equal(letta.reason, 'client_not_configured'); assert.equal(letta.category, 'lifecycle'); assert.equal(letta.flag, 'MEMORY_LETTA_ENABLED'); assert.deepEqual(resolved.memories, [{ label: 'fact', text: 'legacy resolve with letta' }]); assert.equal(written.source, 'legacy-conversation-memory'); assert.equal(compacted.source, 'legacy-conversation-memory'); }); test('createMemoryV2Runtime can select letta with default HTTP client', async () => { const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy letta fallback' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'letta', MEMORY_LETTA_ENABLED: '1', MEMORY_LETTA_API_KEY: 'secret', MEMORY_LETTA_PROJECT_ID: 'memind_project', MEMORY_LETTA_AGENT_ID: 'agent_1', MEMORY_LETTA_BASE_URL: 'https://letta.local', }, async fetchImpl(url, options) { assert.match(url, /^https:\/\/letta\.local\/v1\/agents\/agent_1\//); assert.equal(options.headers.authorization, 'Bearer secret'); if (url.endsWith('/memory')) { return { ok: true, async json() { return { memories: ['letta lifecycle memory'], activeGoals: ['memory-v2'] }; }, }; } return { ok: true, async json() { return { saved: 1, analyzed: 1, memories: 1 }; }, }; }, }); const status = memory.getStatus(); const letta = status.backends.find((backend) => backend.name === 'letta'); 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(status.selectedBackend, 'letta'); assert.equal(letta.available, true); assert.equal(resolved.source, 'letta'); assert.deepEqual(resolved.memories, [{ label: 'lifecycle', text: 'letta lifecycle memory' }]); assert.deepEqual(resolved.activeGoals, ['memory-v2']); assert.equal(written.source, 'letta'); assert.equal(compacted.source, 'letta'); }); test('createMemoryV2Runtime reports letta init failure without blocking memory', async () => { const logger = silentLogger(); const memory = await createMemoryV2Runtime({ logger, legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy after letta error' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'letta', MEMORY_LETTA_ENABLED: '1', MEMORY_LETTA_API_KEY: 'secret', MEMORY_LETTA_AGENT_ID: 'bad agent id', }, }); const status = memory.getStatus(); const letta = status.backends.find((backend) => backend.name === 'letta'); const result = await memory.resolve({ userId: 'u1' }); assert.equal(status.selectedBackend, 'legacy-conversation-memory'); assert.equal(letta.available, false); assert.equal(letta.reason, 'init_failed'); assert.deepEqual(result.memories, [{ label: 'fact', text: 'legacy after letta error' }]); assert.match(logger.warnings[0], /letta backend unavailable/); }); test('createMemoryV2Runtime wires remaining external adapters as safe fallbacks', async () => { const memory = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy external fallback' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'weaviate', MEMORY_WEAVIATE_ENABLED: '1', MEMORY_WEAVIATE_URL: 'https://weaviate.local', MEMORY_NEO4J_ENABLED: '1', MEMORY_NEO4J_URI: 'neo4j://localhost:7687', MEMORY_NEO4J_USER: 'neo4j', MEMORY_NEO4J_PASSWORD: 'secret', MEMORY_REDIS_STREAMS_ENABLED: '1', MEMORY_LANGGRAPH_ENABLED: '1', MEMORY_LANGGRAPH_POLICY_ID: 'memory_policy', }, }); const status = memory.getStatus(); const byName = new Map(status.backends.map((backend) => [backend.name, backend])); const resolved = await memory.resolve({ userId: 'u1', query: 'memory-chain' }); const written = await memory.write({ userId: 'u1', sessionId: 's1', messages: [] }); assert.equal(status.selectedBackend, 'legacy-conversation-memory'); assert.equal(byName.get('weaviate').reason, 'client_not_configured'); assert.equal(byName.get('neo4j').reason, 'client_not_configured'); assert.equal(byName.get('redis-streams').reason, 'url_not_configured'); assert.equal(byName.get('langgraph').reason, 'client_not_configured'); assert.deepEqual(resolved.memories, [{ label: 'fact', text: 'legacy external fallback' }]); assert.equal(written.source, 'legacy-conversation-memory'); }); test('createMemoryV2Runtime can select external adapters with built-in HTTP clients', async () => { const weaviate = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService([{ label: 'fact', text: 'legacy weaviate fallback' }]), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'weaviate', MEMORY_WEAVIATE_ENABLED: '1', MEMORY_WEAVIATE_URL: 'https://weaviate.local', MEMORY_WEAVIATE_EMBEDDING_MODULE: './weaviate-embed.mjs', }, async importModule() { return { async embedQuery() { return [0.1, 0.2]; } }; }, async fetchImpl(url, options) { assert.equal(url, 'https://weaviate.local/v1/graphql'); assert.equal(options.method, 'POST'); return { ok: true, async json() { return { data: { Get: { MemindMemory: [{ content: 'weaviate selected memory', _additional: { id: 'w1', certainty: 0.9 }, }], }, }, }; }, }; }, }); assert.equal(weaviate.getStatus().selectedBackend, 'weaviate'); assert.equal((await weaviate.resolve({ userId: 'u1', query: 'memory-chain' })).source, 'weaviate'); const langGraph = await createMemoryV2Runtime({ logger: silentLogger(), legacyMemoryService: legacyService(), env: { MEMORY_ENABLED: '1', MEMORY_BACKEND: 'langgraph', MEMORY_LANGGRAPH_ENABLED: '1', MEMORY_LANGGRAPH_POLICY_ID: 'memory_policy', MEMORY_LANGGRAPH_URL: 'https://langgraph.local', }, async fetchImpl(url, options) { assert.equal(url, 'https://langgraph.local/memory/resolve'); assert.equal(options.method, 'POST'); return { ok: true, async json() { return { activeGoals: ['route-memory'] }; }, }; }, }); assert.equal(langGraph.getStatus().selectedBackend, 'langgraph'); assert.deepEqual((await langGraph.resolve({ userId: 'u1' })).activeGoals, ['route-memory']); });