合并主分支
解决简单冲突
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
|
||||
import {
|
||||
completeEditorBackgroundMusicPrompt,
|
||||
createEditorAsset,
|
||||
createEditorAssetFolder,
|
||||
createEditorProject,
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS,
|
||||
EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES,
|
||||
EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH,
|
||||
EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS,
|
||||
extractEditorUiDesignAssets,
|
||||
generateEditorBackgroundMusic,
|
||||
generateEditorCharacterAnimation,
|
||||
@@ -32,6 +35,7 @@ import {
|
||||
removeEditorImageBackground,
|
||||
renameEditorProject,
|
||||
saveEditorProjectLayout,
|
||||
simplifyEditorBackgroundMusicPrompt,
|
||||
snapEditorImageToPixelArt,
|
||||
splitEditorIconSpritesheet,
|
||||
submitEditorAssetShowcase,
|
||||
@@ -46,6 +50,10 @@ const editorRetryOptionsExpectation = expect.objectContaining({
|
||||
maxRetries: 0,
|
||||
retryUnsafeMethods: false,
|
||||
});
|
||||
// 助手请求只带有界超时:额外的 transport retry 会把一次业务语义轮放大成多次上游调用。
|
||||
const promptAssistRequestOptions = {
|
||||
timeoutMs: EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: requestJsonMock,
|
||||
@@ -1983,13 +1991,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',
|
||||
@@ -1997,7 +2013,7 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
const result = await generateEditorBackgroundMusic({
|
||||
gptDescriptionPrompt: '森林冒险背景音乐',
|
||||
gptDescriptionPrompt: canonicalPrompt,
|
||||
makeInstrumental: true,
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '游戏背景音乐 1',
|
||||
@@ -2010,20 +2026,196 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
gptDescriptionPrompt: '森林冒险背景音乐',
|
||||
gptDescriptionPrompt: canonicalPrompt,
|
||||
makeInstrumental: true,
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '游戏背景音乐 1',
|
||||
}),
|
||||
}),
|
||||
'生成游戏背景音乐失败',
|
||||
expect.objectContaining({
|
||||
{
|
||||
timeoutMs: 1_200_000,
|
||||
retry: editorRetryOptionsExpectation,
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards background music transport failures without configuring retries', async () => {
|
||||
requestJsonMock.mockRejectedValueOnce(new Error('transport failed'));
|
||||
|
||||
await expect(
|
||||
generateEditorBackgroundMusic({
|
||||
gptDescriptionPrompt: '森林冒险背景音乐',
|
||||
makeInstrumental: true,
|
||||
}),
|
||||
).rejects.toThrow('transport failed');
|
||||
|
||||
expect(requestJsonMock.mock.calls[0]?.[3]).toEqual({
|
||||
timeoutMs: 1_200_000,
|
||||
});
|
||||
});
|
||||
|
||||
it('completes a background music prompt through the authenticated internal BFF', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
prompt: '轻快明亮的森林冒险背景音乐',
|
||||
charCount: 13,
|
||||
});
|
||||
|
||||
const result = await completeEditorBackgroundMusicPrompt({
|
||||
currentPrompt: '森林冒险',
|
||||
targetChars: 180,
|
||||
maxChars: 200,
|
||||
} as Parameters<typeof completeEditorBackgroundMusicPrompt>[0] & {
|
||||
targetChars: number;
|
||||
maxChars: number;
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
prompt: '轻快明亮的森林冒险背景音乐',
|
||||
charCount: 13,
|
||||
});
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/editor/audios/background-music/prompts/completions',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
currentPrompt: '森林冒险',
|
||||
}),
|
||||
signal: undefined,
|
||||
},
|
||||
'AI 补全背景音乐提示词失败',
|
||||
promptAssistRequestOptions,
|
||||
);
|
||||
expect(EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS).toBe(180_000);
|
||||
expect(requestJsonMock.mock.calls[0]).toHaveLength(4);
|
||||
expect(requestJsonMock.mock.calls[0][3]).toEqual(promptAssistRequestOptions);
|
||||
});
|
||||
|
||||
it('simplifies a background music prompt without forwarding client-controlled limits', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
prompt: '紧张推进的战斗背景音乐',
|
||||
charCount: 11,
|
||||
});
|
||||
|
||||
const result = await simplifyEditorBackgroundMusicPrompt({
|
||||
currentPrompt: '紧张推进、铜管与鼓点交织的战斗背景音乐'.repeat(10),
|
||||
targetChars: 170,
|
||||
maxChars: 200,
|
||||
model: 'client-must-not-control',
|
||||
} as Parameters<typeof simplifyEditorBackgroundMusicPrompt>[0] & {
|
||||
targetChars: number;
|
||||
maxChars: number;
|
||||
model: string;
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
prompt: '紧张推进的战斗背景音乐',
|
||||
charCount: 11,
|
||||
});
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/editor/audios/background-music/prompts/simplifications',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
currentPrompt: '紧张推进、铜管与鼓点交织的战斗背景音乐'.repeat(10),
|
||||
}),
|
||||
signal: undefined,
|
||||
},
|
||||
'简化背景音乐提示词失败',
|
||||
promptAssistRequestOptions,
|
||||
);
|
||||
expect(requestJsonMock.mock.calls[0]).toHaveLength(4);
|
||||
expect(requestJsonMock.mock.calls[0][3]).toEqual(promptAssistRequestOptions);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'completion',
|
||||
completeEditorBackgroundMusicPrompt,
|
||||
'/api/editor/audios/background-music/prompts/completions',
|
||||
'AI 补全背景音乐提示词失败',
|
||||
],
|
||||
[
|
||||
'simplification',
|
||||
simplifyEditorBackgroundMusicPrompt,
|
||||
'/api/editor/audios/background-music/prompts/simplifications',
|
||||
'简化背景音乐提示词失败',
|
||||
],
|
||||
] as const)(
|
||||
'forwards the AbortSignal for background music prompt %s',
|
||||
async (_label, requestPromptAssist, path, fallbackMessage) => {
|
||||
const controller = new AbortController();
|
||||
const abortReason = new Error('prompt assist cancelled');
|
||||
requestJsonMock.mockImplementationOnce(
|
||||
(_url: string, init: RequestInit) =>
|
||||
new Promise((_resolve, reject) => {
|
||||
init.signal?.addEventListener(
|
||||
'abort',
|
||||
() => reject(init.signal?.reason),
|
||||
{ once: true },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const request = requestPromptAssist(
|
||||
{ currentPrompt: '森林冒险' },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
controller.abort(abortReason);
|
||||
|
||||
await expect(request).rejects.toBe(abortReason);
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
path,
|
||||
expect.objectContaining({
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({ currentPrompt: '森林冒险' }),
|
||||
}),
|
||||
fallbackMessage,
|
||||
promptAssistRequestOptions,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
'completion',
|
||||
completeEditorBackgroundMusicPrompt,
|
||||
'AI 补全背景音乐提示词失败',
|
||||
],
|
||||
[
|
||||
'simplification',
|
||||
simplifyEditorBackgroundMusicPrompt,
|
||||
'简化背景音乐提示词失败',
|
||||
],
|
||||
] as const)(
|
||||
'preserves the v1 API error from background music prompt %s',
|
||||
async (_label, requestPromptAssist, fallbackMessage) => {
|
||||
const apiError = Object.assign(new Error('登录状态已失效'), {
|
||||
name: 'ApiClientError',
|
||||
status: 401,
|
||||
code: 'UNAUTHORIZED',
|
||||
});
|
||||
requestJsonMock.mockRejectedValueOnce(apiError);
|
||||
|
||||
await expect(
|
||||
requestPromptAssist({ currentPrompt: '森林冒险' }),
|
||||
).rejects.toBe(apiError);
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/^\/api\/editor\/audios\/background-music\/prompts\//,
|
||||
),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ currentPrompt: '森林冒险' }),
|
||||
}),
|
||||
fallbackMessage,
|
||||
promptAssistRequestOptions,
|
||||
);
|
||||
expect(requestJsonMock.mock.calls[0]).toHaveLength(4);
|
||||
},
|
||||
);
|
||||
|
||||
it('edits editor images through the backend BFF', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
imageSrc: 'data:image/png;base64,edited',
|
||||
|
||||
Reference in New Issue
Block a user