修复音效直连回包越账号刷新钱包

为 SFX 直连生成回包的钱包刷新补上账号门禁,与排队路径和 BGM 分支口径一致。

新增直连回包成功、失败两条切账号回归测试和一条同账号刷新对照测试。
This commit is contained in:
2026-08-07 04:35:47 +00:00
parent 2281458ba6
commit cb7bcbc67e
2 changed files with 167 additions and 1 deletions
@@ -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<ReturnType<typeof createAudioGenerated>>();
const refreshWalletBalance = vi.fn();
generateEditorSoundEffectMock.mockReturnValueOnce(generation.promise);
render(
<SubmissionWorkflowHarness
currentUserId="user-a"
projectId="project-a"
onWalletBalanceMayHaveChanged={refreshWalletBalance}
initialDialog={createSoundEffectWalletDialog(
'dialog-sfx-wallet-current',
'账号 A 的音效',
)}
/>,
);
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<ReturnType<typeof createAudioGenerated>>();
const refreshWalletBalance = vi.fn();
generateEditorSoundEffectMock.mockReturnValueOnce(generation.promise);
const initialDialog = createSoundEffectWalletDialog(
'dialog-sfx-wallet-account-switch',
'账号 A 的音效',
);
const view = render(
<SubmissionWorkflowHarness
currentUserId="user-a"
projectId="project-a"
onWalletBalanceMayHaveChanged={refreshWalletBalance}
initialDialog={initialDialog}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
view.rerender(
<SubmissionWorkflowHarness
currentUserId="user-b"
projectId="project-a"
onWalletBalanceMayHaveChanged={refreshWalletBalance}
initialDialog={{
...initialDialog,
prompt: '账号 B 的音效',
}}
/>,
);
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<ReturnType<typeof createAudioGenerated>>();
const refreshWalletBalance = vi.fn();
generateEditorSoundEffectMock.mockReturnValueOnce(generation.promise);
const initialDialog = createSoundEffectWalletDialog(
'dialog-sfx-wallet-account-switch-failure',
'账号 A 的音效',
);
const view = render(
<SubmissionWorkflowHarness
currentUserId="user-a"
projectId="project-a"
onWalletBalanceMayHaveChanged={refreshWalletBalance}
initialDialog={initialDialog}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
view.rerender(
<SubmissionWorkflowHarness
currentUserId="user-b"
projectId="project-a"
onWalletBalanceMayHaveChanged={refreshWalletBalance}
initialDialog={{
...initialDialog,
prompt: '账号 B 的音效',
}}
/>,
);
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<ReturnType<typeof createAudioGenerated>>();
@@ -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,