import { describe, expect, it } from 'vitest'; import soundEffectPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/sound-effect-prompt-canonicalization.json'; import { canonicalizeSoundEffectPrompt, countSoundEffectPromptCodePoints, createSoundEffectPromptStateModel, SOUND_EFFECT_PROMPT_MAX_CODE_POINTS, validateSoundEffectPrompt, } from './ImageCanvasSoundEffectPromptModel'; describe('ImageCanvasSoundEffectPromptModel', () => { it('matches the shared Unicode canonicalization fixtures', () => { for (const fixture of soundEffectPromptCanonicalizationCases) { const repeat = fixture.repeat ?? 1; const input = `${fixture.prefix ?? ''}${fixture.input.repeat(repeat)}${fixture.suffix ?? ''}`; const expectedPrompt = fixture.prompt.repeat(repeat); expect(canonicalizeSoundEffectPrompt(input), fixture.name).toBe( expectedPrompt, ); expect(countSoundEffectPromptCodePoints(input), fixture.name).toBe( fixture.charCount, ); expect( canonicalizeSoundEffectPrompt(canonicalizeSoundEffectPrompt(input)), fixture.name, ).toBe(expectedPrompt); if (fixture.validation === 'valid') { expect(validateSoundEffectPrompt(input).ok, fixture.name).toBe(true); } else if (fixture.validation === 'too-long') { expect(validateSoundEffectPrompt(input), fixture.name).toEqual({ ok: false, prompt: expectedPrompt, charCount: fixture.charCount, reason: 'too-long', }); } } }); it('accepts 2048 code points and rejects 2049 without truncating', () => { const maximum = `\uFEFF${'😀'.repeat( SOUND_EFFECT_PROMPT_MAX_CODE_POINTS, )}\u2003`; const accepted = validateSoundEffectPrompt(maximum); expect(accepted).toEqual({ ok: true, prompt: '😀'.repeat(SOUND_EFFECT_PROMPT_MAX_CODE_POINTS), charCount: SOUND_EFFECT_PROMPT_MAX_CODE_POINTS, }); const overlong = '声'.repeat(SOUND_EFFECT_PROMPT_MAX_CODE_POINTS + 1); expect(validateSoundEffectPrompt(overlong)).toEqual({ ok: false, prompt: overlong, charCount: SOUND_EFFECT_PROMPT_MAX_CODE_POINTS + 1, reason: 'too-long', }); }); it('rejects empty canonical prompts without adding a default description', () => { for (const prompt of ['', ' \t\r\n', '\uFEFF\u2003\u00a0']) { expect(validateSoundEffectPrompt(prompt)).toEqual({ ok: false, prompt: '', charCount: 0, reason: 'empty', }); } }); it('creates one dialog-scoped optimization operation and swaps one undo snapshot', () => { const model = createSoundEffectPromptStateModel(); const started = model.beginOperation({ dialogId: 'dialog-a', status: 'optimizing', prompt: '\uFEFF金币落地声\u3000', }); expect(started.started).toBe(true); expect(started.canonicalPrompt).toBe('金币落地声'); expect(started.state).toMatchObject({ status: 'optimizing', undoPromptSnapshot: null, temporaryPromptSnapshot: '金币落地声', }); expect( model.beginOperation({ dialogId: 'dialog-a', status: 'optimizing', prompt: '另一个输入', }).reason, ).toBe('duplicate'); const resolved = model.resolveAiOperation( started.operation!, '清脆明亮的金币落地声', ); expect(resolved).toMatchObject({ applied: true, prompt: '清脆明亮的金币落地声', state: { status: 'idle', undoPromptSnapshot: '金币落地声', temporaryPromptSnapshot: null, }, }); expect( model.swapUndoSnapshot('dialog-a', '清脆明亮的金币落地声'), ).toMatchObject({ applied: true, prompt: '金币落地声', state: { undoPromptSnapshot: '清脆明亮的金币落地声' }, }); }); it('clears the replaced snapshot when a later optimization fails', () => { const model = createSoundEffectPromptStateModel(); const first = model.beginOperation({ dialogId: 'dialog-a', status: 'optimizing', prompt: '原始描述', }); model.resolveAiOperation(first.operation!, '第一次优化'); const second = model.beginOperation({ dialogId: 'dialog-a', status: 'optimizing', prompt: '手动修改后的描述', }); expect(second.state.undoPromptSnapshot).toBeNull(); expect(model.rejectOperation(second.operation!)).toMatchObject({ applied: true, prompt: '手动修改后的描述', state: { status: 'idle', undoPromptSnapshot: null, temporaryPromptSnapshot: null, }, }); }); it('keeps submission and preset state isolated by dialog ID', () => { const model = createSoundEffectPromptStateModel(); const submission = model.beginOperation({ dialogId: 'dialog-a', status: 'submitting', prompt: ' 提交描述 ', }); expect(submission.started).toBe(true); expect(model.preparePreset('dialog-a', '不应写入').applied).toBe(false); expect(model.preparePreset('dialog-b', ' 另一面板 ').prompt).toBe( '另一面板', ); expect( model.completeSubmittingOperation(submission.operation!), ).toMatchObject({ applied: true, prompt: '提交描述', state: { status: 'idle' }, }); }); });