Files
memind/memory-v2-runtime.test.mjs
T
john 1d165bc6e3 feat(goose): complete v1.49 phase3 closeout gates and context fusion plan
Expand Goose v1.49 smoke coverage (memory chat, portal resume, page e2e,
multiturn provider), add canary memory policy lock, refresh baselines, and
ignore one-off evidence artifacts. Document headroom-based context runtime
fusion plan; include auth, scheduled-task, and wechat intent fixes on branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 18:11:39 +08:00

927 lines
32 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createManagedMemoryV2Runtime, createMemoryV2Runtime } from './memory-v2-runtime.mjs';
import { assertGooseCanaryMemoryStatus } from './goose-canary-memory-policy.mjs';
test('local Goose canary overrides admin memory settings and restores them when disabled', async () => {
const env = { TKMIND_GOOSE_CANARY: 'all', MEMORY_ENABLED: '1' };
const overrides = {
MEMORY_BACKEND: 'pgvector', MEMORY_VECTOR_ENABLED: '1',
MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'active',
MEMORY_AGENT_RESOLVE_ENABLED: '1', MEMORY_AGENT_INJECTION_MODE: 'active',
MEMORY_LIFECYCLE_ENABLED: '1', MEMORY_LIFECYCLE_WORKER_ENABLED: '1',
MEMORY_LIFECYCLE_ROLLOUT_MODE: 'active', MEMORY_PROMOTION_ENABLED: '1',
};
const runtime = await createManagedMemoryV2Runtime({
env, legacyMemoryService: legacyService(), logger: silentLogger(),
configService: { async getRuntimeState() { return { source: 'admin-db', fingerprint: 'same', overrides }; } },
});
try {
const status = await runtime.getStatus();
assertGooseCanaryMemoryStatus(status);
assert.equal(status.configSource, 'goose-canary:admin-db');
assert.equal(overrides.MEMORY_BACKEND, 'pgvector');
env.TKMIND_GOOSE_CANARY = 'off';
const restored = await runtime.getStatus();
assert.equal(restored.backend, 'pgvector');
assert.equal(restored.configSource, 'admin-db');
assert.throws(() => assertGooseCanaryMemoryStatus(restored), /requires legacy/);
} finally { await runtime.close(); }
});
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.ok(
calls.some((call) => call.sql.startsWith('INSERT')),
`expected candidate INSERT, got: ${calls.map((call) => call.sql.slice(0, 40)).join(' | ')}`,
);
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, 2);
const semanticQuery = queries.find((entry) => /recent_candidates/.test(entry.sql));
assert.ok(semanticQuery);
assert.equal(semanticQuery.options.connectionString, 'postgresql://local/memory');
assert.equal(semanticQuery.options.max, 2);
assert.match(semanticQuery.sql, /recent_candidates/);
assert.deepEqual(semanticQuery.params, ['u1', '[0.1,0.2,0.3]', 50, 0.15, 200]);
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']);
});