import assert from 'node:assert/strict'; import test from 'node:test'; import { inspectMemoryV2Backend } from './memory-v2-backend-contract.mjs'; import { createLangGraphHttpClient, createLangGraphMemoryBackend } from './memory-v2-langgraph.mjs'; import { createNeo4jHttpClient, createNeo4jMemoryBackend } from './memory-v2-neo4j.mjs'; import { createRedisStreamsClient, createRedisStreamsMemoryBackend, } from './memory-v2-redis-streams.mjs'; import { createWeaviateHttpClient, createWeaviateMemoryBackend } from './memory-v2-weaviate.mjs'; test('weaviate backend is disabled by default and delegates only with client plus embedding', async () => { const disabled = createWeaviateMemoryBackend(); assert.equal(disabled.name, 'weaviate'); assert.equal(disabled.category, 'semantic'); assert.equal(disabled.role, 'knowledge-graph-fusion'); assert.equal(disabled.flag, 'MEMORY_WEAVIATE_ENABLED'); assert.equal(disabled.isAvailable(), false); assert.equal(disabled.getUnavailableReason(), 'not_configured'); assert.equal(inspectMemoryV2Backend(disabled).ok, true); const backend = createWeaviateMemoryBackend({ enabled: true, url: 'https://weaviate.local', collection: 'MemindMemory', async embedQuery(query) { assert.equal(query, 'memory-chain'); return [0.1, 0.2]; }, client: { async search(input) { assert.equal(input.collection, 'MemindMemory'); assert.deepEqual(input.vector, [0.1, 0.2]); return [{ id: 'w1', type: 'semantic', content: 'weaviate memory', score: 0.8 }]; }, }, }); const result = await backend.resolve({ userId: 'u1', query: 'memory-chain' }); assert.equal(backend.isAvailable(), true); assert.deepEqual(result.semanticMemories, ['weaviate memory']); }); test('weaviate backend validates url and collection', () => { assert.throws( () => createWeaviateMemoryBackend({ enabled: true, url: 'ftp://weaviate.local' }), /Invalid MEMORY_WEAVIATE_URL/, ); assert.throws( () => createWeaviateMemoryBackend({ collection: 'bad collection' }), /Invalid MEMORY_WEAVIATE_COLLECTION/, ); }); test('weaviate HTTP client performs GraphQL vector search', async () => { const client = createWeaviateHttpClient({ url: 'https://weaviate.local', apiKey: 'secret', async fetchImpl(url, options) { const body = JSON.parse(options.body); assert.equal(url, 'https://weaviate.local/v1/graphql'); assert.equal(options.headers.authorization, 'Bearer secret'); assert.deepEqual(body.variables.where, { path: ['user_id'], operator: 'Equal', valueText: 'u1', }); assert.match(body.query, /limit:\s*2/); return { ok: true, async json() { return { data: { Get: { MemindMemory: [{ content: 'weaviate memory', type: 'semantic', _additional: { id: 'w1', certainty: 0.91 }, }], }, }, }; }, }; }, }); const result = await client.search({ collection: 'MemindMemory', vector: [0.1, 0.2], userId: 'u1', limit: 2, }); assert.deepEqual(result, [{ id: 'w1', label: 'semantic', text: 'weaviate memory', score: 0.91, createdAt: undefined, }]); }); test('neo4j backend is disabled by default and delegates only with explicit client', async () => { const disabled = createNeo4jMemoryBackend(); assert.equal(disabled.name, 'neo4j'); assert.equal(disabled.category, 'behavior'); assert.equal(disabled.role, 'behavior-graph'); assert.equal(disabled.flag, 'MEMORY_NEO4J_ENABLED'); assert.equal(disabled.isAvailable(), false); assert.equal(disabled.getUnavailableReason(), 'not_configured'); assert.equal(inspectMemoryV2Backend(disabled).ok, true); const backend = createNeo4jMemoryBackend({ enabled: true, uri: 'neo4j://localhost:7687', username: 'neo4j', password: 'secret', client: { async resolve() { return { behaviorSummary: 'prefers careful rollouts' }; }, async write() { return { saved: 1, analyzed: 0, memories: 0 }; }, }, }); assert.equal(backend.isAvailable(), true); assert.equal((await backend.resolve({ userId: 'u1' })).behaviorSummary, 'prefers careful rollouts'); assert.deepEqual(await backend.write({ userId: 'u1' }), { saved: 1, analyzed: 0, memories: 0 }); }); test('neo4j HTTP client writes and resolves through transactional endpoint', async () => { const calls = []; const client = createNeo4jHttpClient({ httpUrl: 'http://neo4j.local:7474', username: 'neo4j', password: 'secret', async fetchImpl(url, options) { calls.push({ url, body: JSON.parse(options.body), headers: options.headers }); return { ok: true, async json() { if (calls.length === 1) return { results: [{ data: [{ row: [1] }] }] }; return { results: [{ data: [{ row: ['prefers careful rollouts'] }] }] }; }, }; }, }); assert.deepEqual(await client.write({ userId: 'u1', sessionId: 's1', messages: [{ text: 'hello' }], }), { saved: 1, analyzed: 0, memories: 0 }); assert.equal((await client.resolve({ userId: 'u1' })).behaviorSummary, 'prefers careful rollouts'); assert.equal(calls[0].url, 'http://neo4j.local:7474/db/neo4j/tx/commit'); assert.match(calls[0].headers.authorization, /^Basic /); assert.match(calls[0].body.statements[0].statement, /CREATE \(e:MemoryEvent/); }); test('redis-streams backend is write-only and client-injected', async () => { const disabled = createRedisStreamsMemoryBackend(); assert.equal(disabled.name, 'redis-streams'); assert.equal(disabled.category, 'behavior'); assert.equal(disabled.role, 'event-tracking'); assert.equal(disabled.flag, 'MEMORY_REDIS_STREAMS_ENABLED'); assert.equal(disabled.isAvailable(), false); assert.equal(disabled.getUnavailableReason(), 'not_configured'); assert.equal(inspectMemoryV2Backend(disabled).ok, true); assert.equal(typeof disabled.resolve, 'undefined'); const backend = createRedisStreamsMemoryBackend({ enabled: true, url: 'redis://localhost:6379', stream: 'memind:memory-events', client: { async write(input) { assert.equal(input.stream, 'memind:memory-events'); return { saved: 1, analyzed: 0, memories: 0 }; }, }, }); assert.equal(backend.isAvailable(), true); assert.deepEqual(await backend.write({ userId: 'u1' }), { saved: 1, analyzed: 0, memories: 0 }); }); test('redis streams client connects lazily, xadds event, and closes', async () => { const calls = []; const client = await createRedisStreamsClient({ url: 'redis://localhost:6379', async importRedis() { return { createClient(options) { calls.push(['createClient', options.url]); return { async connect() { calls.push(['connect']); }, async xAdd(stream, id, fields) { calls.push(['xAdd', stream, id, fields.user_id]); }, async quit() { calls.push(['quit']); }, }; }, }; }, }); assert.deepEqual(await client.write({ userId: 'u1', sessionId: 's1', messages: [] }), { saved: 1, analyzed: 0, memories: 0, }); await client.close(); assert.deepEqual(calls.map((call) => call.slice(0, 3)), [ ['createClient', 'redis://localhost:6379'], ['connect'], ['xAdd', 'memind:memory-events', '*'], ['quit'], ]); }); test('langgraph backend is disabled by default and delegates policy resolve only with client', async () => { const disabled = createLangGraphMemoryBackend(); assert.equal(disabled.name, 'langgraph'); assert.equal(disabled.category, 'policy'); assert.equal(disabled.role, 'routing-reasoning-policy'); assert.equal(disabled.flag, 'MEMORY_LANGGRAPH_ENABLED'); assert.equal(disabled.isAvailable(), false); assert.equal(disabled.getUnavailableReason(), 'not_configured'); assert.equal(inspectMemoryV2Backend(disabled).ok, true); const backend = createLangGraphMemoryBackend({ enabled: true, policyId: 'memory_policy', client: { async resolve(input) { assert.equal(input.policyId, 'memory_policy'); return { activeGoals: ['route-memory'] }; }, }, }); assert.equal(backend.isAvailable(), true); assert.deepEqual((await backend.resolve({ userId: 'u1' })).activeGoals, ['route-memory']); }); test('langgraph HTTP client posts resolve payload', async () => { const client = createLangGraphHttpClient({ baseUrl: 'https://langgraph.local', apiKey: 'secret', modelProviderKeyId: 'key_langgraph', model: 'gpt-4.1-nano', modelApiType: 'response', async fetchImpl(url, options) { assert.equal(url, 'https://langgraph.local/memory/resolve'); assert.equal(options.headers.authorization, 'Bearer secret'); assert.deepEqual(JSON.parse(options.body), { user_id: 'u1', session_id: 's1', query: 'hello', policy_id: 'memory_policy', limit: 3, model_provider_key_id: 'key_langgraph', model: 'gpt-4.1-nano', model_api_type: 'response', }); return { ok: true, async json() { return { activeGoals: ['route-memory'] }; }, }; }, }); assert.deepEqual(await client.resolve({ userId: 'u1', sessionId: 's1', query: 'hello', policyId: 'memory_policy', limit: 3, }), { activeGoals: ['route-memory'] }); }); test('external adapter skeletons validate unsafe config values', () => { assert.throws( () => createNeo4jMemoryBackend({ uri: 'http://localhost:7474' }), /Invalid MEMORY_NEO4J_URI/, ); assert.throws( () => createRedisStreamsMemoryBackend({ url: 'http://localhost:6379' }), /Invalid MEMORY_REDIS_STREAMS_URL/, ); assert.throws( () => createRedisStreamsMemoryBackend({ stream: 'bad stream' }), /Invalid MEMORY_REDIS_STREAMS_STREAM/, ); assert.throws( () => createLangGraphMemoryBackend({ policyId: 'bad policy id' }), /Invalid MEMORY_LANGGRAPH_POLICY_ID/, ); });