import assert from 'node:assert/strict'; import test from 'node:test'; import { createPersonalMemoryShadowPipeline, resolvePersonalShadowConfig, shouldAutoAcceptCandidate, } from './memory-v2-personal-shadow.mjs'; import { createMemoryV2 } from './memory-v2.mjs'; test('personal memory shadow config maps active mode to auto review', () => { const config = resolvePersonalShadowConfig({ MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'active', }); assert.equal(config.enabled, true); assert.equal(config.requestedMode, 'active'); assert.equal(config.effectiveMode, 'active'); assert.equal(config.autoReviewEnabled, true); }); test('personal memory shadow config keeps shadow mode for manual review', () => { const config = resolvePersonalShadowConfig({ MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'shadow', }); assert.equal(config.effectiveMode, 'shadow'); assert.equal(config.autoReviewEnabled, false); }); test('shouldAutoAcceptCandidate auto accepts explicit requests in canary mode', () => { const config = resolvePersonalShadowConfig({ MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'canary', MEMORY_CANDIDATE_MIN_CONFIDENCE: '0.8', }); assert.equal(shouldAutoAcceptCandidate({ policyReason: 'explicit_memory_request', confidence: 0.98, }, config), true); assert.equal(shouldAutoAcceptCandidate({ policyReason: 'stable_fact_signal', confidence: 0.82, }, config), false); assert.equal(shouldAutoAcceptCandidate({ policyReason: 'decision_signal', confidence: 0.9, }, config), true); }); test('shadow pipeline auto accepts durable candidates in active mode', async () => { const saved = []; const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'active', MEMORY_POLICY_ENABLED: '1', }, store: { async saveCandidate(candidate, options = {}) { saved.push({ candidate, options }); return { inserted: true, status: options.autoAccept ? 'accepted' : 'candidate' }; }, }, }); const result = await pipeline.observeWrite({ userId: 'u1', sessionId: 's1', messages: [{ role: 'user', text: '我决定所有生产发布必须从完整 main 分支打包。' }], }); assert.equal(result.autoReviewed, 1); assert.equal(result.pendingReview, 0); assert.equal(saved.length, 1); assert.equal(saved[0].options.autoAccept, true); assert.equal(pipeline.getStatus().autoReviewEnabled, true); assert.equal(pipeline.getStatus().phase, 'auto-review-v1'); }); test('shadow pipeline keeps low-confidence canary candidates pending review', async () => { const saved = []; const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'canary', MEMORY_CANDIDATE_MIN_CONFIDENCE: '0.8', }, store: { async saveCandidate(candidate, options = {}) { saved.push({ candidate, options }); return { inserted: true, status: options.autoAccept ? 'accepted' : 'candidate' }; }, }, }); await pipeline.observeWrite({ userId: 'u1', sessionId: 's1', messages: [{ role: 'user', text: '项目使用 pgvector 作为向量存储。' }], }); assert.equal(saved.length, 1); assert.equal(saved[0].options.autoAccept, false); assert.equal(pipeline.getStatus().health.pendingReview, 1); }); test('shadow pipeline extracts durable candidates with evidence and deduplicates', async () => { let clock = 1000; const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'shadow', MEMORY_POLICY_ENABLED: '1', MEMORY_POLICY_REQUIRE_EVIDENCE: '1', MEMORY_CANDIDATE_MIN_IMPORTANCE: '0.7', MEMORY_CANDIDATE_MIN_CONFIDENCE: '0.8', }, now: () => { clock += 1; return clock; }, }); const input = { userId: 'u1', sessionId: 's1', messages: [ { role: 'user', text: '我决定所有生产发布必须从完整 main 分支打包。' }, { role: 'assistant', text: '收到。' }, ], }; const first = await pipeline.observeWrite(input); const second = await pipeline.observeWrite(input); assert.equal(first.accepted, 1); assert.equal(second.accepted, 0); assert.equal(pipeline.listCandidates().length, 1); assert.equal(pipeline.listCandidates()[0].memoryType, 'episodic'); assert.equal(pipeline.listCandidates()[0].evidence.sourceId, 's1:0'); assert.equal(pipeline.getStatus().health.deduped, 1); assert.equal(pipeline.getStatus().injectionEnabled, false); }); test('shadow policy rejects trivial and sensitive messages', async () => { const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'shadow', MEMORY_POLICY_REJECT_SENSITIVE: '1', }, }); const result = await pipeline.observeWrite({ userId: 'u1', sessionId: 's1', messages: [ { role: 'user', text: '谢谢' }, { role: 'user', text: '请记住 API_KEY=sk-this-is-a-secret-token' }, { role: 'user', text: '今天天气怎么样?' }, ], }); assert.equal(result.accepted, 0); assert.equal(result.rejected, 3); assert.equal(pipeline.listCandidates().length, 0); }); test('shadow pipeline prefers user-facing displayText over internal prompt content', async () => { const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'shadow' }, }); await pipeline.observeWrite({ userId: 'u1', sessionId: 's1', messages: [{ role: 'user', content: [{ type: 'text', text: '[用户身份] 内部前缀\n我决定先测试再发布。' }], metadata: { displayText: '我决定先测试再发布。' }, }], }); assert.equal(pipeline.listCandidates().length, 1); assert.equal(pipeline.listCandidates()[0].content, '我决定先测试再发布。'); assert.doesNotMatch(pipeline.listCandidates()[0].content, /用户身份/); }); test('shadow pipeline strips protected user identity prefix when displayText is absent', async () => { const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'shadow' }, }); await pipeline.observeWrite({ userId: 'u1', sessionId: 's1', messages: [{ role: 'user', content: [{ type: 'text', text: '[用户身份]\n- 当前登录用户称呼:John\n- 内部提示\n\n我决定先测试再发布。', }], }], }); assert.equal(pipeline.listCandidates().length, 1); assert.equal(pipeline.listCandidates()[0].content, '我决定先测试再发布。'); }); test('shadow candidate pool remains bounded', async () => { const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'shadow', MEMORY_CANDIDATE_MAX_PENDING: '2', }, }); await pipeline.observeWrite({ userId: 'u1', sessionId: 's1', messages: [ { role: 'user', text: '我决定项目一使用完整 main 发布。' }, { role: 'user', text: '我决定项目二使用完整 main 发布。' }, { role: 'user', text: '我决定项目三使用完整 main 发布。' }, ], }); assert.equal(pipeline.listCandidates().length, 2); assert.match(pipeline.listCandidates()[0].content, /项目三/); }); test('shadow pipeline persists accepted candidates when a store is configured', async () => { const saved = []; const pipeline = createPersonalMemoryShadowPipeline({ env: { MEMORY_CANDIDATE_ENABLED: '1', MEMORY_CANDIDATE_MODE: 'shadow' }, store: { async saveCandidate(candidate) { saved.push(candidate); return { inserted: true }; } }, }); await pipeline.observeWrite({ userId: 'u1', sessionId: 's1', messages: [{ role: 'user', text: '我的长期目标是建设一个持续成长的 Personal Agent。' }], }); assert.equal(saved.length, 1); assert.equal(saved[0].memoryType, 'goal'); assert.equal(pipeline.getStatus().persistence, 'mysql'); }); test('Memory V2 does not wait for the shadow pipeline before returning legacy write result', async () => { let releaseShadow; const shadowBlocked = new Promise((resolve) => { releaseShadow = resolve; }); const memory = createMemoryV2({ policy: { enabled: true, eventLogEnabled: true, failOpen: true, backend: 'legacy' }, backends: [{ name: 'legacy-conversation-memory', isAvailable: () => true, async write() { return { saved: 1, analyzed: 0, memories: 0 }; }, }], personalShadowPipeline: { config: { enabled: true }, observeWrite: () => shadowBlocked, getStatus: () => ({ enabled: true }), }, }); const result = await Promise.race([ memory.write({ userId: 'u1', sessionId: 's1', messages: [] }), new Promise((_, reject) => setTimeout(() => reject(new Error('write waited for shadow')), 50)), ]); assert.equal(result.saved, 1); releaseShadow(); }); test('Memory V2 exposes a shadow-only observation entry without calling legacy write', async () => { let legacyWrites = 0; const observed = []; const memory = createMemoryV2({ policy: { enabled: true, eventLogEnabled: true, failOpen: true, backend: 'legacy' }, backends: [{ name: 'legacy-conversation-memory', isAvailable: () => true, async write() { legacyWrites += 1; return { saved: 1 }; }, }], personalShadowPipeline: { config: { enabled: true }, async observeWrite(input) { observed.push(input); return { accepted: 1 }; }, getStatus: () => ({ enabled: true }), }, }); const result = await memory.observePersonalMemory({ userId: 'u1', sessionId: 's1', messages: [{ role: 'user', text: '我决定先完成测试。' }], }); assert.equal(result.accepted, 1); assert.equal(observed.length, 1); assert.equal(legacyWrites, 0); });