Files
memind/memory-v2-runtime.test.mjs
john c2e2209344
Memind CI / Test, build, and release guards (push) Successful in 2m31s
fix(memory): sync new memories into pgvector
2026-07-21 23:30:18 +08:00

894 lines
30 KiB
JavaScript

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 ensures candidate schema before enabling MySQL persistence', async () => {
const calls = [];
const mysqlPool = {
async query(sql, params = []) {
calls.push({ sql, params });
if (sql.startsWith('CREATE TABLE')) return [[], []];
if (sql.startsWith('INSERT')) return [{ affectedRows: 1 }, []];
throw new Error(`Unexpected SQL: ${sql}`);
},
};
const memory = await createMemoryV2Runtime({
logger: silentLogger(),
legacyMemoryService: legacyService(),
mysqlPool,
env: {
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'shadow',
MEMORY_CANDIDATE_PERSISTENCE_ENABLED: '1',
},
});
assert.equal(memory.getStatus().personalMemory.persistence, 'mysql');
assert.match(calls[0].sql, /CREATE TABLE IF NOT EXISTS `h5_memory_v2_candidates`/);
const observed = await memory.observePersonalMemory({
userId: 'u-1',
sessionId: 's-1',
messages: [{ role: 'user', content: '请记住我偏好使用简洁的中文回答' }],
});
assert.equal(observed.accepted, 1);
assert.equal(calls[1].sql.startsWith('INSERT'), true);
await memory.close();
});
test('createMemoryV2Runtime fails open when candidate schema initialization fails', async () => {
const logger = silentLogger();
const memory = await createMemoryV2Runtime({
logger,
legacyMemoryService: legacyService(),
mysqlPool: { async query() { throw new Error('ddl denied'); } },
env: {
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'shadow',
MEMORY_CANDIDATE_PERSISTENCE_ENABLED: '1',
},
});
assert.equal(memory.getStatus().personalMemory.persistence, 'bounded-memory');
assert.match(logger.warnings[0], /candidate persistence unavailable: ddl denied/);
await memory.close();
});
test('createMemoryV2Runtime syncs a successful user memory write into pgvector before returning', async () => {
const mysqlCalls = [];
const pgCalls = [];
class FakePool {
async query(sql, params) {
pgCalls.push({ sql, params });
return { rows: [] };
}
async end() {}
}
const mysqlPool = {
async query(sql, params) {
mysqlCalls.push({ sql, params });
if (sql.includes('FROM h5_user_memory_items')) {
return [[{
id: 'mem-new',
user_id: 'u-1',
label: 'fact',
memory_text: '灰度代号 MEM-NEW',
evidence_message_id: 'msg-1',
source_session_id: 's-new',
confidence: 0.9,
status: 'active',
created_at: 100,
updated_at: 200,
}]];
}
throw new Error(`Unexpected MySQL query: ${sql}`);
},
};
const memory = await createMemoryV2Runtime({
logger: silentLogger(),
mysqlPool,
legacyMemoryService: legacyService(),
env: {
MEMORY_ENABLED: '1',
MEMORY_EVENT_LOG_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
MEMORY_VECTOR_ENABLED: '1',
MEMORY_PGVECTOR_DATABASE_URL: 'postgresql://local/memory',
MEMORY_PGVECTOR_EMBEDDING_MODULE: './fake-embed.mjs',
},
async importPg() { return { Pool: FakePool }; },
async importModule() { return { embedQuery: async () => [0.1, 0.2] }; },
});
const result = await memory.write({ userId: 'u-1', sessionId: 's-new', messages: [] });
assert.equal(result.memories, 1);
assert.match(mysqlCalls[0].sql, /user_id = \?/);
assert.match(mysqlCalls[0].sql, /source_session_id = \?/);
assert.deepEqual(mysqlCalls[0].params, ['u-1', 's-new', 50]);
const insert = pgCalls.find((call) => call.sql.includes('INSERT INTO "memory_embeddings"'));
assert.ok(insert);
assert.equal(insert.params[1], '灰度代号 MEM-NEW');
await memory.close();
});
test('createMemoryV2Runtime retries scoped pgvector sync after a deduplicated memory write', async () => {
const pgCalls = [];
class FakePool {
async query(sql, params) {
pgCalls.push({ sql, params });
return { rows: [] };
}
async end() {}
}
const mysqlPool = {
async query(sql) {
if (sql.includes('FROM h5_user_memory_items')) {
return [[{
id: 'mem-existing',
user_id: 'u-1',
label: 'fact',
memory_text: '需要重试同步的既有记忆',
source_session_id: 's-existing',
confidence: 0.9,
created_at: 100,
updated_at: 200,
}]];
}
throw new Error(`Unexpected MySQL query: ${sql}`);
},
};
const memory = await createMemoryV2Runtime({
logger: silentLogger(),
mysqlPool,
legacyMemoryService: {
...legacyService(),
async saveAndAnalyze() {
return { saved: 0, analyzed: 0, memories: 0 };
},
},
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
MEMORY_VECTOR_ENABLED: '1',
MEMORY_PGVECTOR_DATABASE_URL: 'postgresql://local/memory',
MEMORY_PGVECTOR_EMBEDDING_MODULE: './fake-embed.mjs',
},
async importPg() { return { Pool: FakePool }; },
async importModule() { return { embedQuery: async () => [0.1, 0.2] }; },
});
const result = await memory.write({ userId: 'u-1', sessionId: 's-existing', messages: [] });
assert.equal(result.memories, 0);
const insert = pgCalls.find((call) => call.sql.includes('INSERT INTO "memory_embeddings"'));
assert.ok(insert);
assert.equal(insert.params[1], '需要重试同步的既有记忆');
await memory.close();
});
test('createMemoryV2Runtime syncs promoted canary candidates into pgvector', async () => {
const pgCalls = [];
class FakePool {
async query(sql, params) {
pgCalls.push({ sql, params });
return { rows: [] };
}
async end() {}
}
const mysqlPool = {
async query(sql) {
if (sql.startsWith('SELECT * FROM h5_memory_v2_candidates')) {
return [[{
user_id: 'u-1', memory_type: 'episodic', content: '候选灰度代号',
session_id: 's-1', confidence: 0.9, evidence_json: '{}', created_at: 100,
}]];
}
if (sql.startsWith('INSERT IGNORE INTO h5_user_memory_items')) return [{ affectedRows: 1 }];
if (sql.includes('FROM h5_user_memory_items')) {
return [[{
id: 'mem-promoted', user_id: 'u-1', label: 'episodic', memory_text: '候选灰度代号',
source_session_id: 's-1', confidence: 0.9, status: 'active', created_at: 100, updated_at: 200,
}]];
}
throw new Error(`Unexpected MySQL query: ${sql}`);
},
};
const memory = await createMemoryV2Runtime({
logger: silentLogger(),
mysqlPool,
legacyMemoryService: legacyService(),
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'pgvector',
MEMORY_VECTOR_ENABLED: '1',
MEMORY_PGVECTOR_DATABASE_URL: 'postgresql://local/memory',
MEMORY_PGVECTOR_EMBEDDING_MODULE: './fake-embed.mjs',
MEMORY_LIFECYCLE_ENABLED: '1',
MEMORY_PROMOTION_ENABLED: '1',
MEMORY_LIFECYCLE_ROLLOUT_MODE: 'canary',
MEMORY_LIFECYCLE_ROLLOUT_USER_IDS: 'u-1',
},
async importPg() { return { Pool: FakePool }; },
async importModule() { return { embedQuery: async () => [0.3, 0.4] }; },
});
const result = await memory.lifecycle.promote({ userId: 'u-1' });
assert.equal(result.promoted, 1);
assert.deepEqual(result.promotedUserIds, ['u-1']);
const insert = pgCalls.find((call) => call.sql.includes('INSERT INTO "memory_embeddings"'));
assert.ok(insert);
assert.equal(insert.params[1], '候选灰度代号');
await memory.close();
});
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']);
});