/** @vitest-environment jsdom */ import { act, render } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent'; import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary'; import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView'; import { WorldType, type CustomWorldProfile } from '../../types'; import { executeRpgCreationAction, getRpgCreationOperation, upsertRpgWorldProfile, } from '../../services/rpg-creation'; import { useRpgCreationResultAutosave } from './useRpgCreationResultAutosave'; import { useRpgEntryLibraryDetail } from './useRpgEntryLibraryDetail'; vi.mock('../../services/rpg-creation', () => ({ executeRpgCreationAction: vi.fn(), getRpgCreationOperation: vi.fn(), upsertRpgWorldProfile: vi.fn(), })); vi.mock('../../services/rpg-entry', () => ({ deleteRpgEntryWorldProfile: vi.fn(), getRpgEntryWorldGalleryDetail: vi.fn(), listRpgEntryWorldLibrary: vi.fn(), publishRpgEntryWorldProfile: vi.fn(), unpublishRpgEntryWorldProfile: vi.fn(), })); function buildProfile(name: string): CustomWorldProfile { return { id: `profile-${name}`, settingText: name, name, subtitle: name, summary: name, tone: '测试', playerGoal: '测试', templateWorldType: WorldType.WUXIA, compatibilityTemplateWorldType: WorldType.WUXIA, majorFactions: [], coreConflicts: [], attributeSchema: { id: `schema-${name}`, worldId: `profile-${name}`, schemaVersion: 1, generatedFrom: { worldType: WorldType.CUSTOM, worldName: name, settingSummary: name, tone: '测试', conflictCore: '测试', }, slots: [], }, playableNpcs: [], storyNpcs: [], items: [], landmarks: [], generationMode: 'full', generationStatus: 'complete', }; } function buildSession( overrides: Partial = {}, ): CustomWorldAgentSessionSnapshot { return { sessionId: 'agent-session-1', currentTurn: 1, anchorContent: { worldPromise: null, playerFantasy: null, themeBoundary: null, playerEntryPoint: null, coreConflict: null, keyRelationships: null, hiddenLines: null, iconicElements: null, }, progressPercent: 20, lastAssistantReply: '继续补齐世界草稿。', stage: 'clarifying', focusCardId: null, creatorIntent: null, creatorIntentReadiness: { isReady: false, completedKeys: [], missingKeys: [], }, anchorPack: null, lockState: null, draftProfile: null, messages: [], draftCards: [], pendingClarifications: [], suggestedActions: [], recommendedReplies: [], qualityFindings: [], assetCoverage: { roleAssets: [], sceneAssets: [], allRoleAssetsReady: false, allSceneAssetsReady: false, }, resultPreview: null, updatedAt: '2026-04-25T00:00:00.000Z', ...overrides, }; } function buildResultView( overrides: Partial = {}, ): RpgCreationResultView { const session = overrides.session ?? buildSession(); return { session, profile: null, profileSource: 'none', targetStage: 'agent-workspace', generationViewSource: null, resultViewSource: null, canAutosaveLibrary: false, canSyncResultProfile: false, publishReady: false, canEnterWorld: false, blockerCount: 0, recoveryAction: 'continue_agent', recoveryReason: null, ...overrides, }; } describe('RPG Agent 草稿恢复', () => { beforeEach(() => { vi.clearAllMocks(); }); it('作品摘要已有对象数量但 session 没有 draftProfile 时恢复 Agent 页面', async () => { const syncAgentCreationResultView = vi.fn(async () => buildResultView({ session: buildSession({ stage: 'clarifying', draftProfile: null, }), targetStage: 'agent-workspace', recoveryAction: 'continue_agent', }), ); const setSelectionStage = vi.fn(); const persistAgentUiState = vi.fn(); const setGeneratedCustomWorldProfile = vi.fn(); const setCustomWorldResultViewSource = vi.fn(); const suppressAgentDraftResultAutoOpen = vi.fn(); let openWork: | ((work: CustomWorldWorkSummary) => Promise) | null = null; function Harness() { openWork = useRpgEntryLibraryDetail({ userId: 'user-1', selectedDetailEntry: null, setSelectedDetailEntry: vi.fn(), savedCustomWorldEntries: [], setSavedCustomWorldEntries: vi.fn(), setGeneratedCustomWorldProfile, setCustomWorldError: vi.fn(), setCustomWorldAutoSaveError: vi.fn(), setCustomWorldAutoSaveState: vi.fn(), setCustomWorldGenerationViewSource: vi.fn(), setCustomWorldResultViewSource, setSelectionStage, setPlatformTabToCreate: vi.fn(), setPlatformError: vi.fn(), appendBrowseHistoryEntry: vi.fn(async () => {}), refreshCustomWorldWorks: vi.fn(async () => []), refreshPublishedGallery: vi.fn(async () => []), persistAgentUiState, syncAgentCreationResultView, buildDraftResultProfile: (view) => (view?.profile as CustomWorldProfile | null) ?? null, suppressAgentDraftResultAutoOpen, releaseAgentDraftResultAutoOpenSuppression: vi.fn(), resetAutoSaveTrackingToIdle: vi.fn(), markAutoSavedProfile: vi.fn(), }).handleOpenCreationWork; return null; } render(); await act(async () => { await openWork?.({ workId: 'draft:agent-session-1', sourceType: 'agent_session', status: 'draft', title: '未生成草稿作品', subtitle: '', summary: '', updatedAt: '2026-04-25T00:00:00.000Z', stage: 'clarifying', stageLabel: '澄清中', playableNpcCount: 2, landmarkCount: 3, sessionId: 'agent-session-1', canResume: true, canEnterWorld: false, }); }); expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1'); expect(suppressAgentDraftResultAutoOpen).toHaveBeenCalled(); expect(persistAgentUiState).toHaveBeenCalledWith('agent-session-1', null); expect(setGeneratedCustomWorldProfile).toHaveBeenLastCalledWith(null); expect(setCustomWorldResultViewSource).toHaveBeenLastCalledWith(null); expect(setSelectionStage).toHaveBeenLastCalledWith('agent-workspace'); expect(setSelectionStage).not.toHaveBeenCalledWith('custom-world-result'); }); it('Agent 结果页自动保存先回写 session,再保存后端 result-view profile', async () => { const oldProfile = buildProfile('旧前端快照'); const latestProfile = { ...buildProfile('服务端草稿快照'), summary: '自动保存应保存这份 session 最新草稿。', }; const latestSession = buildSession({ stage: 'object_refining', draftProfile: latestProfile as unknown as Record, }); const syncAgentSessionSnapshot = vi.fn(async () => latestSession); const syncAgentCreationResultView = vi.fn(async () => buildResultView({ session: latestSession, profile: latestProfile, profileSource: 'result_preview', targetStage: 'custom-world-result', resultViewSource: 'agent-draft', canAutosaveLibrary: true, canSyncResultProfile: true, recoveryAction: 'open_result', }), ); vi.mocked(executeRpgCreationAction).mockResolvedValue({ operation: { operationId: 'operation-sync-result', type: 'sync_result_profile', status: 'running', phaseLabel: '结果页同步中', phaseDetail: '正在同步结果页。', progress: 50, }, }); vi.mocked(getRpgCreationOperation).mockResolvedValue({ operationId: 'operation-sync-result', type: 'sync_result_profile', status: 'completed', phaseLabel: '结果页已同步', phaseDetail: '结果页已同步。', progress: 100, }); vi.mocked(upsertRpgWorldProfile).mockResolvedValue({ entry: { ownerUserId: 'user-1', profileId: latestProfile.id, publicWorkCode: null, authorPublicUserCode: null, profile: latestProfile, visibility: 'draft', publishedAt: null, updatedAt: '2026-04-25T00:00:00.000Z', authorDisplayName: '测试玩家', worldName: latestProfile.name, subtitle: latestProfile.subtitle, summaryText: latestProfile.summary, coverImageSrc: null, themeMode: 'tide', playableNpcCount: 0, landmarkCount: 0, likeCount: 0, }, entries: [], }); function Harness() { useRpgCreationResultAutosave({ selectionStage: 'custom-world-result', activeAgentSessionId: 'agent-session-1', generatedCustomWorldProfile: oldProfile, isAgentDraftResultView: true, userId: 'user-1', setGeneratedCustomWorldProfile: vi.fn(), setAgentOperation: vi.fn(), setSavedCustomWorldEntries: vi.fn(), setSelectedDetailEntry: vi.fn(), refreshCustomWorldWorks: vi.fn(async () => []), persistAgentUiState: vi.fn(), syncAgentSessionSnapshot, syncAgentCreationResultView, buildDraftResultProfile: (view) => (view?.profile as CustomWorldProfile | null) ?? null, }); return null; } vi.useFakeTimers(); render(); await act(async () => { await vi.advanceTimersByTimeAsync(650); }); vi.useRealTimers(); expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1'); expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1'); expect(executeRpgCreationAction).toHaveBeenCalledWith('agent-session-1', { action: 'sync_result_profile', profile: expect.objectContaining({ id: oldProfile.id, name: oldProfile.name, }), }); expect(upsertRpgWorldProfile).toHaveBeenCalledWith( expect.objectContaining({ id: latestProfile.id, name: latestProfile.name, summary: latestProfile.summary, }), { sourceAgentSessionId: 'agent-session-1', }, ); }); });