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,