- 兩本帳(原典基準、存檔累積),日限、有效增量、期望違反公式 - 階段 陌生/熟人/朋友/好友,只升不降;14 天無互動每日 −0.5 - 舊 affinity 存檔讀檔時自動升級 - 版本 0.0.2 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
522 lines
20 KiB
TypeScript
522 lines
20 KiB
TypeScript
import { describe, expect, test } from 'claude-code/testing'
|
||
|
||
import { createCompanion } from './core'
|
||
import { fakeClock, memoryFs } from './fs'
|
||
import { dailyLimits, effective, gain, initialRelation, interact, SEVERITY, stageFor, trustLoss } from './relation'
|
||
import type { CompanionCore } from './types'
|
||
|
||
// 測試用原創範例角色,不依賴網路或受版權保護的資料
|
||
const MIKO: CompanionCore = {
|
||
id: 'miko',
|
||
name: '月見ミコ',
|
||
facts: {
|
||
birthday: { value: '3/3', source: 'test', kind: 'novel', confidence: 1 },
|
||
zodiac: 'unknown',
|
||
bloodType: 'unknown',
|
||
hair: 'unknown',
|
||
face: 'unknown',
|
||
height: 'unknown',
|
||
weight: 'unknown',
|
||
measurements: 'unknown',
|
||
},
|
||
neuroticism: 50,
|
||
baseline: { p: 0, a: 0, d: 0 },
|
||
}
|
||
const H = 3_600_000
|
||
const DAY = 24 * H
|
||
const ROOT = '/h/.companion'
|
||
|
||
function setup(builtins = [MIKO]) {
|
||
const fs = memoryFs()
|
||
const clock = fakeClock()
|
||
return { fs, clock, c: createCompanion({ fs, clock, root: ROOT, builtins }) }
|
||
}
|
||
|
||
describe('啟用', () => {
|
||
test('第一次啟用自動啟用預設角色,之後不再', async () => {
|
||
const { c } = setup()
|
||
expect((await c.ensureActivated()).first).toBe(true)
|
||
expect((await c.view())?.core.name).toBe('月見ミコ')
|
||
expect((await c.ensureActivated()).first).toBe(false)
|
||
})
|
||
|
||
test('off 後沒有角色,且重新啟動不會自動再啟用', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.command('off')
|
||
expect(await c.view()).toBe(null)
|
||
await c.ensureActivated()
|
||
expect(await c.view()).toBe(null)
|
||
expect(await c.command('status')).toBe('目前沒有啟用角色。')
|
||
})
|
||
|
||
test('切換角色保留原角色資料', async () => {
|
||
const other = { ...MIKO, id: 'ren', name: '蓮' }
|
||
const { c } = setup([MIKO, other])
|
||
await c.ensureActivated()
|
||
await c.moodEvent('thanks')
|
||
const before = (await c.view())!.mood
|
||
expect(await c.command('switch ren')).toContain('蓮')
|
||
expect((await c.view())?.core.id).toBe('ren')
|
||
await c.command('switch miko')
|
||
expect((await c.view())!.mood).toEqual(before)
|
||
expect(await c.command('switch nobody')).toContain('找不到')
|
||
})
|
||
})
|
||
|
||
describe('原子寫入', () => {
|
||
test('不留暫存檔', async () => {
|
||
const { c, fs } = setup()
|
||
await c.ensureActivated()
|
||
await c.moodEvent('thanks')
|
||
expect([...fs.files.keys()].some(k => k.endsWith('.tmp'))).toBe(false)
|
||
})
|
||
|
||
test('寫到一半當機時舊資料不損毀', async () => {
|
||
const { c, fs } = setup()
|
||
await c.ensureActivated()
|
||
const path = `${ROOT}/companions/miko/state.json`
|
||
const old = fs.files.get(path)
|
||
const real = fs.rename
|
||
fs.rename = async () => {
|
||
throw new Error('crash')
|
||
}
|
||
await expect(c.moodEvent('thanks')).rejects.toThrow()
|
||
fs.rename = real
|
||
expect(fs.files.get(path)).toBe(old)
|
||
})
|
||
})
|
||
|
||
describe('心情', () => {
|
||
test('事件更新 PAD 並對應標籤', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.moodEvent('thanks')
|
||
const v = (await c.view())!
|
||
expect(Math.abs(v.mood.p - (0.15))).toBeLessThan(1e-9)
|
||
expect(v.label).toBe('興奮')
|
||
})
|
||
|
||
test('衰減依經過時間回到基線,半衰期 8 小時', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
await c.moodEvent('thanks')
|
||
clock.advance(8 * H)
|
||
expect(Math.abs((await c.view())!.mood.p - (0.075))).toBeLessThan(1e-9)
|
||
clock.advance(10 * DAY)
|
||
expect(Math.abs((await c.view())!.mood.p)).toBeLessThan(0.001)
|
||
})
|
||
|
||
test('高神經質衰減較慢', async () => {
|
||
const nervous = { ...MIKO, neuroticism: 100 }
|
||
const { c, clock } = setup([nervous])
|
||
await c.ensureActivated()
|
||
await c.moodEvent('thanks')
|
||
clock.advance(8 * H)
|
||
expect((await c.view())!.mood.p).toBeGreaterThan(0.09)
|
||
})
|
||
|
||
test('單軸每回合變化不超過 0.3,且不超出 ±1', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
for (let i = 0; i < 10; i++) await c.moodEvent('scold')
|
||
expect((await c.view())!.mood.p).toBe(-1)
|
||
const { c: c2 } = setup()
|
||
await c2.ensureActivated()
|
||
await c2.moodEvent('scold')
|
||
expect(Math.abs((await c2.view())!.mood.p - (-0.2))).toBeLessThan(1e-9)
|
||
})
|
||
|
||
test('八分區標籤', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.moodEvent('scold')
|
||
expect((await c.view())!.label).toBe('焦慮')
|
||
await c.moodEvent('lateNight')
|
||
expect((await c.view())!.label).toBeTruthy()
|
||
})
|
||
})
|
||
|
||
describe('關係四維度與階段', () => {
|
||
const rel = async (c: ReturnType<typeof setup>['c']) => (await c.view())!.relation
|
||
|
||
test('從 0 開始,同一天的增量受日限約束', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
expect(await rel(c)).toEqual({ affection: 0, trust: 0, intimacy: 0, dependence: 0 })
|
||
for (let i = 0; i < 50; i++) await c.affinityEvent('meaningful')
|
||
const r = await rel(c)
|
||
const lim = dailyLimits()
|
||
expect(r.affection).toBeLessThanOrEqual(lim.affection + 1e-9)
|
||
expect(r.trust).toBeLessThanOrEqual(lim.trust + 1e-9)
|
||
expect(r.intimacy).toBeLessThanOrEqual(lim.intimacy + 1e-9)
|
||
expect(r.dependence).toBeLessThanOrEqual(lim.dependence + 1e-9)
|
||
expect(r.affection).toBeGreaterThan(lim.affection * 0.7)
|
||
})
|
||
|
||
test('日限公式依人格傾向調整', () => {
|
||
expect(dailyLimits()).toEqual({ affection: 4, trust: 5 * 0.7, intimacy: 3.5 * 0.8 * 1.2, dependence: 2.5 * 1 })
|
||
expect(dailyLimits({ guard: 1 }).trust).toBeCloseTo(2)
|
||
expect(dailyLimits({ guard: 0 }).trust).toBe(5)
|
||
})
|
||
|
||
test('有效增量 = 原始增量 × (1 − 今日累計/日限) × (1 − 目前值/100)', () => {
|
||
let r = initialRelation(0)
|
||
r = gain(r, 'affection', 3)
|
||
expect(r.save.affection).toBeCloseTo(3)
|
||
r = gain(r, 'affection', 3)
|
||
expect(r.save.affection).toBeCloseTo(3 + 3 * (1 - 3 / 4) * (1 - 3 / 100))
|
||
const hi = { ...initialRelation(0), save: { affection: 50, trust: 0, intimacy: 0, dependence: 0 } }
|
||
expect(gain(hi, 'affection', 3).save.affection).toBeCloseTo(50 + 3 * 0.5)
|
||
})
|
||
|
||
test('隔天日限重置', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
for (let i = 0; i < 20; i++) await c.affinityEvent('meaningful')
|
||
const day1 = (await rel(c)).affection
|
||
clock.advance(DAY)
|
||
for (let i = 0; i < 20; i++) await c.affinityEvent('meaningful')
|
||
expect((await rel(c)).affection).toBeGreaterThan(day1 + 3)
|
||
})
|
||
|
||
test('期望違反:被罵扣好感 3,信賴扣得更多,越親密越多,單次上限 30', () => {
|
||
const r0 = { ...initialRelation(0), save: { affection: 20, trust: 40, intimacy: 0, dependence: 0 } }
|
||
const a = interact(r0, 'negative', 0)
|
||
expect(a.save.affection).toBeCloseTo(17)
|
||
expect(a.save.trust).toBeCloseTo(40 - 3 * 1.7)
|
||
const close = interact({ ...r0, save: { ...r0.save, intimacy: 100 } }, 'negative', 0)
|
||
expect(40 - close.save.trust).toBeGreaterThan(40 - a.save.trust)
|
||
expect(trustLoss(3, SEVERITY.lie, 100, 1)).toBeLessThanOrEqual(30)
|
||
expect(trustLoss(100, SEVERITY.lie, 100, 1)).toBe(30)
|
||
expect(trustLoss(3, SEVERITY.lie, 0)).toBeGreaterThan(trustLoss(3, SEVERITY.broken, 0))
|
||
expect(interact(initialRelation(0), 'negative', 0).save.trust).toBeCloseTo(0)
|
||
})
|
||
|
||
test('升階條件逐一檢查', () => {
|
||
const d = (affection: number, trust: number, intimacy: number) => ({ affection, trust, intimacy, dependence: 0 })
|
||
expect(stageFor(d(14, 99, 99), 0)).toBe(0)
|
||
expect(stageFor(d(15, 0, 0), 0)).toBe(1)
|
||
expect(stageFor(d(30, 24, 0), 0)).toBe(1)
|
||
expect(stageFor(d(30, 25, 0), 0)).toBe(2)
|
||
expect(stageFor(d(50, 45, 29), 0)).toBe(2)
|
||
expect(stageFor(d(50, 45, 30), 0)).toBe(3)
|
||
expect(stageFor(d(100, 100, 100), 0)).toBe(3) // 不實作曖昧以上
|
||
})
|
||
|
||
test('只升不降:數值回落、被罵都不降階', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
expect((await c.view())!.stage).toBe('陌生')
|
||
const seen = new Set<string>()
|
||
for (let d = 0; d < 60; d++) {
|
||
for (let i = 0; i < 20; i++) await c.affinityEvent('meaningful')
|
||
seen.add((await c.view())!.stage)
|
||
clock.advance(DAY)
|
||
}
|
||
expect([...seen]).toEqual(['陌生', '熟人', '朋友', '好友'])
|
||
for (let i = 0; i < 40; i++) await c.affinityEvent('negative')
|
||
expect((await rel(c)).trust).toBeLessThan(45)
|
||
expect((await c.view())!.stage).toBe('好友')
|
||
})
|
||
|
||
test('久未互動 14 天內不變,之後每天 −0.5,階段不降', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
for (let d = 0; d < 20; d++) {
|
||
for (let i = 0; i < 20; i++) await c.affinityEvent('meaningful')
|
||
clock.advance(DAY)
|
||
}
|
||
clock.advance(-DAY)
|
||
const before = await rel(c)
|
||
const stage = (await c.view())!.stage
|
||
clock.advance(14 * DAY)
|
||
expect(await rel(c)).toEqual(before)
|
||
clock.advance(10 * DAY)
|
||
expect((await rel(c)).affection).toBeCloseTo(before.affection - 5)
|
||
expect((await rel(c)).trust).toBeCloseTo(before.trust - 5)
|
||
clock.advance(1000 * DAY)
|
||
expect((await rel(c)).affection).toBe(0)
|
||
expect((await c.view())!.stage).toBe(stage)
|
||
})
|
||
|
||
test('原典基準與存檔累積兩本帳相加', () => {
|
||
const base = { affection: 20, trust: 10, intimacy: 0, dependence: 0 }
|
||
const r = interact(initialRelation(0, base), 'meaningful', 0)
|
||
expect(effective(r).affection).toBeCloseTo(20 + 3 * (1 - 20 / 100))
|
||
expect(r.save.affection).toBeLessThan(3)
|
||
expect(r.stage).toBe(1)
|
||
})
|
||
})
|
||
|
||
describe('舊 affinity 存檔升級', () => {
|
||
const legacy = (value: number, stage: number, at: number) => ({
|
||
mood: { p: 0, a: 0, d: 0, at },
|
||
affinity: { value, stage, dayKey: '2026-10-14', dayGain: 0.02, lastSeen: at },
|
||
todos: 0,
|
||
})
|
||
|
||
const cases: [number, number, string][] = [
|
||
[0.05, 0, '陌生'],
|
||
[0.3, 1, '熟人'],
|
||
[0.55, 2, '朋友'],
|
||
[0.8, 3, '好友'],
|
||
]
|
||
for (const [value, stage, name] of cases) test(`value ${value}、stage ${stage} → ${name},好感值保留`, async () => {
|
||
const { c, fs, clock } = setup()
|
||
await c.ensureActivated()
|
||
await fs.write(`${ROOT}/companions/miko/state.json`, JSON.stringify(legacy(value, stage, clock.now())))
|
||
const v = (await c.view())!
|
||
expect(v.stage).toBe(name)
|
||
expect(v.relation.affection).toBeCloseTo(value * 100)
|
||
const saved = JSON.parse((await fs.read(`${ROOT}/companions/miko/state.json`))!)
|
||
expect(saved.affinity).toBeUndefined()
|
||
expect(saved.relation.stage).toBe(stage)
|
||
expect(saved.relation.dayGain.affection).toBeCloseTo(2)
|
||
})
|
||
|
||
test('升級後可繼續互動', async () => {
|
||
const { c, fs, clock } = setup()
|
||
await c.ensureActivated()
|
||
await fs.write(`${ROOT}/companions/miko/state.json`, JSON.stringify(legacy(0.3, 1, clock.now())))
|
||
await c.affinityEvent('meaningful')
|
||
expect((await c.view())!.relation.affection).toBeGreaterThan(30)
|
||
})
|
||
})
|
||
|
||
describe('指令與設定', () => {
|
||
test('status 展開事實欄位與來源,查不到為 unknown', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
const out = await c.command('status')
|
||
expect(out).toContain('月見ミコ')
|
||
expect(out).toContain('birthday:3/3(novel,信心 1,test)')
|
||
expect(out).toContain('bloodType:unknown')
|
||
})
|
||
|
||
test('setup 寫入共用 user.json,換角色不受影響', async () => {
|
||
const { c, fs } = setup()
|
||
await c.ensureActivated()
|
||
expect(await c.command('setup nickname=阿建 careLimit=3 quietStart=13:00 quietEnd=14:00')).toContain('設定已儲存')
|
||
const u = JSON.parse(fs.files.get(`${ROOT}/user.json`)!)
|
||
expect(u.nickname).toBe('阿建')
|
||
expect(u.careLimit).toBe(3)
|
||
expect(u.quiet).toEqual({ start: '13:00', end: '14:00' })
|
||
expect(await c.command('setup bogus=1')).toContain('無法解析')
|
||
expect((await c.getUser()).nickname).toBe('阿建')
|
||
})
|
||
|
||
test('預設角色白上吹雪的事實帶來源', async () => {
|
||
const fs = memoryFs()
|
||
const c = createCompanion({ fs, clock: fakeClock(), root: ROOT })
|
||
await c.ensureActivated()
|
||
const out = await c.command('status')
|
||
expect(out).toContain('白上吹雪')
|
||
expect(out).toContain('hololivepro.com')
|
||
expect(out).toContain('weight:unknown')
|
||
})
|
||
})
|
||
|
||
describe('語氣注入', () => {
|
||
const MIKO_TONE: CompanionCore = { ...MIKO, persona: ['句尾常加「にゃ」'], phrases: ['おはミコ'] }
|
||
|
||
test('prompt 含角色名、稱呼、個性、招牌短語與技術內容保留規則', async () => {
|
||
const { c } = setup([MIKO_TONE])
|
||
await c.ensureActivated()
|
||
await c.command('setup nickname=阿建')
|
||
const p = (await c.prompt())!
|
||
expect(p).toContain('月見ミコ')
|
||
expect(p).toContain('阿建')
|
||
expect(p).toContain('にゃ')
|
||
expect(p).toContain('おはミコ')
|
||
expect(p).toContain('逐字保持原樣')
|
||
})
|
||
|
||
test('角色關閉時沒有 prompt', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.command('off')
|
||
expect(await c.prompt()).toBeNull()
|
||
})
|
||
|
||
test('送出提示:責罵降心情與好感,感謝升心情,普通互動累積好感', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.onPrompt('請幫我看一下這個函式')
|
||
expect((await c.view())!.relation.affection).toBeCloseTo(3)
|
||
await c.onPrompt('謝謝你')
|
||
expect((await c.view())!.mood.p).toBeGreaterThan(0.1)
|
||
const before = (await c.view())!.relation.affection
|
||
await c.onPrompt('你很煩')
|
||
expect((await c.view())!.relation.affection).toBeLessThan(before)
|
||
expect((await c.view())!.mood.p).toBeLessThan(0.1)
|
||
})
|
||
})
|
||
|
||
describe('記憶、待辦與關心', () => {
|
||
test('提示寫入記憶,之後相關提示會檢索到並進入 prompt', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.onPrompt('我正在重構 payment 模組的退款流程')
|
||
await c.onPrompt('接著處理 payment 退款的測試')
|
||
expect(await c.prompt()).toContain('payment')
|
||
})
|
||
|
||
test('太短的提示不寫入記憶', async () => {
|
||
const { c, fs } = setup()
|
||
await c.ensureActivated()
|
||
await c.onPrompt('好')
|
||
expect(fs.files.get(`${ROOT}/companions/miko/memory.json`)).toBeUndefined()
|
||
})
|
||
|
||
test('todo add/list/done,狀態列待辦數同步', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
expect(await c.command('todo add 寫 README')).toContain('#1')
|
||
expect(await c.command('todo promise 週五前補測試')).toContain('【承諾】')
|
||
expect((await c.view())!.todos).toBe(2)
|
||
expect(await c.command('todo')).toContain('寫 README')
|
||
expect(await c.prompt()).toContain('週五前補測試')
|
||
expect(await c.command('todo done 1')).toContain('已完成')
|
||
expect((await c.view())!.todos).toBe(1)
|
||
expect(await c.command('todo done 99')).toContain('找不到')
|
||
expect(await c.command('todo add')).toContain('用法')
|
||
})
|
||
|
||
test('todo due 依使用者時區解析,到期後 tick 提醒一次', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
await c.command('setup tz=+0800')
|
||
// 2026-10-12 08:00 UTC = 16:00 +0800
|
||
await c.command('todo add 開會 due=2026-10-12T16:00')
|
||
expect(await c.tick()).toEqual([])
|
||
clock.advance(8 * H + 60_000)
|
||
const first = await c.tick()
|
||
expect(first.some(x => x.trigger === 'todo_due' && x.text.includes('開會'))).toBe(true)
|
||
expect((await c.tick()).some(x => x.trigger === 'todo_due')).toBe(false)
|
||
})
|
||
|
||
test('連續工具失敗後,下一個提示附帶關心情境並降低心情', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.onPrompt('先跑一下測試看看')
|
||
const before = (await c.view())!.mood.p
|
||
for (let i = 0; i < 3; i++) await c.onToolResult(false)
|
||
expect((await c.view())!.mood.p).toBeLessThan(before)
|
||
expect((await c.onPrompt('再試一次')).length).toBeGreaterThan(0)
|
||
})
|
||
|
||
test('quiet 靜音期間不送出關心', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
expect(await c.command('quiet 30')).toContain('30')
|
||
await c.onPrompt('先跑一下測試看看')
|
||
for (let i = 0; i < 3; i++) await c.onToolResult(false)
|
||
expect(await c.onPrompt('再試一次')).toEqual([])
|
||
expect(await c.command('quiet 0')).toContain('取消')
|
||
expect(await c.command('quiet abc')).toContain('用法')
|
||
})
|
||
|
||
test('replying 或 typing 時 tick 不彈出也不消耗待辦提醒', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
await c.command('todo add 喝水 due=2026-10-12T00:00')
|
||
clock.advance(H)
|
||
expect(await c.tick({ replying: true })).toEqual([])
|
||
expect((await c.tick()).some(x => x.trigger === 'todo_due')).toBe(true)
|
||
})
|
||
|
||
test('session 結束時可鞏固記憶且不拋錯', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.onPrompt('我正在重構 payment 模組的退款流程')
|
||
await c.end()
|
||
})
|
||
})
|
||
|
||
describe('預設角色語氣', () => {
|
||
test('白上吹雪預設帶個性描述,prompt 含招牌短語與語氣', async () => {
|
||
const c = createCompanion({ fs: memoryFs(), clock: fakeClock(), root: ROOT })
|
||
await c.ensureActivated()
|
||
const p = (await c.prompt())!
|
||
expect(p).toContain('白上吹雪')
|
||
expect(p).toContain('Good foxy day!')
|
||
expect(p).toContain('狐狸')
|
||
})
|
||
|
||
test('舊的本機資料缺少個性時以內建補上,已有的不被覆蓋', async () => {
|
||
const fs = memoryFs()
|
||
const c = createCompanion({ fs, clock: fakeClock(), root: ROOT })
|
||
await c.ensureActivated()
|
||
const path = `${ROOT}/companions/fubuki/core.json`
|
||
const core = JSON.parse(fs.files.get(path)!)
|
||
delete core.persona
|
||
fs.files.set(path, JSON.stringify(core))
|
||
expect(await c.prompt()).toContain('狐狸')
|
||
fs.files.set(path, JSON.stringify({ ...core, persona: ['自訂個性'] }))
|
||
const p = (await c.prompt())!
|
||
expect(p).toContain('自訂個性')
|
||
expect(p).not.toContain('狐狸')
|
||
})
|
||
})
|
||
|
||
describe('工作事件反應', () => {
|
||
test('同一個測試指令失敗後成功:toast 反應,心情變好', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
await c.onPrompt('開始跑測試修 bug')
|
||
await c.tick()
|
||
clock.advance(10 * 60_000)
|
||
await c.onToolResult(false, { command: 'bun test' })
|
||
const before = (await c.view())!.mood.p
|
||
await c.onToolResult(true, { command: 'bun test' })
|
||
expect((await c.view())!.mood.p).toBeGreaterThan(before)
|
||
expect((await c.tick()).map(x => x.trigger)).toEqual(['test_recovered'])
|
||
})
|
||
|
||
test('沒有轉通過的成功不改變心情', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
const before = (await c.view())!.mood
|
||
await c.onToolResult(true, { command: 'bun test' })
|
||
expect((await c.view())!.mood).toEqual(before)
|
||
})
|
||
|
||
test('有用過工具的回合結束:toast 反應並提升心情;純聊天不反應', async () => {
|
||
const { c, clock } = setup()
|
||
await c.ensureActivated()
|
||
await c.onPrompt('幫我整理一下目錄結構')
|
||
await c.tick()
|
||
clock.advance(10 * 60_000)
|
||
await c.onTurnComplete(false)
|
||
expect(await c.tick()).toEqual([])
|
||
const before = (await c.view())!.mood.p
|
||
await c.onTurnComplete(true)
|
||
expect((await c.view())!.mood.p).toBeGreaterThan(before)
|
||
expect((await c.tick()).map(x => x.trigger)).toEqual(['turn_done'])
|
||
})
|
||
|
||
test('沒有啟用角色時事件被忽略', async () => {
|
||
const { c } = setup()
|
||
await c.ensureActivated()
|
||
await c.command('off')
|
||
await c.onToolResult(true, { command: 'bun test' })
|
||
await c.onTurnComplete(true)
|
||
expect(await c.tick()).toEqual([])
|
||
})
|
||
|
||
test('角色資料的 freq 會調整主動頻率', async () => {
|
||
const quiet = { ...MIKO, freq: { turn_done: 0.001 } }
|
||
const { c, clock } = setup([quiet])
|
||
await c.ensureActivated()
|
||
await c.onPrompt('幫我整理一下目錄結構')
|
||
await c.tick()
|
||
clock.advance(10 * 60_000)
|
||
await c.onTurnComplete(true)
|
||
expect((await c.tick()).map(x => x.trigger)).toEqual(['turn_done'])
|
||
clock.advance(2 * 3600_000)
|
||
await c.onTurnComplete(true)
|
||
expect(await c.tick()).toEqual([])
|
||
})
|
||
})
|