/* @vitest-environment jsdom */ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { beforeEach, expect, test, vi } from 'vitest'; import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent'; import { createCustomWorldAgentSession, executeCustomWorldAgentAction, getCustomWorldAgentOperation, getCustomWorldAgentSession, } from '../../services/aiService'; import { listCustomWorldGallery, listCustomWorldLibrary, upsertCustomWorldProfile, } from '../../services/storageService'; import type { GameState } from '../../types'; import { PreGameSelectionFlow, type SelectionStage, } from './PreGameSelectionFlow'; vi.mock('../../services/aiService', () => ({ createCustomWorldAgentSession: vi.fn(), executeCustomWorldAgentAction: vi.fn(), generateCustomWorldProfile: vi.fn(), getCustomWorldAgentOperation: vi.fn(), getCustomWorldAgentSession: vi.fn(), sendCustomWorldAgentMessage: vi.fn(), })); vi.mock('../../services/storageService', () => ({ getCustomWorldGalleryDetail: vi.fn(), listCustomWorldGallery: vi.fn(), listCustomWorldLibrary: vi.fn(), publishCustomWorldProfile: vi.fn(), unpublishCustomWorldProfile: vi.fn(), upsertCustomWorldProfile: vi.fn(), })); vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({ CustomWorldAgentWorkspace: ({ session, onExecuteAction, }: { session: CustomWorldAgentSessionSnapshot | null; onExecuteAction: (payload: { action: string }) => void; }) => (
Agent工作区:{session?.sessionId ?? 'missing-session'}
), })); const mockSession: CustomWorldAgentSessionSnapshot = { sessionId: 'custom-world-agent-session-1', stage: 'clarifying', focusCardId: null, creatorIntent: {}, creatorIntentReadiness: { isReady: false, completedKeys: ['world_hook'], missingKeys: [ 'player_premise', 'theme_and_tone', 'core_conflict', 'relationship_seed', 'iconic_element', ], }, anchorPack: {}, lockState: {}, draftProfile: null, messages: [ { id: 'message-1', role: 'assistant', kind: 'summary', text: '先告诉我你想做一个怎样的 RPG 世界。', createdAt: '2026-04-14T12:00:00.000Z', relatedOperationId: null, }, ], draftCards: [], pendingClarifications: [], suggestedActions: [], recommendedReplies: [], qualityFindings: [], assetCoverage: { roleAssets: [], sceneAssets: [], allRoleAssetsReady: false, allSceneAssetsReady: false, }, updatedAt: '2026-04-14T12:00:00.000Z', }; function TestWrapper() { const [selectionStage, setSelectionStage] = useState('platform'); return ( {}} handleStartNewGame={() => {}} handleCustomWorldSelect={() => {}} /> ); } beforeEach(() => { vi.clearAllMocks(); window.history.replaceState(null, '', '/'); window.sessionStorage.clear(); vi.mocked(listCustomWorldLibrary).mockResolvedValue([]); vi.mocked(listCustomWorldGallery).mockResolvedValue([]); vi.mocked(upsertCustomWorldProfile).mockResolvedValue({ entry: { ownerUserId: 'user-1', profileId: 'agent-draft-custom-world-agent-session-1', profile: { id: 'agent-draft-custom-world-agent-session-1', name: '潮雾列岛', } as never, visibility: 'draft', publishedAt: null, updatedAt: '2026-04-14T12:00:00.000Z', authorDisplayName: '玩家', worldName: '潮雾列岛', subtitle: '旧灯塔与失控航路', summaryText: '第一版世界底稿已经整理完成。', coverImageSrc: null, themeMode: 'tide', playableNpcCount: 1, landmarkCount: 1, }, entries: [], }); vi.mocked(createCustomWorldAgentSession).mockResolvedValue({ session: mockSession, }); vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({ operation: { operationId: 'operation-draft-foundation-1', type: 'draft_foundation', status: 'queued', phaseLabel: '已接收请求', phaseDetail: '正在准备生成世界底稿。', progress: 10, error: null, }, }); vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({ operationId: 'operation-draft-foundation-1', type: 'draft_foundation', status: 'running', phaseLabel: '生成世界底稿', phaseDetail: '正在根据已确认锚点编译第一版世界结构。', progress: 38, error: null, }); vi.mocked(getCustomWorldAgentSession).mockResolvedValue(mockSession); }); test('create tab opens game type modal, keeps AIRP and visual novel locked, and enters agent workspace for RPG', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: '创作' })); await user.click(screen.getByRole('button', { name: /开启新的创作/u })); expect(screen.getByText('选择创作类型')).toBeTruthy(); const airpButton = screen.getByRole('button', { name: /AIRP/u }); const visualNovelButton = screen.getByRole('button', { name: /视觉小说/u, }); expect((airpButton as HTMLButtonElement).disabled).toBe(true); expect((visualNovelButton as HTMLButtonElement).disabled).toBe(true); await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u })); await waitFor(() => { expect(createCustomWorldAgentSession).toHaveBeenCalledTimes(1); }); expect( await screen.findByText('Agent工作区:custom-world-agent-session-1'), ).toBeTruthy(); }); test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: '创作' })); await user.click(screen.getByRole('button', { name: /开启新的创作/u })); await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u })); expect( await screen.findByText('Agent工作区:custom-world-agent-session-1'), ).toBeTruthy(); await user.click(screen.getByRole('button', { name: '开始生成草稿' })); await waitFor(() => { expect(executeCustomWorldAgentAction).toHaveBeenCalledWith( 'custom-world-agent-session-1', { action: 'draft_foundation', }, ); }); expect(await screen.findByText('世界草稿生成进度')).toBeTruthy(); expect(screen.queryByText(/Agent工作区/u)).toBeNull(); expect(screen.getAllByText('生成世界底稿').length).toBeGreaterThan(0); }); test('existing draft sessions enter the legacy result layout directly', async () => { const user = userEvent.setup(); vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({ operationId: 'operation-draft-foundation-1', type: 'draft_foundation', status: 'completed', phaseLabel: '世界底稿已生成', phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。', progress: 100, error: null, }); vi.mocked(getCustomWorldAgentSession).mockResolvedValue({ ...mockSession, stage: 'object_refining', creatorIntent: { sourceMode: 'card', worldHook: '被海雾吞没的旧航路群岛', playerPremise: '玩家回到群岛调查沉船真相。', themeKeywords: ['海雾', '旧航路'], toneDirectives: ['压抑', '悬疑'], openingSituation: '首夜就有陌生船只闯入禁航区。', coreConflicts: ['航运公会与守灯会争夺航路控制权'], keyFactions: [], keyCharacters: [], keyLandmarks: [], iconicElements: ['会移动的海雾'], forbiddenDirectives: [], rawSettingText: '', }, draftProfile: { name: '潮雾列岛', subtitle: '旧灯塔与失控航路', summary: '第一版世界底稿已经整理完成。', tone: '压抑、潮湿、悬疑', playerGoal: '查清沉船与禁航区异动的真相。', majorFactions: ['守灯会', '航运公会'], coreConflicts: ['守灯会与航运公会争夺旧航路控制权'], playableNpcs: [ { id: 'playable-1', name: '沈砺', title: '旧航路引路人', role: '关键同行者', publicIdentity: '最熟悉旧航路的人。', publicMask: '看上去像可靠旧友。', currentPressure: '他必须在两股势力间站队。', hiddenHook: '暗中替沉船商盟引路。', relationToPlayer: '旧友兼潜在背叛者', threadIds: ['thread-1'], summary: '他像旧友,但也像一把始终没收回鞘的刀。', }, ], storyNpcs: [ { id: 'story-1', name: '顾潮音', title: '守灯会值夜人', role: '场景关键角色', publicIdentity: '负责夜间巡灯与封锁。', publicMask: '对外一直冷静克制。', currentPressure: '她知道更多禁航区真相。', hiddenHook: '曾亲眼见过失控海雾吞船。', relationToPlayer: '最早愿意交换线索的人', threadIds: ['thread-1'], summary: '她总像比所有人更早知道海雾会往哪一侧压下来。', }, ], landmarks: [ { id: 'landmark-1', name: '回潮旧灯塔', purpose: '观察雾潮与往来船只', mood: '潮湿、压抑、风声不止', importance: '开局核心场景', characterIds: ['story-1'], threadIds: ['thread-1'], summary: '旧灯塔是整片群岛最先看见异动的地方。', }, ], factions: [], threads: [], chapters: [], worldHook: '被海雾吞没的旧航路群岛', playerPremise: '玩家回到群岛调查沉船真相。', openingSituation: '首夜就有陌生船只闯入禁航区。', iconicElements: ['会移动的海雾'], sourceAnchorSummary: '海雾、旧灯塔、失控航路。', }, draftCards: [ { id: 'world-foundation', kind: 'world', title: '潮雾列岛', subtitle: '旧灯塔与失控航路', summary: '第一版世界底稿已经整理完成。', status: 'warning', linkedIds: ['playable-1', 'story-1', 'landmark-1'], warningCount: 0, }, ], }); render(); await user.click(screen.getByRole('button', { name: '创作' })); await user.click(screen.getByRole('button', { name: /开启新的创作/u })); await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u })); await waitFor( async () => { expect(await screen.findByText('世界档案')).toBeTruthy(); expect( screen.getByRole('button', { name: /保存到我的作品|自动保存中|已保存到我的作品/u, }), ).toBeTruthy(); }, { timeout: 2500 }, ); expect(screen.queryByText(/Agent工作区/u)).toBeNull(); expect(screen.queryByRole('button', { name: /^锚点/u })).toBeNull(); expect(screen.getByText(/原始设定/u)).toBeTruthy(); await user.click(screen.getByRole('button', { name: /场景角色/u })); await user.click(screen.getByRole('button', { name: /顾潮音/u })); expect(await screen.findByText(/编辑场景角色:顾潮音/u)).toBeTruthy(); expect( screen.getByRole('button', { name: /AI生成形象与动作/u }), ).toBeTruthy(); expect(screen.getByText('技能')).toBeTruthy(); });