5c316fc680
将 SFX Prompt 首尾规范化改为 ECMAScript String.trim 语义并同步 Rust 等值实现 更新共享边界 fixture、前后端与 Worker 回归测试 同步权威设计、实施计划、决策日志和 External OpenAPI 描述
162 lines
5.3 KiB
TypeScript
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' },
|
|
});
|
|
});
|
|
});
|