import { expect, mock, test } from 'claude-code/testing' import { HIDE_ORDER, bar, layout, remainColor, usedColor } from './statusline' import type { Segment } from './statusline' import { CHARACTERS, packToCharacter, personaSection, registerCharacters, themeColor } from './characters' import type { PackLike } from './characters' const seg = (id: string, text: string): Segment => ({ id, parts: [{ text }] }) test('啟動角色後 persona 注入系統提示', async ($, on) => { mock.store(on) on('command.register', () => ({ value: { name: 'x' } }) as never) on('session.start', () => ({ cwd: '/tmp' }) as never) on('command.run', () => ({ text: '' })) on('prompt.compose', () => ({ sections: [] })) await $.session.start({ source: 'startup', cwd: '/tmp' } as never) await $.command.run({ command: 'sao-asuna', args: '' } as never) const res = await $.prompt.compose({ model: 'm', promptModel: 'm', surfaces: [], tools: [], outputStyle: null, traits: [] } as never) expect(res.sections.some(s => s.id === 'jsc-anima:persona' && s.text.includes('亞絲娜'))).toBe(true) }) test('切換角色與離開', async ($, on) => { mock.store(on) on('command.register', () => ({ value: { name: 'x' } }) as never) on('session.start', () => ({ cwd: '/tmp' }) as never) on('command.run', () => ({ text: '' })) on('prompt.compose', () => ({ sections: [] })) await $.session.start({ source: 'startup', cwd: '/tmp' } as never) await $.command.run({ command: 'sao-asuna', args: '' } as never) await $.command.run({ command: 'sao-suguha', args: '' } as never) const compose = () => $.prompt.compose({ model: 'm', promptModel: 'm', surfaces: [], tools: [], outputStyle: null, traits: [] } as never) expect((await compose()).sections.find(s => s.id === 'jsc-anima:persona')?.text).toContain('直葉') await $.command.run({ command: 'anima-off', args: '' } as never) expect((await compose()).sections.some(s => s.id === 'jsc-anima:persona')).toBe(false) }) test('狀態列:窄時依序整欄隱藏,不截斷', () => { const all = [seg('character', 'AB'), seg('model', 'CD'), seg('dir', 'EF'), seg('branch', 'GH'), seg('5h', 'IJ'), seg('7d', 'KL'), seg('ctx', 'MN'), seg('sys', 'OP')] expect(layout(all, 999).length).toBe(8) expect(layout(all, 26).map(s => s.id)).not.toContain('sys') const narrow = layout(all, 14).map(s => s.id) expect(narrow).toEqual(['character', 'model', '5h', 'ctx']) expect([...HIDE_ORDER]).toEqual(['sys', '7d', 'dir', 'branch']) }) test('量表固定長度,剩餘越少越紅', () => { expect(bar(0).length).toBe(bar(100).length) expect(remainColor(10, 60, 85)).toBe('red') expect(remainColor(90, 60, 85)).toBe('green') expect(usedColor(70, 60, 85)).toBe('yellow') }) test('persona 含動漫層與口頭禪,主題色可用於狀態列', () => { const c = CHARACTERS[0]! const text = personaSection(c, c.axes) expect(text).toContain('動漫刻畫') expect(text).toContain('口頭禪') expect(themeColor(c)).toBe('red') }) test('角色包轉成可啟動角色:代號、年齡與原作戀人', () => { const pack: PackLike = { slug: 'rin', name: '凜', work: 'demo', persona: { selfRef: '我', address: '你', tone: '冷靜', catchphrase: '嗯', values: '誠實', taboos: '不說謊' }, axes: CHARACTERS[0]!.axes, anime: { moe: 'm', speech: 's', emotions: 'e', kaomoji: 'k', themeColor: 'blue' }, timeline: [{ chapter: '第2章', age: 19, relationships: [{ with: '蓮', kind: 'romantic' }] }], } const c = packToCharacter(pack, '示範') expect(c.slug).toBe('demo-rin') expect(c.timeline).toEqual([{ chapter: 2, age: 19, romance: '蓮' }]) expect(themeColor(c)).toBe('cyan') registerCharacters([c, c]) expect(CHARACTERS.filter(x => x.slug === 'demo-rin').length).toBe(1) })