import { describe, test } from 'claude-code/testing' import assert from './assert' import { memoryFs } from './io' import { CONSOLIDATE_THRESHOLD, FORGET_BELOW, TOP_K, createMemory, redact, retention, stability } from './memory' const H = 3600_000 const T0 = Date.UTC(2026, 9, 12, 10, 0) function setup() { let now = T0 const fs = memoryFs() const mem = createMemory(fs, () => now, 'mem.json') return { mem, fs, advance: (ms: number) => void (now += ms), now: () => now } } describe('記憶檢索', () => { test('相關性高者排前面', async () => { const s = setup() await s.mem.add({ layer: 'semantic', text: '使用者喜歡抹茶拿鐵', importance: 5, source: 'u' }) await s.mem.add({ layer: 'semantic', text: '使用者養了一隻貓', importance: 5, source: 'u' }) const r = await s.mem.recall('抹茶') assert.equal(r[0]!.text, '使用者喜歡抹茶拿鐵') }) test('較新、較重要者在其他條件相同時勝出', async () => { const s = setup() const old = await s.mem.add({ layer: 'episodic', text: '討論部署', importance: 5, source: 'u' }) s.advance(100 * H) const fresh = await s.mem.add({ layer: 'episodic', text: '討論部署', importance: 5, source: 'u' }) assert.equal((await s.mem.recall('部署'))[0]!.id, fresh.id) assert.notEqual(old.id, fresh.id) const s2 = setup() await s2.mem.add({ layer: 'episodic', text: '討論測試', importance: 2, source: 'u' }) const hi = await s2.mem.add({ layer: 'episodic', text: '討論測試', importance: 9, source: 'u' }) assert.equal((await s2.mem.recall('測試'))[0]!.id, hi.id) }) test('情緒吻合度影響排序', async () => { const s = setup() await s.mem.add({ layer: 'emotional', text: '一起慶祝', importance: 5, source: 'u', emotion: { p: -0.8, a: 0.5, d: -0.5 } }) const happy = await s.mem.add({ layer: 'emotional', text: '一起慶祝', importance: 5, source: 'u', emotion: { p: 0.8, a: 0.5, d: 0.5 } }) assert.equal((await s.mem.recall('慶祝', { p: 0.7, a: 0.4, d: 0.4 }))[0]!.id, happy.id) }) test('最多取前 6 筆,並累計取用次數', async () => { const s = setup() for (let i = 0; i < 10; i++) await s.mem.add({ layer: 'semantic', text: `筆記${i}`, importance: 5, source: 'u' }) const r = await s.mem.recall('筆記') assert.equal(r.length, TOP_K) assert.ok((await s.mem.all()).filter(m => m.accessCount === 1).length === TOP_K) }) test('寫入前過濾疑似金鑰', async () => { const s = setup() const m = await s.mem.add({ layer: 'episodic', text: '金鑰是 sk-abcdefghijklmnop1234 請記住', importance: 3, source: 'u' }) assert.ok(!m.text.includes('sk-abcdef')) assert.ok(m.text.includes('[已過濾]')) assert.equal(redact('普通的一句話'), '普通的一句話') }) }) describe('遺忘曲線', () => { test('保留率隨時間下降,重要度與取用次數使其變慢', () => { const base = { id: 1, layer: 'episodic' as const, text: '', source: '', createdAt: T0, lastAccess: T0 } const weak = { ...base, importance: 2, accessCount: 0 } const strong = { ...base, importance: 8, accessCount: 0 } const used = { ...weak, accessCount: 5 } assert.ok(retention(weak, T0 + 24 * H) < retention(weak, T0)) assert.ok(retention(strong, T0 + 100 * H) > retention(weak, T0 + 100 * H)) assert.ok(retention(used, T0 + 100 * H) > retention(weak, T0 + 100 * H)) assert.ok(stability(strong) > stability(weak)) }) test('鞏固時淘汰低保留率的記憶,保留重要與常用的', async () => { const s = setup() const trivial = await s.mem.add({ layer: 'episodic', text: '隨口聊天', importance: 1, source: 'u' }) const key = await s.mem.add({ layer: 'semantic', text: '生日是三月', importance: 9, source: 'u' }) s.advance(10 * 24 * H) assert.ok(retention(trivial, s.now()) < FORGET_BELOW) const r = await s.mem.consolidate({ force: true }) assert.equal(r.forgotten, 1) const ids = (await s.mem.all()).map(m => m.id) assert.ok(!ids.includes(trivial.id)) assert.ok(ids.includes(key.id)) }) test('取用過的記憶會重置時效而不被淘汰', async () => { const s = setup() const m = await s.mem.add({ layer: 'episodic', text: '常提起的事', importance: 2, source: 'u' }) s.advance(40 * H) await s.mem.recall('常提起') s.advance(40 * H) await s.mem.consolidate({ force: true }) assert.ok((await s.mem.all()).some(x => x.id === m.id)) }) }) describe('鞏固與反思', () => { test('累積重要度未達門檻不執行,達門檻才執行並歸零', async () => { const s = setup() await s.mem.add({ layer: 'episodic', text: '小事', importance: 5, source: 'u' }) assert.deepEqual(await s.mem.consolidate(), { ran: false, forgotten: 0 }) for (let i = 0; i < 5; i++) await s.mem.add({ layer: 'episodic', text: `事件${i}`, importance: 5, source: 'u' }) assert.ok((await s.mem.pending()) >= CONSOLIDATE_THRESHOLD) const r = await s.mem.consolidate() assert.equal(r.ran, true) assert.equal(await s.mem.pending(), 0) assert.equal(r.reflection?.layer, 'reflection') }) test('手動 reflect(force)即使未達門檻也執行,並使用注入的摘要', async () => { const s = setup() await s.mem.add({ layer: 'episodic', text: '一起修好了建置', importance: 4, source: 'u' }) const r = await s.mem.consolidate({ force: true, summarize: items => `反思:${items.length} 件事` }) assert.equal(r.ran, true) assert.equal(r.reflection?.text, '反思:1 件事') assert.ok(r.reflection!.source.startsWith('reflect:')) }) test('反思不重複涵蓋舊事件,也不遞迴反思反思', async () => { const s = setup() await s.mem.add({ layer: 'episodic', text: '甲', importance: 4, source: 'u' }) await s.mem.consolidate({ force: true }) s.advance(H) await s.mem.add({ layer: 'episodic', text: '乙', importance: 4, source: 'u' }) const r = await s.mem.consolidate({ force: true, summarize: items => items.map(i => i.text).join('+') }) assert.equal(r.reflection?.text, '乙') }) test('沒有新記憶時鞏固不產生空反思', async () => { const s = setup() const r = await s.mem.consolidate({ force: true }) assert.equal(r.reflection, undefined) }) test('資料經檔案持久化,重建後可繼續', async () => { const s = setup() await s.mem.add({ layer: 'semantic', text: '喜歡貓', importance: 5, source: 'u' }) const again = createMemory(s.fs, s.now, 'mem.json') assert.equal((await again.recall('貓'))[0]!.text, '喜歡貓') }) })