From cb7bcbc67e014c1eb619ee4821a345ffaa2a0b66 Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 7 Aug 2026 04:35:47 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9F=B3=E6=95=88=E7=9B=B4?= =?UTF-8?q?=E8=BF=9E=E5=9B=9E=E5=8C=85=E8=B6=8A=E8=B4=A6=E5=8F=B7=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E9=92=B1=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 SFX 直连生成回包的钱包刷新补上账号门禁,与排队路径和 BGM 分支口径一致。 新增直连回包成功、失败两条切账号回归测试和一条同账号刷新对照测试。 --- ...anvasGenerationSubmissionWorkflow.test.tsx | 152 ++++++++++++++++++ ...ImageCanvasGenerationSubmissionWorkflow.ts | 16 +- 2 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx index e6daf7b52..f81210b13 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -155,6 +155,31 @@ function createAudioGenerated(overrides = {}) { }; } +function createSoundEffectWalletDialog( + id: string, + prompt: string, +): GenerateDialogState { + return { + id, + mode: 'audio-sound-effect', + prompt, + status: 'idle', + composerOpen: true, + soundModel: 'eleven_text_to_sound_v2', + soundDurationMode: 'auto', + soundDurationSeconds: 5, + soundLoop: false, + placeholder: { + x: 200, + y: 160, + width: 420, + height: 120, + originalWidth: 420, + originalHeight: 120, + }, + }; +} + function createIconResult(name: string, imageSrc: string) { return { name, @@ -2636,6 +2661,133 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { ); }); + it('refreshes the wallet for an inline sound effect response while the account stays current', async () => { + const generation = + createDeferred>(); + const refreshWalletBalance = vi.fn(); + generateEditorSoundEffectMock.mockReturnValueOnce(generation.promise); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '提交当前生成' })); + + await act(async () => { + // 直连回包:不带 queueState,钱包刷新由共享 wrapper 立即触发。 + generation.resolve( + createAudioGenerated({ + prompt: '账号 A 的音效', + audioKind: 'sound-effect', + }), + ); + await generation.promise; + await Promise.resolve(); + }); + + expect(refreshWalletBalance).toHaveBeenCalledTimes(1); + }); + + it('does not refresh the previous account wallet when an inline sound effect response lands after the account changes', async () => { + const generation = + createDeferred>(); + const refreshWalletBalance = vi.fn(); + generateEditorSoundEffectMock.mockReturnValueOnce(generation.promise); + const initialDialog = createSoundEffectWalletDialog( + 'dialog-sfx-wallet-account-switch', + '账号 A 的音效', + ); + const view = render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '提交当前生成' })); + view.rerender( + , + ); + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + + await act(async () => { + generation.resolve( + createAudioGenerated({ + prompt: '账号 A 的音效', + audioKind: 'sound-effect', + }), + ); + await generation.promise; + await Promise.resolve(); + }); + + expect(refreshWalletBalance).not.toHaveBeenCalled(); + }); + + it('does not refresh the previous account wallet when an inline sound effect request fails after the account changes', async () => { + const generation = + createDeferred>(); + const refreshWalletBalance = vi.fn(); + generateEditorSoundEffectMock.mockReturnValueOnce(generation.promise); + const initialDialog = createSoundEffectWalletDialog( + 'dialog-sfx-wallet-account-switch-failure', + '账号 A 的音效', + ); + const view = render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '提交当前生成' })); + view.rerender( + , + ); + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + + await act(async () => { + generation.reject(new Error('音效生成失败')); + await generation.promise.catch(() => undefined); + await Promise.resolve(); + }); + + expect(refreshWalletBalance).not.toHaveBeenCalled(); + expect(screen.getByTestId('tracked-dialog').textContent).toBe( + 'audio-sound-effect:账号 B 的音效:idle:open:-', + ); + }); + it('does not write a background music response after the scope changes away and back before acceptance', async () => { const generation = createDeferred>(); diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts index cae713ff0..cafc5d98a 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts @@ -2182,6 +2182,20 @@ export function useImageCanvasGenerationSubmissionWorkflow({ } else if (submissionPlan.kind === 'audio') { const canvasCompletionPlaceholder = getGeneratingDialogPlaceholder(dialog); + // 直连回包的钱包刷新由共享 wrapper 触发,而 wrapper 不持有本次 submission scope。 + // 这里补上与排队路径和 BGM 分支同一道账号门禁:切号后迟到的旧回包不得刷新新账号钱包。 + const refreshSoundEffectWalletBalance = + soundEffectSubmission && onWalletBalanceMayHaveChanged + ? () => { + if ( + isBackgroundMusicSubmissionAccountCurrent( + soundEffectSubmission, + ) + ) { + onWalletBalanceMayHaveChanged(); + } + } + : onWalletBalanceMayHaveChanged; const generated = submissionPlan.audioKind === 'sound-effect' ? await runEditorGenerationWithWalletRefresh( @@ -2201,7 +2215,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({ } : {}), }), - onWalletBalanceMayHaveChanged, + refreshSoundEffectWalletBalance, ) : await generateEditorBackgroundMusic({ ...submissionPlan.input,