309 lines
10 KiB
JavaScript
309 lines
10 KiB
JavaScript
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/,
|
|
);
|
|
});
|