Merge Memory V2 runtime facade
This commit is contained in:
@@ -0,0 +1,676 @@
|
||||
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']);
|
||||
});
|
||||
Reference in New Issue
Block a user