测试:完成BGM提示词优化收口并切换助手模型

增加 TypeScript/Rust 共用 canonicalization fixture
补齐 BGM 跨层等值、SFX 和后端边界测试
将 BGM 补全与简化请求模型改为 gpt-5.6-luna,保留画布 Agent gpt-5.4-mini
同步权威设计、项目记忆和 T6 发布门禁记录
验证本地定向测试与真实 VectorEngine 补全/简化请求
This commit is contained in:
2026-08-05 13:35:02 +00:00
parent 69426ee61c
commit aeb5894b9d
13 changed files with 628 additions and 209 deletions
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
import {
BACKGROUND_MUSIC_PROMPT_COMPLETION_MIN_EFFECTIVE_CODE_POINTS,
BACKGROUND_MUSIC_PROMPT_GENERATION_MIN_EFFECTIVE_CODE_POINTS,
@@ -15,86 +16,15 @@ import {
} from './ImageCanvasBackgroundMusicPromptModel';
describe('ImageCanvasBackgroundMusicPromptModel', () => {
it('removes only leading and trailing Unicode White_Space code points', () => {
const unicodeWhiteSpace = [
'\u0009',
'\u000A',
'\u000B',
'\u000C',
'\u000D',
'\u0020',
'\u0085',
'\u00A0',
'\u1680',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200A',
'\u2028',
'\u2029',
'\u202F',
'\u205F',
'\u3000',
].join('');
const internalWhiteSpace = 'A \t\r\n\u0085\u00A0\u3000B';
expect(
canonicalizeBackgroundMusicPrompt(
`${unicodeWhiteSpace}${internalWhiteSpace}${unicodeWhiteSpace}`,
),
).toBe(internalWhiteSpace);
expect(
canonicalizeBackgroundMusicPrompt('\u0085\u200B内容\uFEFF\u0085'),
).toBe('\u200B内容\uFEFF');
expect(canonicalizeBackgroundMusicPrompt('\u200B\uFEFF')).toBe(
'\u200B\uFEFF',
);
});
it('is idempotent without normalizing internal Unicode content', () => {
const combiningText = 'e\u0301';
const zwjEmoji = '👨‍👩‍👧‍👦';
const prompt = ` \u0085${combiningText}\r\n${zwjEmoji}\u200B\uFEFF\u3000`;
const canonicalPrompt = `${combiningText}\r\n${zwjEmoji}\u200B\uFEFF`;
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe(canonicalPrompt);
expect(canonicalizeBackgroundMusicPrompt(canonicalPrompt)).toBe(
canonicalPrompt,
);
expect(canonicalPrompt).toContain(combiningText);
expect(canonicalPrompt).toContain('\r\n');
expect(canonicalPrompt).toContain(zwjEmoji);
expect(canonicalPrompt).toContain('\u200B\uFEFF');
});
it('counts code points from the canonical prompt', () => {
const combiningText = 'e\u0301';
const zwjEmoji = '👨‍👩‍👧‍👦';
const prompt = ` \r\n${combiningText}\r\n${zwjEmoji}\u200B\uFEFF `;
expect(countPromptCodePoints('')).toBe(0);
expect(countPromptCodePoints('\r\n')).toBe(0);
expect(countPromptCodePoints(combiningText)).toBe(2);
expect(countPromptCodePoints(zwjEmoji)).toBe(7);
expect(countPromptCodePoints(prompt)).toBe(13);
expect(prompt).toBe(` \r\n${combiningText}\r\n${zwjEmoji}\u200B\uFEFF `);
});
it('counts only non-Unicode-White_Space code points in the canonical prompt as effective', () => {
expect(countEffectivePromptCodePoints(' \t\r\n\u0085\u00A0\u3000')).toBe(0);
expect(countEffectivePromptCodePoints('e\u0301')).toBe(2);
expect(countEffectivePromptCodePoints('👨‍👩‍👧‍👦')).toBe(7);
expect(countEffectivePromptCodePoints('\uFEFF')).toBe(1);
expect(countEffectivePromptCodePoints('\u200B')).toBe(1);
expect(countEffectivePromptCodePoints('。1A')).toBe(3);
});
it.each(backgroundMusicPromptCanonicalizationCases)(
'canonicalizes shared fixture case $name',
({ input, prompt, charCount, effectiveCharCount }) => {
expect(canonicalizeBackgroundMusicPrompt(input)).toBe(prompt);
expect(countPromptCodePoints(input)).toBe(charCount);
expect(countEffectivePromptCodePoints(input)).toBe(effectiveCharCount);
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe(prompt);
},
);
it('allows formal generation for 1-200 code points with effective content', () => {
expect(BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS).toBe(200);
@@ -1039,32 +1039,35 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
});
});
it('uses 5 seconds as default game sound effect duration', () => {
it('uses the game sound effect fallback for an all-whitespace prompt', () => {
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'audio-sound-effect',
prompt: '按钮确认短促音',
prompt: ' \t\r\n ',
status: 'idle',
},
layers: [],
nextGeneratedIndex: 6,
});
expect(plan).toMatchObject({
expect(plan).toEqual({
kind: 'audio',
audioKind: 'sound-effect',
normalizedPrompt: '游戏音效',
input: {
prompt: '按钮确认短促音',
prompt: '游戏音效',
model: 'audio1.0',
duration: 5,
},
result: {
title: '游戏音效 6',
generationInputs: {
fields: [
{ title: 'prompt', value: '按钮确认短促音' },
{ title: 'prompt', value: '游戏音效' },
{ title: 'model', value: 'audio1.0' },
{ title: 'duration', value: '5秒' },
],
references: [],
},
},
});
@@ -10,6 +10,7 @@ import {
import { useRef, useState } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
import { ApiClientError } from '../../services/apiClient';
import type {
CanvasGenerationDialogState,
@@ -1814,18 +1815,27 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
});
it('claims and canonicalizes one background music submission before the first await', async () => {
const canonicalPrompt = '\uFEFF森林冒险\n夜晚\uFEFF';
const representativeComplexPromptCase =
backgroundMusicPromptCanonicalizationCases.find(
({ name }) => name === 'representative-complex',
);
if (!representativeComplexPromptCase) {
throw new Error('missing representative-complex prompt fixture');
}
const canonicalPrompt = representativeComplexPromptCase.prompt;
const generation =
createDeferred<ReturnType<typeof createAudioGenerated>>();
const onAppendCanvasLayers = vi.fn<[CanvasLayer[]], void>();
generateEditorBackgroundMusicMock.mockReturnValueOnce(generation.promise);
render(
<SubmissionWorkflowHarness
currentUserId="user-a"
projectId="editor-project-bgm"
onAppendCanvasLayers={onAppendCanvasLayers}
initialDialog={{
id: 'dialog-bgm',
mode: 'audio-background-music',
prompt: `\u0085 ${canonicalPrompt} \u0085`,
prompt: representativeComplexPromptCase.input,
status: 'idle',
composerOpen: true,
placeholder: {
@@ -1848,22 +1858,34 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
fireEvent.click(submitButton);
expect(generateEditorBackgroundMusicMock).toHaveBeenCalledTimes(1);
expect(generateEditorBackgroundMusicMock).toHaveBeenCalledWith(
expect.objectContaining({
gptDescriptionPrompt: canonicalPrompt,
makeInstrumental: true,
projectId: 'editor-project-bgm',
generationInputs: {
fields: [
{
title: 'gpt_description_prompt',
value: canonicalPrompt,
},
],
references: [],
expect(generateEditorBackgroundMusicMock).toHaveBeenCalledWith({
gptDescriptionPrompt: canonicalPrompt,
makeInstrumental: true,
projectId: 'editor-project-bgm',
generationInputs: {
fields: [
{
title: 'gpt_description_prompt',
value: canonicalPrompt,
},
],
references: [],
},
assetFolderId: 'project',
assetLabel: '游戏背景音乐 1',
canvasCompletion: {
dialogId: 'dialog-bgm',
title: '游戏背景音乐 1',
placeholder: {
x: 200,
y: 160,
width: 420,
height: 120,
originalWidth: 420,
originalHeight: 120,
},
}),
);
},
});
expect(screen.getByTestId('tracked-dialog').textContent).toContain(
`audio-background-music:${canonicalPrompt}:idle:open`,
);
@@ -1891,6 +1913,19 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(screen.getByTestId('layers').textContent).toContain(
'background-music',
);
expect(onAppendCanvasLayers).toHaveBeenCalledTimes(1);
});
const generatedLayer = onAppendCanvasLayers.mock.calls[0]?.[0]?.[0];
expect(generatedLayer?.prompt).toBe(canonicalPrompt);
expect(generatedLayer?.actualPrompt).toBe(canonicalPrompt);
expect(generatedLayer?.generationInputs).toEqual({
fields: [
{
title: 'gpt_description_prompt',
value: canonicalPrompt,
},
],
references: [],
});
});
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
import {
completeEditorBackgroundMusicPrompt,
createEditorAsset,
@@ -1582,13 +1583,21 @@ describe('editorProjectClient', () => {
});
it('generates editor background music through the backend BFF', async () => {
const representativeComplexPromptCase =
backgroundMusicPromptCanonicalizationCases.find(
({ name }) => name === 'representative-complex',
);
if (!representativeComplexPromptCase) {
throw new Error('missing representative-complex prompt fixture');
}
const canonicalPrompt = representativeComplexPromptCase.prompt;
requestJsonMock.mockResolvedValueOnce({
audioSrc: '/generated-character-drafts/editor-audios/bgm.mp3',
width: 420,
height: 120,
sourceType: 'generated',
prompt: '森林冒险背景音乐',
actualPrompt: '森林冒险背景音乐',
prompt: canonicalPrompt,
actualPrompt: canonicalPrompt,
model: 'chirp-v4',
provider: 'VectorEngine',
taskId: 'music-task-1',
@@ -1596,7 +1605,7 @@ describe('editorProjectClient', () => {
});
const result = await generateEditorBackgroundMusic({
gptDescriptionPrompt: '森林冒险背景音乐',
gptDescriptionPrompt: canonicalPrompt,
makeInstrumental: true,
assetFolderId: 'project',
assetLabel: '游戏背景音乐 1',
@@ -1609,7 +1618,7 @@ describe('editorProjectClient', () => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
gptDescriptionPrompt: '森林冒险背景音乐',
gptDescriptionPrompt: canonicalPrompt,
makeInstrumental: true,
assetFolderId: 'project',
assetLabel: '游戏背景音乐 1',