fix(memory): initialize candidates and enforce lifecycle rollout
Memind CI / Test, build, and release guards (push) Successful in 3m29s

This commit is contained in:
john
2026-07-21 22:55:19 +08:00
parent 0dd9331e5b
commit bfb1f6fea9
8 changed files with 207 additions and 14 deletions
+52 -2
View File
@@ -1,6 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createMemoryV2LifecycleService, resolveMemoryV2LifecyclePolicy } from './memory-v2-lifecycle.mjs';
import {
createMemoryV2LifecycleService,
resolveMemoryV2LifecyclePolicy,
resolveMemoryV2LifecycleWorkerScopes,
} from './memory-v2-lifecycle.mjs';
test('lifecycle policy defaults to safe disabled workers', () => {
const policy = resolveMemoryV2LifecyclePolicy({});
@@ -25,6 +29,16 @@ test('lifecycle canary only runs for configured users', async () => {
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' }), {
@@ -39,9 +53,45 @@ test('list and forget use user-scoped SQL', async () => {
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' } });
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');
});