From dcbf02bbda4227045ab08a789146e2294232656b Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 5 Jun 2026 22:49:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=B1=E8=B4=A5=E8=8D=89=E7=A8=BF?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E5=A4=8D=E7=94=A8=E5=8E=9F=E4=BC=9A=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/pitfalls.md | 10 +- ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 2 +- .../PlatformEntryFlowShellImpl.tsx | 42 ++-- ...gEntryFlowShell.agent.interaction.test.tsx | 216 ++++++++++++++++++ 4 files changed, 255 insertions(+), 15 deletions(-) diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 694eb853..e0a1b1c8 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -1601,10 +1601,18 @@ - 现象:拼图生æˆé¡µå·²ç»æ”¶åˆ° VectorEngine 图片编辑失败并进入é‡è¯•æ€ï¼Œä½†ç”¨æˆ·è¿”回è‰ç¨¿ Tab åŽï¼ŒåŒä¸€è‰ç¨¿ä»æ˜¾ç¤ºâ€œç”Ÿæˆä¸­â€ï¼›è¿žç»­è§¦å‘å¤šä¸ªæ‹¼å›¾ç”Ÿæˆæ—¶ï¼Œå¤±è´¥åŽè¿˜å¯èƒ½åªå‰©ä¸€æ¡æ–°å¢žè‰ç¨¿ï¼Œæˆ–者åªçœ‹åˆ°æ ‡é¢˜ä¸ºâ€œç¬¬1å…³â€çš„åŠæˆå“空壳;抓大鹅åŽå°å¤±è´¥æ—¶ä¹Ÿå¯èƒ½æ²¡æœ‰ä»»ä½•通知,点击è‰ç¨¿åˆåƒé‡æ–°å¼€å§‹ç”Ÿæˆã€‚ - 原因:å‰ç«¯å¤±è´¥ notice åªæ›´æ–°ç”Ÿæˆé¡µå±€éƒ¨çжæ€ï¼Œpending ä½œå“æž¶æ¡ç›®åœ¨å¤±è´¥æ—¶è¢«æ¸…æŽ‰æˆ–è¢«éž `generating` 状æ€è¯¯æ˜ å°„为 `ready`ï¼›åŽç«¯ä½œå“摘è¦ä¹Ÿå¯èƒ½çŸ­æš‚仿˜¯ `generationStatus=generating`ã€‚å¦‚æžœå¤±è´¥æ¶ˆæ¯æ²¡æœ‰å†™å…¥ notice,用户离开生æˆé¡µåŽä¸ä¼šå¼¹å‡º `PlatformErrorDialog`;如果打开è‰ç¨¿åªçœ‹æŒä¹…化 `generating`ï¼Œå°±ä¼šç»•è¿‡å¤±è´¥æ€æ¢å¤ã€‚ -- 处ç†ï¼šå¤±è´¥æ—¶æŒ‰ session ä¿ç•™ pending ä½œå“æž¶æ¡ç›®å¹¶æ ‡è®° `failed`,失败 notice ä¿å­˜é”™è¯¯æ¶ˆæ¯å¹¶è§¦å‘å¸¦æ¥æºçš„ `PlatformErrorDialog`;拼图契约没有 `failed` 枚举,pending 拼图映射为 `idle`ï¼ŒåŒæ—¶ç”¨æœ¬åœ°å¤±è´¥ notice 覆盖æŒä¹…化生æˆä¸­çжæ€å’Œæ—§çš„“正在生æˆâ€æ‘˜è¦ã€‚点击失败è‰ç¨¿åº”优先用 notice / åŽç«¯ session / fallback payload 组装失败生æˆé¡µï¼Œä¸èƒ½é‡æ–°ä»Ž 0 ç§’å¯åŠ¨æ–°è¿›åº¦ï¼›æ‹¼å›¾å¤±è´¥åŠæˆå“没有有效 `workTitle` æ—¶ï¼Œä½œå“æž¶æ ‡é¢˜å›žé€€ä¸ºâ€œæ‹¼å›¾è‰ç¨¿â€ã€‚ +- 处ç†ï¼šå¤±è´¥æ—¶æŒ‰ session ä¿ç•™ pending ä½œå“æž¶æ¡ç›®å¹¶æ ‡è®° `failed`,失败 notice ä¿å­˜é”™è¯¯æ¶ˆæ¯å¹¶è§¦å‘å¸¦æ¥æºçš„ `PlatformErrorDialog`;拼图契约没有 `failed` 枚举,pending 拼图映射为 `idle`ï¼ŒåŒæ—¶ç”¨æœ¬åœ°å¤±è´¥ notice 覆盖æŒä¹…化生æˆä¸­çжæ€å’Œæ—§çš„“正在生æˆâ€æ‘˜è¦ã€‚点击失败è‰ç¨¿åº”优先用 notice / åŽç«¯ session / fallback payload 组装失败生æˆé¡µï¼Œä¸èƒ½é‡æ–°ä»Ž 0 ç§’å¯åŠ¨æ–°è¿›åº¦ï¼›å¤±è´¥é¡µç‚¹å‡»é‡æ–°ç”Ÿæˆå¿…须优先å¤ç”¨å½“å‰ `sessionId` 执行编译 action,ä¸å¾—因存在表å•缓存 payload 就调用 create-sessionã€‚æ‹¼å›¾å¤±è´¥åŠæˆå“没有有效 `workTitle` æ—¶ï¼Œä½œå“æž¶æ ‡é¢˜å›žé€€ä¸ºâ€œæ‹¼å›¾è‰ç¨¿â€ã€‚ - 验è¯ï¼š`node node_modules/vitest/vitest.mjs run src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "failed parallel puzzle|background match3d"`。 - å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/custom-world-home/creationWorkShelf.ts`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 +## 生æˆå¤±è´¥é‡è¯•ä¸è¦èµ°æ–°å»ºè‰ç¨¿ + +- 现象:拼图或抓大鹅生æˆå¤±è´¥åŽï¼Œåœ¨å¤±è´¥é¡µç‚¹å‡»â€œé‡æ–°ç”Ÿæˆâ€ï¼Œä½œå“架里多出一份新的è‰ç¨¿ï¼ŒåŽŸå¤±è´¥è‰ç¨¿ä»ç•™åœ¨åˆ—表里。 +- 原因:é‡è¯• handler 曾优先读å–ç¼“å­˜çš„è¡¨å• payload 并调用 create-session 路径;失败è‰ç¨¿æŒ‰ session ç•™åœ¨ä½œå“æž¶æ˜¯æ­£ç¡®è¡Œä¸ºï¼ŒäºŽæ˜¯é‡è¯•动作é¢å¤–创建了第二份è‰ç¨¿ã€‚ +- 处ç†ï¼šåªè¦å½“å‰å¤±è´¥é¡µè¿˜èƒ½æ¢å¤åˆ°åŽŸ `sessionId`,é‡è¯•就走该 session çš„ compile actionï¼›åªæœ‰æ²¡æœ‰å¯æ¢å¤ session 时,æ‰å…è®¸ç”¨è¡¨å• payload 釿–°åˆ›å»ºè‰ç¨¿ã€‚ +- 验è¯ï¼š`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "failed .* draft retry reuses current session"`。 +- å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + ## 汪汪声浪è‰ç¨¿è¯•玩ä¸è¦å†™æ­£å¼ run - 现象:如果è‰ç¨¿ç»“果页试玩和å‘å¸ƒåŽ runtime 共用åŒä¸€å†™æˆç»©è·¯å¾„,未å‘布或未确认资æºçš„è‰ç¨¿è¯•玩会污染正å¼å•å±€ã€æŽ’è¡Œæ¦œå’Œä½œå“统计。 diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index 063f48f8..0258cda0 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -54,7 +54,7 @@ 4. 生æˆä¸­ä½œå“在整å¡ä¸ŠåŠ ç­‰å¾…é®ç½©ï¼Œä½†ä¸ç§»é™¤ä½œå“基础信æ¯ã€‚ 5. 生æˆä¸­çжæ€ä¸èƒ½åªå­˜åœ¨å‰ç«¯å†…å­˜ notice。åŽç«¯ä½œå“摘è¦å¿…须下å‘坿¢å¤çš„ `generationStatus`ï¼›å‰ç«¯åˆ·æ–°æˆ–退出产å“åŽï¼Œä½œå“架优先用摘è¦çŠ¶æ€æ¢å¤ç­‰å¾…é®ç½©ï¼Œæœ¬è½®å†…å­˜ notice åªä½œä¸ºå³æ—¶å馈。 6. 点击 `generationStatus=generating` çš„è‰ç¨¿å¡å¿…é¡»æ¢å¤å¯¹åº”玩法的生æˆè¿›åº¦é¡µï¼Œä¸èƒ½è¿›å…¥ç©ºç™½ç»“果页或普通工作区;æ¢å¤ç”Ÿæˆé¡µçš„ `startedAtMs` 优先使用åŽç«¯ session çš„ `updatedAt`,没有 session æ—¶å†ä½¿ç”¨ä½œå“æ‘˜è¦ `updatedAt`,ä¸å¾—因釿–°è¿›å…¥é¡µé¢ä»Ž 0 ç§’é‡æ–°è®¡æ—¶ã€‚ -7. 生æˆå¤±è´¥å¿…须按 session 独立记录,ä¸èƒ½ç”¨ä¸€ä¸ªå¤±è´¥æ‰“断或覆盖åŒçŽ©æ³•çš„å…¶å®ƒç”Ÿæˆä»»åŠ¡ã€‚å¤±è´¥ notice 需è¦ä¿å­˜é”™è¯¯æ¶ˆæ¯å¹¶è¦†ç›–ä½œå“æž¶æœ¬åœ°çжæ€ï¼šå³ä½¿åŽç«¯æ‘˜è¦æš‚æ—¶ä»æ˜¯ `generationStatus=generating` 或åªå†™å‡ºåŠæˆå“投影,è‰ç¨¿å¡ä¹Ÿä¸å¾—继续显示“生æˆä¸­â€ï¼Œç‚¹å‡»åŽå¿…须进入失败 / é‡è¯•生æˆé¡µï¼Œä¸èƒ½é‡æ–°åˆ›å»ºä¸€è½®ç”Ÿæˆï¼›æ‹¼å›¾è¿™ç±»å¤±è´¥åŠæˆå“若没有有效 `workTitle`ï¼Œä½œå“æž¶æ ‡é¢˜å›žé€€ä¸ºâ€œæ‹¼å›¾è‰ç¨¿â€ï¼Œä¸æš´éœ²â€œç¬¬1å…³â€ç©ºå£³ã€‚ +7. 生æˆå¤±è´¥å¿…须按 session 独立记录,ä¸èƒ½ç”¨ä¸€ä¸ªå¤±è´¥æ‰“断或覆盖åŒçŽ©æ³•çš„å…¶å®ƒç”Ÿæˆä»»åŠ¡ã€‚å¤±è´¥ notice 需è¦ä¿å­˜é”™è¯¯æ¶ˆæ¯å¹¶è¦†ç›–ä½œå“æž¶æœ¬åœ°çжæ€ï¼šå³ä½¿åŽç«¯æ‘˜è¦æš‚æ—¶ä»æ˜¯ `generationStatus=generating` 或åªå†™å‡ºåŠæˆå“投影,è‰ç¨¿å¡ä¹Ÿä¸å¾—继续显示“生æˆä¸­â€ï¼Œç‚¹å‡»åŽå¿…须进入失败 / é‡è¯•生æˆé¡µï¼Œä¸èƒ½é‡æ–°åˆ›å»ºä¸€è½®ç”Ÿæˆã€‚å¤±è´¥é¡µç‚¹å‡»é‡æ–°ç”Ÿæˆæ—¶å¿…须优先å¤ç”¨å½“å‰å¯æ¢å¤ `sessionId` 执行编译 actionï¼›åªæœ‰æ²¡æœ‰å¯æ¢å¤ session æ—¶æ‰å…许回退到新建è‰ç¨¿ã€‚æ‹¼å›¾è¿™ç±»å¤±è´¥åŠæˆå“若没有有效 `workTitle`ï¼Œä½œå“æž¶æ ‡é¢˜å›žé€€ä¸ºâ€œæ‹¼å›¾è‰ç¨¿â€ï¼Œä¸æš´éœ²â€œç¬¬1å…³â€ç©ºå£³ã€‚ 8. 从è‰ç¨¿ Tab ä½œå“æž¶æ‰“å¼€è‰ç¨¿å·¥ä½œåŒºã€ç”Ÿæˆé¡µæˆ–结果页时,返回按钮必须回到è‰ç¨¿ Tab çš„åŒä¸€ä½œå“架语境;从创作 Tab 新建或直接进入创作链路时æ‰å›žåˆ°åˆ›ä½œ Tab。平å°å£³å±‚éœ€è¦æ˜¾å¼è®°å½•本次创作æµçš„è¿”å›žæ¥æºï¼Œä¸èƒ½è®©ç»“果页返回动作固定跳到创作入å£ã€‚ 9. ç§æœ‰ generated 图片必须通过 `ResolvedAssetImage` / `/api/assets/read-url` æ¢ç­¾è¯»å–。 10. æ•²æœ¨é±¼ä½œå“æž¶è¯»å–当å‰ç”¨æˆ·ä½œå“列表时走 `GET /api/creation/wooden-fish/works`ï¼›å‘布æˆåŠŸåŽå¹³å°å£³å¿…é¡»åŒæ—¶åˆ·æ–°ä½œå“架与公开广场,é¿å…作å“刚å‘布时ä»åœç•™åœ¨æ—§åˆ—表。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index b4d77e98..86e77d3c 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -9449,20 +9449,26 @@ export function PlatformEntryFlowShellImpl({ const executeSquareHoleAction = squareHoleFlow.executeAction; const retryMatch3DDraftGeneration = useCallback(() => { - if (match3dFormDraftPayload && !match3dSession?.draft?.profileId) { - void createMatch3DDraftFromForm(match3dFormDraftPayload); + if (match3dSession?.sessionId) { + const retryPayload = + match3dFormDraftPayload ?? + buildMatch3DFormPayloadFromSession(match3dSession); + void executeMatch3DAction({ + action: 'match3d_compile_draft', + generateClickSound: retryPayload.generateClickSound, + }); return; } - void executeMatch3DAction({ - action: 'match3d_compile_draft', - generateClickSound: match3dFormDraftPayload?.generateClickSound, - }); + if (match3dFormDraftPayload) { + void createMatch3DDraftFromForm(match3dFormDraftPayload); + return; + } }, [ createMatch3DDraftFromForm, executeMatch3DAction, match3dFormDraftPayload, - match3dSession?.draft?.profileId, + match3dSession, ]); const retrySquareHoleAssetGeneration = useCallback(() => { @@ -10336,15 +10342,25 @@ export function PlatformEntryFlowShellImpl({ ); const retryPuzzleDraftGeneration = useCallback(() => { - if (puzzleFormDraftPayload) { - void createPuzzleDraftFromForm(puzzleFormDraftPayload); + if (puzzleSession?.sessionId) { + const retryPayload = + puzzleFormDraftPayload ?? + buildPuzzleFormPayloadFromSession(puzzleSession); + void executePuzzleAction( + buildPuzzleCompileActionFromFormPayload(retryPayload), + ); return; } - void executePuzzleAction( - buildPuzzleCompileActionFromFormPayload(puzzleFormDraftPayload), - ); - }, [createPuzzleDraftFromForm, executePuzzleAction, puzzleFormDraftPayload]); + if (puzzleFormDraftPayload) { + void createPuzzleDraftFromForm(puzzleFormDraftPayload); + } + }, [ + createPuzzleDraftFromForm, + executePuzzleAction, + puzzleFormDraftPayload, + puzzleSession, + ]); const retryVisualNovelDraftGeneration = useCallback(() => { if (!visualNovelFormDraftPayload) { diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index 64d065a3..d1323f12 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -4228,6 +4228,115 @@ test('background match3d draft failure notifies and reopens failed retry page', expect(match3dCreationClient.executeAction).toHaveBeenCalledTimes(1); }); +test('failed match3d draft retry reuses current session instead of creating another draft', async () => { + const user = userEvent.setup(); + const failedSession = buildMockMatch3DAgentSession({ + sessionId: 'match3d-retry-failed-session', + draft: null, + stage: 'collecting_config', + updatedAt: '2026-05-18T12:05:00.000Z', + }); + const persistedFailedWork: Match3DWorkSummary = { + workId: 'match3d-retry-failed-work', + profileId: 'match3d-retry-failed-profile', + ownerUserId: 'user-1', + sourceSessionId: failedSession.sessionId, + gameName: 'é‡è¯•抓鹅', + themeText: '霓虹水果摊', + summary: '抓大鹅素æç”Ÿæˆå¤±è´¥ï¼Œå¯é‡æ–°æ‰“开处ç†ã€‚', + tags: ['æ°´æžœ', '抓大鹅'], + coverImageSrc: null, + referenceImageSrc: null, + clearCount: 12, + difficulty: 4, + publicationStatus: 'draft', + playCount: 0, + updatedAt: '2026-05-18T12:05:00.000Z', + publishedAt: null, + publishReady: false, + generationStatus: 'generating', + generatedItemAssets: [], + }; + let rejectCompile!: (reason?: unknown) => void; + vi.mocked(match3dCreationClient.createSession).mockResolvedValue({ + session: failedSession, + }); + vi.mocked(match3dCreationClient.executeAction).mockReturnValueOnce( + new Promise((_, reject) => { + rejectCompile = reject; + }), + ); + vi.mocked(match3dCreationClient.getSession).mockResolvedValue({ + session: failedSession, + }); + + render(); + + await openCreateTemplateHub(user); + await user.click(await findCreationTypeButton('抓大鹅')); + await user.click( + await screen.findByRole('button', { name: 'ç”ŸæˆæŠ“å¤§é¹…è‰ç¨¿' }), + ); + await screen.findByRole('progressbar', { name: '抓大鹅è‰ç¨¿ç”Ÿæˆè¿›åº¦' }); + await user.click(screen.getByRole('button', { name: '返回创作中心' })); + await openDraftHub(user); + vi.mocked(listMatch3DWorks).mockResolvedValue({ + items: [persistedFailedWork], + }); + + await act(async () => { + rejectCompile(new Error('æŠ“å¤§é¹…ç´ ææœåŠ¡å¤±è´¥')); + await Promise.resolve(); + }); + const failureDialog = await screen.findByRole('dialog', { + name: 'å‘生错误', + }); + await user.click(within(failureDialog).getByRole('button', { name: '关闭' })); + + const draftPanel = getPlatformTabPanel('saves'); + await user.click( + await within(draftPanel).findByRole('button', { + name: /继续创作《(?:é‡è¯•抓鹅|抓大鹅è‰ç¨¿)》/u, + }), + ); + const reopenedFailureDialog = await screen.findByRole('dialog', { + name: 'å‘生错误', + }); + await user.click( + within(reopenedFailureDialog).getByRole('button', { name: '关闭' }), + ); + vi.mocked(match3dCreationClient.executeAction).mockResolvedValueOnce({ + session: buildMockMatch3DAgentSession({ + sessionId: failedSession.sessionId, + stage: 'draft_ready', + draft: { + profileId: persistedFailedWork.profileId, + gameName: persistedFailedWork.gameName, + themeText: persistedFailedWork.themeText, + summary: persistedFailedWork.summary, + tags: persistedFailedWork.tags, + coverImageSrc: null, + referenceImageSrc: null, + clearCount: persistedFailedWork.clearCount, + difficulty: persistedFailedWork.difficulty, + generatedItemAssets: [], + }, + }), + }); + + await user.click(await screen.findByRole('button', { name: '釿–°ç”Ÿæˆè‰ç¨¿' })); + + await waitFor(() => { + expect(match3dCreationClient.executeAction).toHaveBeenCalledTimes(2); + }); + expect(match3dCreationClient.createSession).toHaveBeenCalledTimes(1); + expect(match3dCreationClient.executeAction).toHaveBeenNthCalledWith( + 2, + failedSession.sessionId, + expect.objectContaining({ action: 'match3d_compile_draft' }), + ); +}); + test('running match3d persisted draft reopens progress instead of unfinished result', async () => { const user = userEvent.setup(); const runningSession = buildMockMatch3DAgentSession({ @@ -4921,6 +5030,113 @@ test('failed parallel puzzle generations stay as separate non-generating drafts' .toBeTruthy(); }); +test('failed puzzle draft retry reuses current session instead of creating another draft', async () => { + const user = userEvent.setup(); + const failedSession = buildMockPuzzleAgentSession({ + sessionId: 'puzzle-retry-failed-session', + draft: null, + stage: 'collecting_anchors', + updatedAt: '2026-05-18T12:00:00.000Z', + }); + const persistedFailedWork: PuzzleWorkSummary = { + workId: `puzzle-work-${failedSession.sessionId}`, + profileId: `puzzle-profile-${failedSession.sessionId}`, + ownerUserId: 'user-1', + sourceSessionId: failedSession.sessionId, + authorDisplayName: '测试玩家', + workTitle: '', + workDescription: '一套雨夜猫街主题拼图。', + levelName: '第1å…³', + summary: '一套雨夜猫街主题拼图。', + themeTags: [], + coverImageSrc: null, + coverAssetId: null, + publicationStatus: 'draft', + updatedAt: '2026-05-18T12:00:00.000Z', + publishedAt: null, + playCount: 0, + remixCount: 0, + likeCount: 0, + publishReady: false, + generationStatus: 'failed', + levels: [], + }; + let rejectCompile!: (reason?: unknown) => void; + vi.mocked(createPuzzleAgentSession).mockResolvedValue({ + session: failedSession, + }); + vi.mocked(getPuzzleAgentSession).mockResolvedValue({ + session: failedSession, + }); + vi.mocked(executePuzzleAgentAction).mockReturnValueOnce( + new Promise((_, reject) => { + rejectCompile = reject; + }), + ); + + render(); + + await openCreateTemplateHub(user); + await user.click(await findCreationTypeButton('拼图')); + await user.click(await screen.findByRole('button', { name: '生æˆè‰ç¨¿' })); + await screen.findByRole('progressbar', { name: '拼图图片生æˆè¿›åº¦' }); + await user.click(screen.getByRole('button', { name: '返回创作中心' })); + await openDraftHub(user); + vi.mocked(listPuzzleWorks).mockResolvedValue({ + items: [persistedFailedWork], + }); + + await act(async () => { + rejectCompile(new Error('拼图图片生æˆå¤±è´¥')); + await Promise.resolve(); + }); + const failureDialog = await screen.findByRole('dialog', { + name: 'å‘生错误', + }); + await user.click(within(failureDialog).getByRole('button', { name: '关闭' })); + + const draftPanel = getPlatformTabPanel('saves'); + await user.click( + await within(draftPanel).findByRole('button', { + name: /继续创作《[^》]+》/u, + }), + ); + const reopenedFailureDialog = await screen.findByRole('dialog', { + name: 'å‘生错误', + }); + await user.click( + within(reopenedFailureDialog).getByRole('button', { name: '关闭' }), + ); + vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({ + operation: { + operationId: 'compile-puzzle-retry', + type: 'compile_puzzle_draft', + status: 'completed', + phaseLabel: '已完æˆ', + phaseDetail: 'è‰ç¨¿å·²ç”Ÿæˆ', + progress: 1, + }, + session: buildMockPuzzleAgentSession({ + sessionId: failedSession.sessionId, + stage: 'ready_to_publish', + progressPercent: 100, + draft: buildReadyPuzzleDraft(), + }), + }); + + await user.click(await screen.findByRole('button', { name: '釿–°ç”Ÿæˆå›¾ç‰‡' })); + + await waitFor(() => { + expect(executePuzzleAgentAction).toHaveBeenCalledTimes(2); + }); + expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1); + expect(executePuzzleAgentAction).toHaveBeenNthCalledWith( + 2, + failedSession.sessionId, + expect.objectContaining({ action: 'compile_puzzle_draft' }), + ); +}); + test('running puzzle draft opens generation progress from draft tab', async () => { const user = userEvent.setup(); const runningSession = buildMockPuzzleAgentSession({