130 lines
5.0 KiB
JavaScript
130 lines
5.0 KiB
JavaScript
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']);
|
|
});
|