refactor: jsc-mod 改名 jsc-mod-statusline、jsc-companion 改名 jsc-mod-companion,指令拆成獨立指令
- jsc-mod-statusline:/jsc-mod-statusline:enable、compact、disable - jsc-mod-companion:/jsc-mod-companion:status、switch、disable、setup、todo、quiet - 同步更新 plugin.json、marketplace.json 與各 README - 補上 statusline band 測試缺少的 ui.render 底層處理 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
0e7636c185
commit
ea17f1cb2d
@@ -0,0 +1,143 @@
|
||||
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, '喜歡貓')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user