import test from 'node:test'; import assert from 'node:assert/strict'; import { createMemoryV2LifecycleService, resolveMemoryV2LifecyclePolicy, resolveMemoryV2LifecycleWorkerScopes, } from './memory-v2-lifecycle.mjs'; test('lifecycle policy defaults to safe disabled workers', () => { const policy = resolveMemoryV2LifecyclePolicy({}); assert.equal(policy.enabled, false); assert.equal(policy.promotionEnabled, false); assert.equal(policy.compactionEnabled, false); assert.equal(policy.reflectionEnabled, false); assert.equal(policy.rolloutMode, 'off'); }); test('lifecycle canary only runs for configured users', async () => { const lifecycle = createMemoryV2LifecycleService({ env: { MEMORY_LIFECYCLE_ENABLED: '1', MEMORY_LIFECYCLE_ROLLOUT_MODE: 'canary', MEMORY_LIFECYCLE_ROLLOUT_USER_IDS: 'u-1', MEMORY_PROMOTION_ENABLED: '1', }, }); assert.equal(lifecycle.canRun('u-1'), true); assert.equal(lifecycle.canRun('u-2'), false); assert.equal((await lifecycle.promote({ userId: 'u-2' })).skipped, true); }); test('lifecycle worker scopes match off, canary, and active rollout modes', () => { assert.deepEqual(resolveMemoryV2LifecycleWorkerScopes({ enabled: true, rolloutMode: 'off' }), []); assert.deepEqual(resolveMemoryV2LifecycleWorkerScopes({ enabled: true, rolloutMode: 'canary', rolloutUserIds: ['u-1', 'u-1', 'u-2'], }), ['u-1', 'u-2']); assert.deepEqual(resolveMemoryV2LifecycleWorkerScopes({ enabled: true, rolloutMode: 'active' }), [null]); }); test('forget is fail-safe when lifecycle is disabled', async () => { const lifecycle = createMemoryV2LifecycleService({ env: { MEMORY_LIFECYCLE_ENABLED: '0' } }); assert.deepEqual(await lifecycle.forgetMemory({ userId: 'u-1', memoryId: 'm-1' }), { ok: true, updated: false, skipped: true, reason: 'disabled', }); }); test('list and forget use user-scoped SQL', async () => { const calls = []; const pool = { query: async (sql, params) => { calls.push({ sql, params }); if (sql.startsWith('SELECT')) return [[{ id: 'm-1', user_id: 'u-1', label: 'fact', memory_text: 'x', status: 'active', confidence: 0.9, created_at: 1, updated_at: 2 }]]; return [{ affectedRows: 1 }]; } }; const lifecycle = createMemoryV2LifecycleService({ pool, env: { MEMORY_LIFECYCLE_ENABLED: '1', MEMORY_LIFECYCLE_FORGETTING_ENABLED: '1', MEMORY_LIFECYCLE_ROLLOUT_MODE: 'active', } }); assert.equal((await lifecycle.listMemories({ userId: 'u-1' }))[0].id, 'm-1'); assert.equal((await lifecycle.forgetMemory({ userId: 'u-1', memoryId: 'm-1' })).updated, true); assert.match(calls[0].sql, /user_id = \?/); assert.match(calls[1].sql, /user_id = \?/); }); test('forget and expire never mutate data while lifecycle rollout is off', async () => { const calls = []; const pool = { query: async (...args) => { calls.push(args); return [{ affectedRows: 1 }]; } }; const lifecycle = createMemoryV2LifecycleService({ pool, env: { MEMORY_LIFECYCLE_ENABLED: '1', MEMORY_LIFECYCLE_FORGETTING_ENABLED: '1', MEMORY_LIFECYCLE_ROLLOUT_MODE: 'off', } }); assert.equal((await lifecycle.forgetMemory({ userId: 'u-1', memoryId: 'm-1' })).skipped, true); assert.equal((await lifecycle.expire()).skipped, true); assert.equal(calls.length, 0); }); test('expire is limited to the configured canary user', async () => { const calls = []; const pool = { query: async (sql, params) => { calls.push({ sql, params }); return [{ affectedRows: 2 }]; } }; const lifecycle = createMemoryV2LifecycleService({ pool, now: () => 1_000_000, env: { MEMORY_LIFECYCLE_ENABLED: '1', MEMORY_LIFECYCLE_FORGETTING_ENABLED: '1', MEMORY_LIFECYCLE_ROLLOUT_MODE: 'canary', MEMORY_LIFECYCLE_ROLLOUT_USER_IDS: 'u-1', MEMORY_POLICY_RETENTION_DAYS: '1', } }); assert.equal((await lifecycle.expire({ userId: 'u-2' })).skipped, true); assert.equal((await lifecycle.expire({ userId: 'u-1' })).expired, 2); assert.equal(calls.length, 1); assert.match(calls[0].sql, /user_id = \?/); assert.equal(calls[0].params.at(-1), 'u-1'); }); test('promotion reports the users whose memories were inserted', async () => { const pool = { 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 }]; } throw new Error(`Unexpected SQL: ${sql}`); }, }; const lifecycle = createMemoryV2LifecycleService({ pool, env: { MEMORY_LIFECYCLE_ENABLED: '1', MEMORY_PROMOTION_ENABLED: '1', MEMORY_LIFECYCLE_ROLLOUT_MODE: 'canary', MEMORY_LIFECYCLE_ROLLOUT_USER_IDS: 'u-1', } }); const result = await lifecycle.promote({ userId: 'u-1' }); assert.equal(result.promoted, 1); assert.deepEqual(result.promotedUserIds, ['u-1']); });