Files
Genarrative/src/components/image-editor/ImageCanvasSoundEffectPromptModel.test.ts
T
lhk229 5c316fc680 统一SFX提示词 trim 规范化
将 SFX Prompt 首尾规范化改为 ECMAScript String.trim 语义并同步 Rust 等值实现

更新共享边界 fixture、前后端与 Worker 回归测试

同步权威设计、实施计划、决策日志和 External OpenAPI 描述
2026-08-07 07:27:52 +00:00

162 lines
5.3 KiB
TypeScript

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' },
});
});
});