1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
unpublishRpgEntryWorldProfile,
|
||||
upsertRpgProfileBrowseHistory as upsertProfileBrowseHistory,
|
||||
} from '../../services/rpg-entry';
|
||||
import { createBigFishCreationSession } from '../../services/big-fish-creation';
|
||||
import { createPuzzleAgentSession } from '../../services/puzzle-agent';
|
||||
import { listPuzzleWorks } from '../../services/puzzle-works';
|
||||
import type { GameState } from '../../types';
|
||||
import {
|
||||
AuthUiContext,
|
||||
@@ -39,6 +42,7 @@ import {
|
||||
} from '../auth/AuthUiContext';
|
||||
import {
|
||||
RpgEntryFlowShell,
|
||||
type RpgEntryFlowShellProps,
|
||||
type SelectionStage,
|
||||
} from './RpgEntryFlowShell';
|
||||
|
||||
@@ -63,13 +67,20 @@ async function openCreationHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
expect(await screen.findByText('角色扮演 RPG')).toBeTruthy();
|
||||
}
|
||||
|
||||
async function openNewRpgCreation(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
) {
|
||||
async function openNewRpgCreation(user: ReturnType<typeof userEvent.setup>) {
|
||||
await openCreationHub(user);
|
||||
await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
|
||||
}
|
||||
|
||||
function getPlatformTabPanel(tab: string) {
|
||||
const panel = document.getElementById(`platform-tab-panel-${tab}`);
|
||||
if (!panel) {
|
||||
throw new Error(`Missing platform tab panel: ${tab}`);
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
vi.mock('../../services/rpg-creation', () => ({
|
||||
createRpgCreationSession: vi.fn(),
|
||||
executeRpgCreationAction: vi.fn(),
|
||||
@@ -96,6 +107,23 @@ vi.mock('../../services/rpg-entry', () => ({
|
||||
upsertRpgProfileBrowseHistory: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/puzzle-works', () => ({
|
||||
listPuzzleWorks: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/big-fish-creation', () => ({
|
||||
createBigFishCreationSession: vi.fn(),
|
||||
executeBigFishCreationAction: vi.fn(),
|
||||
streamBigFishCreationMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/puzzle-agent', () => ({
|
||||
createPuzzleAgentSession: vi.fn(),
|
||||
executePuzzleAgentAction: vi.fn(),
|
||||
getPuzzleAgentSession: vi.fn(),
|
||||
streamPuzzleAgentMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
|
||||
CustomWorldAgentWorkspace: ({
|
||||
session,
|
||||
@@ -379,6 +407,7 @@ const compiledAgentDraftSession: CustomWorldAgentSessionSnapshot = {
|
||||
|
||||
type TestAuthValue = {
|
||||
user: AuthUser | null;
|
||||
canAccessProtectedData: boolean;
|
||||
openLoginModal: (postLoginAction?: (() => void) | null) => void;
|
||||
requireAuth: (action: () => void) => void;
|
||||
openSettingsModal: (section?: PlatformSettingsSection) => void;
|
||||
@@ -393,9 +422,12 @@ type TestAuthValue = {
|
||||
settingsError: string | null;
|
||||
};
|
||||
|
||||
function createAuthValue(overrides: Partial<TestAuthValue> = {}): TestAuthValue {
|
||||
function createAuthValue(
|
||||
overrides: Partial<TestAuthValue> = {},
|
||||
): TestAuthValue {
|
||||
return {
|
||||
user: mockAuthUser,
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: () => {},
|
||||
requireAuth: (action) => action(),
|
||||
openSettingsModal: () => {},
|
||||
@@ -416,10 +448,12 @@ function TestWrapper({
|
||||
withAuth = false,
|
||||
authValue,
|
||||
onContinueGame,
|
||||
onSelectWorld,
|
||||
}: {
|
||||
withAuth?: boolean;
|
||||
authValue?: TestAuthValue;
|
||||
onContinueGame?: (snapshot?: HydratedSavedGameSnapshot | null) => void;
|
||||
onSelectWorld?: RpgEntryFlowShellProps['handleCustomWorldSelect'];
|
||||
} = {}) {
|
||||
const [selectionStage, setSelectionStage] =
|
||||
useState<SelectionStage>('platform');
|
||||
@@ -433,7 +467,7 @@ function TestWrapper({
|
||||
savedSnapshot={null}
|
||||
handleContinueGame={onContinueGame ?? (() => {})}
|
||||
handleStartNewGame={() => {}}
|
||||
handleCustomWorldSelect={() => {}}
|
||||
handleCustomWorldSelect={onSelectWorld ?? (() => {})}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -442,9 +476,7 @@ function TestWrapper({
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthUiContext.Provider
|
||||
value={authValue ?? createAuthValue()}
|
||||
>
|
||||
<AuthUiContext.Provider value={authValue ?? createAuthValue()}>
|
||||
{content}
|
||||
</AuthUiContext.Provider>
|
||||
);
|
||||
@@ -513,8 +545,106 @@ beforeEach(() => {
|
||||
vi.mocked(createRpgCreationSession).mockResolvedValue({
|
||||
session: mockSession,
|
||||
});
|
||||
vi.mocked(createBigFishCreationSession).mockResolvedValue({
|
||||
session: {
|
||||
sessionId: 'big-fish-session-1',
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
stage: 'clarifying',
|
||||
anchorPack: {
|
||||
gameplayPromise: {
|
||||
key: 'gameplay_promise',
|
||||
label: '核心玩法',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
ecologyVisualTheme: {
|
||||
key: 'ecology_visual_theme',
|
||||
label: '生态视觉',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
growthLadder: {
|
||||
key: 'growth_ladder',
|
||||
label: '成长阶梯',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
riskTempo: {
|
||||
key: 'risk_tempo',
|
||||
label: '风险节奏',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
},
|
||||
draft: null,
|
||||
assetSlots: [],
|
||||
assetCoverage: {
|
||||
levelMainImageReadyCount: 0,
|
||||
levelMotionReadyCount: 0,
|
||||
backgroundReady: false,
|
||||
requiredLevelCount: 0,
|
||||
publishReady: false,
|
||||
blockers: [],
|
||||
},
|
||||
messages: [],
|
||||
lastAssistantReply: '先说说你想要什么样的大鱼生态。',
|
||||
publishReady: false,
|
||||
updatedAt: '2026-04-22T12:00:00.000Z',
|
||||
},
|
||||
});
|
||||
vi.mocked(createPuzzleAgentSession).mockResolvedValue({
|
||||
session: {
|
||||
sessionId: 'puzzle-session-1',
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
stage: 'clarifying',
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'theme_promise',
|
||||
label: '主题承诺',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visual_subject',
|
||||
label: '视觉主体',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visual_mood',
|
||||
label: '视觉气质',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'composition_hooks',
|
||||
label: '构图钩子',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tags_and_forbidden',
|
||||
label: '标签与禁区',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
},
|
||||
draft: null,
|
||||
messages: [],
|
||||
lastAssistantReply: '先说一个你最想做成拼图的画面。',
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: null,
|
||||
updatedAt: '2026-04-22T12:00:00.000Z',
|
||||
},
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([]);
|
||||
vi.mocked(listPuzzleWorks).mockResolvedValue({
|
||||
items: [],
|
||||
});
|
||||
vi.mocked(executeRpgCreationAction).mockResolvedValue({
|
||||
operation: {
|
||||
operationId: 'operation-draft-foundation-1',
|
||||
@@ -594,9 +724,7 @@ test('create tab opens compiled agent draft in result refinement page', async ()
|
||||
canEnterWorld: false,
|
||||
},
|
||||
]);
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(
|
||||
compiledAgentDraftSession,
|
||||
);
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(compiledAgentDraftSession);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -606,12 +734,19 @@ test('create tab opens compiled agent draft in result refinement page', async ()
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /继续完善/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('正在加载世界编辑器...')).toBeNull();
|
||||
}, { timeout: 5000 });
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.queryByText('正在加载世界编辑器...')).toBeNull();
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
|
||||
expect(await screen.findByText('世界档案', {}, { timeout: 5000 })).toBeTruthy();
|
||||
expect(screen.queryByText('Agent工作区:custom-world-agent-session-1')).toBeNull();
|
||||
expect(
|
||||
await screen.findByText('世界档案', {}, { timeout: 5000 }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText('Agent工作区:custom-world-agent-session-1'),
|
||||
).toBeNull();
|
||||
expect(screen.getByRole('button', { name: /返回创作/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -706,7 +841,7 @@ test('opening a compiled draft with a missing agent session falls back to create
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
within(getPlatformTabPanel('create')).getByText(
|
||||
'这份共创草稿已失效,已为你返回创作中心,请重新开始创作。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
@@ -807,6 +942,88 @@ test('restoring an agent workspace while logged out opens login modal before loa
|
||||
expect(getRpgCreationSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('new creation entry maps raw bearer token errors to user-facing auth copy', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createRpgCreationSession).mockRejectedValueOnce(
|
||||
new ApiClientError({
|
||||
message: '缺少 Authorization Bearer Token',
|
||||
status: 401,
|
||||
code: 'UNAUTHORIZED',
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getByText(
|
||||
'当前登录状态已失效,请重新登录后继续。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(listPuzzleWorks).toHaveBeenCalled();
|
||||
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
|
||||
});
|
||||
|
||||
test('big fish creation timeout exits busy state and shows a readable error', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createBigFishCreationSession).mockRejectedValueOnce(
|
||||
Object.assign(new Error('请求超时:15000ms'), {
|
||||
name: 'TimeoutError',
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
|
||||
const button = screen.getByRole('button', { name: /大鱼吃小鱼/u });
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getAllByText(
|
||||
'开启大鱼吃小鱼创作工作台超时,请确认运行时后端已启动后重试。',
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect((button as HTMLButtonElement).disabled).toBe(false);
|
||||
expect(screen.queryByText(/正在加载大鱼吃小鱼共创工作区/u)).toBeNull();
|
||||
});
|
||||
|
||||
test('puzzle creation timeout exits busy state and shows a readable error', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createPuzzleAgentSession).mockRejectedValueOnce(
|
||||
Object.assign(new Error('请求超时:15000ms'), {
|
||||
name: 'TimeoutError',
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
|
||||
const button = screen.getByRole('button', { name: /拼图玩法/u });
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getAllByText(
|
||||
'开启拼图创作工作台超时,请确认运行时后端已启动后重试。',
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect((button as HTMLButtonElement).disabled).toBe(false);
|
||||
expect(screen.queryByText(/正在准备拼图共创工作区/u)).toBeNull();
|
||||
});
|
||||
|
||||
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -851,9 +1068,7 @@ test('existing draft sessions open result page refinement instead of agent dialo
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(
|
||||
compiledAgentDraftSession,
|
||||
);
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(compiledAgentDraftSession);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -913,9 +1128,7 @@ test('agent result view shows publish blockers and disables publish-enter action
|
||||
|
||||
await openNewRpgCreation(user);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/当前还有 1 个发布阻断项/u),
|
||||
).toBeTruthy();
|
||||
expect(await screen.findByText(/当前还有 1 个发布阻断项/u)).toBeTruthy();
|
||||
const actionButton = screen.getByRole('button', {
|
||||
name: /发布并进入世界/u,
|
||||
});
|
||||
@@ -991,7 +1204,7 @@ test('agent draft result publishes before entering world and uses published prev
|
||||
|
||||
return (
|
||||
<AuthUiContext.Provider value={createAuthValue()}>
|
||||
<RpgEntryFlowShell
|
||||
<RpgEntryFlowShell
|
||||
selectionStage={selectionStage}
|
||||
setSelectionStage={setSelectionStage}
|
||||
gameState={{} as GameState}
|
||||
@@ -1023,11 +1236,13 @@ test('agent draft result publishes before entering world and uses published prev
|
||||
);
|
||||
});
|
||||
expect(
|
||||
vi.mocked(executeRpgCreationAction).mock.calls.some(
|
||||
([sessionId, payload]) =>
|
||||
sessionId === 'custom-world-agent-session-1' &&
|
||||
payload?.action === 'sync_result_profile',
|
||||
),
|
||||
vi
|
||||
.mocked(executeRpgCreationAction)
|
||||
.mock.calls.some(
|
||||
([sessionId, payload]) =>
|
||||
sessionId === 'custom-world-agent-session-1' &&
|
||||
payload?.action === 'sync_result_profile',
|
||||
),
|
||||
).toBe(false);
|
||||
await waitFor(() => {
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledWith(
|
||||
@@ -1219,11 +1434,13 @@ test('agent draft result back button returns to creation hub without redundant s
|
||||
});
|
||||
|
||||
expect(
|
||||
vi.mocked(executeRpgCreationAction).mock.calls.some(
|
||||
([sessionId, payload]) =>
|
||||
sessionId === 'custom-world-agent-session-1' &&
|
||||
payload?.action === 'sync_result_profile',
|
||||
),
|
||||
vi
|
||||
.mocked(executeRpgCreationAction)
|
||||
.mock.calls.some(
|
||||
([sessionId, payload]) =>
|
||||
sessionId === 'custom-world-agent-session-1' &&
|
||||
payload?.action === 'sync_result_profile',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(screen.queryByText('世界档案')).toBeNull();
|
||||
});
|
||||
@@ -1366,7 +1583,9 @@ test('agent draft result auto-save persists the latest profile rebuilt from sync
|
||||
expect(upsertRpgWorldProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const latestSavedProfile = vi.mocked(upsertRpgWorldProfile).mock.calls.at(-1)?.[0];
|
||||
const latestSavedProfile = vi
|
||||
.mocked(upsertRpgWorldProfile)
|
||||
.mock.calls.at(-1)?.[0];
|
||||
expect(latestSavedProfile?.name).toBe('潮雾列岛·session最新版');
|
||||
expect(latestSavedProfile?.summary).toBe(
|
||||
'作品库应该保存这份同步后的最新快照。',
|
||||
@@ -1391,7 +1610,8 @@ test('agent draft result can open from server result preview without embedded le
|
||||
settingText: '被海雾吞没的旧航路群岛',
|
||||
name: '潮雾列岛·服务端预览',
|
||||
subtitle: '结果页改为优先消费 session.resultPreview',
|
||||
summary: '即使 draft 中没有 legacyResultProfile,也应该正常打开结果页。',
|
||||
summary:
|
||||
'即使 draft 中没有 legacyResultProfile,也应该正常打开结果页。',
|
||||
tone: '压抑、潮湿、悬疑',
|
||||
playerGoal: '查清沉船与禁航区异动的真相。',
|
||||
templateWorldType: 'WUXIA',
|
||||
@@ -1420,9 +1640,7 @@ test('agent draft result can open from server result preview without embedded le
|
||||
await waitFor(
|
||||
async () => {
|
||||
expect(await screen.findByText('世界档案')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('潮雾列岛·服务端预览'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('潮雾列岛·服务端预览')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('结果页改为优先消费 session.resultPreview'),
|
||||
).toBeTruthy();
|
||||
@@ -1454,6 +1672,37 @@ test('authenticated users with save archives default into the saves tab', async
|
||||
expect(screen.queryByText('SAVE ARCHIVE')).toBeNull();
|
||||
});
|
||||
|
||||
test('manual tab switch is preserved after platform bootstrap requests finish', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
let resolveGalleryRequest!: (value: []) => void;
|
||||
const delayedGalleryRequest = new Promise<[]>((resolve) => {
|
||||
resolveGalleryRequest = resolve;
|
||||
});
|
||||
|
||||
vi.mocked(listRpgEntryWorldGallery).mockReturnValueOnce(
|
||||
delayedGalleryRequest as Promise<[]>,
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await clickFirstButtonByName(user, '创作');
|
||||
expect(await screen.findByText('角色扮演 RPG')).toBeTruthy();
|
||||
|
||||
resolveGalleryRequest([]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getByText('角色扮演 RPG'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(getPlatformTabPanel('create').getAttribute('aria-hidden')).toBe(
|
||||
'false',
|
||||
);
|
||||
expect(getPlatformTabPanel('home').getAttribute('aria-hidden')).toBe('true');
|
||||
});
|
||||
|
||||
test('save tab can resume a selected archive directly into the game', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleContinueGame = vi.fn();
|
||||
@@ -1504,7 +1753,7 @@ test('save tab can resume a selected archive directly into the game', async () =
|
||||
});
|
||||
});
|
||||
|
||||
test('owned world detail can delete a work and return to the create tab list', async () => {
|
||||
test('creation hub published work can open detail view before deleting from detail page', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
@@ -1572,7 +1821,7 @@ test('owned world detail can delete a work and return to the create tab list', a
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /进入世界/u }));
|
||||
await user.click(await screen.findByRole('button', { name: /查看详情/u }));
|
||||
await user.click(await screen.findByRole('button', { name: '删除作品' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1650,9 +1899,168 @@ test('creation hub published work enters existing detail view', async () => {
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /进入世界/u }));
|
||||
await user.click(await screen.findByRole('button', { name: /查看详情/u }));
|
||||
|
||||
expect(await screen.findByText('世界信息')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '开始游戏' })).toBeTruthy();
|
||||
expect(screen.getByText('已发布')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('creation hub published work experience button enters world directly', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCustomWorldSelect = vi.fn();
|
||||
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([
|
||||
{
|
||||
workId: 'published:world-experience-1',
|
||||
sourceType: 'published_profile',
|
||||
status: 'published',
|
||||
title: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summary: '已经发布的群岛世界作品。',
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||||
stage: null,
|
||||
stageLabel: '已发布',
|
||||
playableNpcCount: 3,
|
||||
landmarkCount: 4,
|
||||
roleVisualReadyCount: 1,
|
||||
roleAnimationReadyCount: 0,
|
||||
roleAssetSummaryLabel: null,
|
||||
sessionId: null,
|
||||
profileId: 'world-experience-1',
|
||||
canResume: false,
|
||||
canEnterWorld: true,
|
||||
},
|
||||
]);
|
||||
vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([
|
||||
{
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'world-experience-1',
|
||||
profile: {
|
||||
id: 'world-experience-1',
|
||||
name: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summary: '已经发布的群岛世界作品。',
|
||||
tone: '压抑、潮湿、悬疑',
|
||||
playerGoal: '查清群岛旧案。',
|
||||
majorFactions: ['守灯会'],
|
||||
coreConflicts: ['假航灯正在扰乱航线'],
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
landmarks: [],
|
||||
} as never,
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||||
authorDisplayName: '测试玩家',
|
||||
worldName: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summaryText: '已经发布的群岛世界作品。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'tide',
|
||||
playableNpcCount: 3,
|
||||
landmarkCount: 4,
|
||||
},
|
||||
]);
|
||||
|
||||
render(
|
||||
<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />,
|
||||
);
|
||||
|
||||
await openCreationHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: '体验' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'world-experience-1',
|
||||
name: '潮雾列岛',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('世界信息')).toBeNull();
|
||||
});
|
||||
|
||||
test('creation hub published work delete button removes the work directly from card list', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
const publishedWork = {
|
||||
workId: 'published:world-card-delete-1',
|
||||
sourceType: 'published_profile' as const,
|
||||
status: 'published' as const,
|
||||
title: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summary: '用于测试卡片删除流程的作品。',
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image' as const,
|
||||
coverCharacterImageSrcs: [],
|
||||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||||
publishedAt: '2026-04-16T12:00:00.000Z',
|
||||
stage: null,
|
||||
stageLabel: '已发布',
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
roleVisualReadyCount: 0,
|
||||
roleAnimationReadyCount: 0,
|
||||
roleAssetSummaryLabel: null,
|
||||
sessionId: null,
|
||||
profileId: 'world-card-delete-1',
|
||||
canResume: false,
|
||||
canEnterWorld: true,
|
||||
};
|
||||
const publishedLibraryEntry = {
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'world-card-delete-1',
|
||||
profile: {
|
||||
id: 'world-card-delete-1',
|
||||
name: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summary: '用于测试卡片删除流程的作品。',
|
||||
tone: '压抑、潮湿、悬疑',
|
||||
playerGoal: '查清旧案。',
|
||||
majorFactions: ['守灯会'],
|
||||
coreConflicts: ['雾潮正在逼近港口'],
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
landmarks: [],
|
||||
} as never,
|
||||
visibility: 'published' as const,
|
||||
publishedAt: '2026-04-16T12:00:00.000Z',
|
||||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||||
authorDisplayName: '测试玩家',
|
||||
worldName: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summaryText: '用于测试卡片删除流程的作品。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'tide' as const,
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
};
|
||||
|
||||
vi.mocked(listRpgCreationWorks)
|
||||
.mockResolvedValueOnce([publishedWork])
|
||||
.mockResolvedValue([]);
|
||||
vi.mocked(listRpgEntryWorldLibrary)
|
||||
.mockResolvedValueOnce([publishedLibraryEntry])
|
||||
.mockResolvedValue([]);
|
||||
vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: '删除' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteRpgEntryWorldProfile).toHaveBeenCalledWith(
|
||||
'world-card-delete-1',
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('还没有作品')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import type {
|
||||
CustomWorldWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { ApiClientError, isTimeoutError } from '../../services/apiClient';
|
||||
import { buildCustomWorldCreatorIntentFoundationText } from '../../services/customWorldCreatorIntent';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
@@ -11,6 +12,28 @@ export function resolveRpgEntryErrorMessage(
|
||||
error: unknown,
|
||||
fallback: string,
|
||||
) {
|
||||
if (isTimeoutError(error)) {
|
||||
if (/拼图/u.test(fallback)) {
|
||||
return '开启拼图创作工作台超时,请确认运行时后端已启动后重试。';
|
||||
}
|
||||
if (/大鱼吃小鱼/u.test(fallback)) {
|
||||
return '开启大鱼吃小鱼创作工作台超时,请确认运行时后端已启动后重试。';
|
||||
}
|
||||
if (/共创工作台/u.test(fallback)) {
|
||||
return '开启创作工作台超时,请确认运行时后端已启动后重试。';
|
||||
}
|
||||
return '请求超时,请稍后重试。';
|
||||
}
|
||||
|
||||
if (
|
||||
error instanceof ApiClientError &&
|
||||
error.status === 401 &&
|
||||
(error.code === 'UNAUTHORIZED' ||
|
||||
error.message.includes('Authorization Bearer Token'))
|
||||
) {
|
||||
return '当前登录状态已失效,请重新登录后继续。';
|
||||
}
|
||||
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ export function useRpgCreationSessionController(
|
||||
onSessionOpened,
|
||||
} = params;
|
||||
const initialAgentUiStateRef = useRef(readCustomWorldAgentUiState());
|
||||
const isHydratingInitialAgentWorkspaceRef = useRef(
|
||||
Boolean(initialAgentUiStateRef.current.activeSessionId),
|
||||
);
|
||||
const hasAppliedInitialAgentWorkspaceRef = useRef(false);
|
||||
const hasRequestedInitialAgentWorkspaceAuthRef = useRef(false);
|
||||
const isAgentDraftResultAutoOpenSuppressedRef = useRef(false);
|
||||
@@ -77,6 +80,8 @@ export function useRpgCreationSessionController(
|
||||
const [isStreamingAgentReply, setIsStreamingAgentReply] = useState(false);
|
||||
const [isLoadingAgentSession, setIsLoadingAgentSession] = useState(false);
|
||||
const [creationTypeError, setCreationTypeError] = useState<string | null>(null);
|
||||
const [agentWorkspaceRestoreError, setAgentWorkspaceRestoreError] =
|
||||
useState<string | null>(null);
|
||||
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
||||
useState<CustomWorldProfile | null>(null);
|
||||
const [customWorldError, setCustomWorldError] = useState<string | null>(null);
|
||||
@@ -135,6 +140,8 @@ export function useRpgCreationSessionController(
|
||||
setIsLoadingAgentSession(false);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setAgentWorkspaceRestoreError(null);
|
||||
isHydratingInitialAgentWorkspaceRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,16 +151,22 @@ export function useRpgCreationSessionController(
|
||||
setIsLoadingAgentSession(false);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setAgentWorkspaceRestoreError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const isInitialWorkspaceRestore =
|
||||
isHydratingInitialAgentWorkspaceRef.current &&
|
||||
activeAgentSessionId === initialAgentUiStateRef.current.activeSessionId;
|
||||
setIsLoadingAgentSession(true);
|
||||
|
||||
void syncAgentSessionSnapshot(activeAgentSessionId)
|
||||
.then(() => {
|
||||
if (!cancelled) {
|
||||
setCreationTypeError(null);
|
||||
setAgentWorkspaceRestoreError(null);
|
||||
isHydratingInitialAgentWorkspaceRef.current = false;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -161,13 +174,20 @@ export function useRpgCreationSessionController(
|
||||
return;
|
||||
}
|
||||
|
||||
setCreationTypeError(
|
||||
resolveRpgCreationErrorMessage(error, '读取 Agent 共创工作区失败。'),
|
||||
);
|
||||
// 登录后自动恢复的是“上一次残留的工作区指针”,
|
||||
// 这里失败时应优先静默清理,避免把旧恢复错误冒充成当前登录已失效。
|
||||
if (isInitialWorkspaceRestore) {
|
||||
setAgentWorkspaceRestoreError(null);
|
||||
} else {
|
||||
setAgentWorkspaceRestoreError(
|
||||
resolveRpgCreationErrorMessage(error, '读取 Agent 共创工作区失败。'),
|
||||
);
|
||||
}
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
isHydratingInitialAgentWorkspaceRef.current = false;
|
||||
persistAgentUiState(null, null);
|
||||
enterCreateTab?.();
|
||||
setSelectionStage('platform');
|
||||
@@ -353,6 +373,7 @@ export function useRpgCreationSessionController(
|
||||
const { session } = await createRpgCreationSession(
|
||||
seedText ? { seedText } : {},
|
||||
);
|
||||
isHydratingInitialAgentWorkspaceRef.current = false;
|
||||
setAgentSession(session);
|
||||
setAgentOperation(null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
@@ -539,6 +560,7 @@ export function useRpgCreationSessionController(
|
||||
isLoadingAgentSession,
|
||||
creationTypeError,
|
||||
setCreationTypeError,
|
||||
agentWorkspaceRestoreError,
|
||||
customWorldError,
|
||||
setCustomWorldError,
|
||||
generatedCustomWorldProfile,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { resolveRpgEntryErrorMessage } from './rpgEntryShared';
|
||||
|
||||
type UseRpgEntryBootstrapParams = {
|
||||
user: AuthUser | null | undefined;
|
||||
canAccessProtectedData?: boolean | undefined;
|
||||
getProfileDashboard: () => Promise<ProfileDashboardSummary | null>;
|
||||
handleContinueGame: (
|
||||
snapshot?: HydratedSavedGameSnapshot | null,
|
||||
@@ -38,12 +39,19 @@ type UseRpgEntryBootstrapParams = {
|
||||
export function useRpgEntryBootstrap(
|
||||
params: UseRpgEntryBootstrapParams,
|
||||
) {
|
||||
const { user, getProfileDashboard, handleContinueGame, hasInitialAgentSession } =
|
||||
params;
|
||||
const {
|
||||
user,
|
||||
canAccessProtectedData = Boolean(user),
|
||||
getProfileDashboard,
|
||||
handleContinueGame,
|
||||
hasInitialAgentSession,
|
||||
} = params;
|
||||
const isAuthenticated = Boolean(user);
|
||||
const canReadProtectedData = Boolean(user) && canAccessProtectedData;
|
||||
const platformTabBootstrapUserIdRef = useRef<string | null | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const hasExplicitPlatformTabSelectionRef = useRef(false);
|
||||
|
||||
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
|
||||
CustomWorldLibraryEntry<CustomWorldProfile>[]
|
||||
@@ -58,7 +66,7 @@ export function useRpgEntryBootstrap(
|
||||
PlatformBrowseHistoryEntry[]
|
||||
>([]);
|
||||
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>([]);
|
||||
const [platformTab, setPlatformTab] = useState<PlatformHomeTab>('home');
|
||||
const [platformTab, setPlatformTabState] = useState<PlatformHomeTab>('home');
|
||||
const [platformError, setPlatformError] = useState<string | null>(null);
|
||||
const [dashboardError, setDashboardError] = useState<string | null>(null);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
@@ -71,8 +79,15 @@ export function useRpgEntryBootstrap(
|
||||
const [profileDashboard, setProfileDashboard] =
|
||||
useState<ProfileDashboardSummary | null>(null);
|
||||
|
||||
const setPlatformTab = useCallback((nextTab: PlatformHomeTab) => {
|
||||
// 区分“平台首屏默认落点”和“用户/流程显式切换”。
|
||||
// 一旦显式切过 Tab,就不能再被首屏异步请求回刷成首页或存档。
|
||||
hasExplicitPlatformTabSelectionRef.current = true;
|
||||
setPlatformTabState(nextTab);
|
||||
}, []);
|
||||
|
||||
const refreshProfileDashboard = useCallback(async () => {
|
||||
if (!user) {
|
||||
if (!user || !canReadProtectedData) {
|
||||
setProfileDashboard(null);
|
||||
setDashboardError(null);
|
||||
setIsLoadingDashboard(false);
|
||||
@@ -91,10 +106,10 @@ export function useRpgEntryBootstrap(
|
||||
} finally {
|
||||
setIsLoadingDashboard(false);
|
||||
}
|
||||
}, [getProfileDashboard, user]);
|
||||
}, [canReadProtectedData, getProfileDashboard, user]);
|
||||
|
||||
const refreshCustomWorldWorks = useCallback(async () => {
|
||||
if (!user) {
|
||||
if (!user || !canReadProtectedData) {
|
||||
setCustomWorldWorkEntries([]);
|
||||
return [];
|
||||
}
|
||||
@@ -102,7 +117,7 @@ export function useRpgEntryBootstrap(
|
||||
const nextItems = await listRpgCreationWorks();
|
||||
setCustomWorldWorkEntries(nextItems);
|
||||
return nextItems;
|
||||
}, [user]);
|
||||
}, [canReadProtectedData, user]);
|
||||
|
||||
const refreshPublishedGallery = useCallback(async () => {
|
||||
const nextEntries = await listRpgEntryWorldGallery();
|
||||
@@ -111,7 +126,7 @@ export function useRpgEntryBootstrap(
|
||||
}, []);
|
||||
|
||||
const refreshSavedCustomWorldLibrary = useCallback(async () => {
|
||||
if (!user) {
|
||||
if (!user || !canReadProtectedData) {
|
||||
setSavedCustomWorldEntries([]);
|
||||
return [];
|
||||
}
|
||||
@@ -119,7 +134,7 @@ export function useRpgEntryBootstrap(
|
||||
const nextEntries = await listRpgEntryWorldLibrary();
|
||||
setSavedCustomWorldEntries(nextEntries);
|
||||
return nextEntries;
|
||||
}, [user]);
|
||||
}, [canReadProtectedData, user]);
|
||||
|
||||
const appendBrowseHistoryEntry = useCallback(
|
||||
async (entry: PlatformBrowseHistoryWriteEntry) => {
|
||||
@@ -139,7 +154,7 @@ export function useRpgEntryBootstrap(
|
||||
|
||||
const handleResumeSaveEntry = useCallback(
|
||||
async (entry: ProfileSaveArchiveSummary) => {
|
||||
if (!user || isResumingSaveWorldKey) {
|
||||
if (!user || !canReadProtectedData || isResumingSaveWorldKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,11 +177,20 @@ export function useRpgEntryBootstrap(
|
||||
setIsResumingSaveWorldKey(null);
|
||||
}
|
||||
},
|
||||
[handleContinueGame, isResumingSaveWorldKey, user],
|
||||
[canReadProtectedData, handleContinueGame, isResumingSaveWorldKey, user],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
const nextPlatformBootstrapUserId = user?.id ?? null;
|
||||
const shouldApplyInitialPlatformTab =
|
||||
platformTabBootstrapUserIdRef.current !== nextPlatformBootstrapUserId;
|
||||
|
||||
if (shouldApplyInitialPlatformTab) {
|
||||
// 在请求发出前先占位,避免首屏请求未完成时用户切了 Tab,
|
||||
// 返回结果又被误判成“还没初始化过”并强制跳回默认页。
|
||||
platformTabBootstrapUserIdRef.current = nextPlatformBootstrapUserId;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
setHistoryEntries([]);
|
||||
@@ -174,9 +198,9 @@ export function useRpgEntryBootstrap(
|
||||
setSaveError(null);
|
||||
setIsLoadingPlatform(true);
|
||||
setPlatformError(null);
|
||||
setIsLoadingDashboard(isAuthenticated);
|
||||
setIsLoadingDashboard(canReadProtectedData);
|
||||
setDashboardError(null);
|
||||
if (!isAuthenticated) {
|
||||
if (!canReadProtectedData) {
|
||||
setSavedCustomWorldEntries([]);
|
||||
setCustomWorldWorkEntries([]);
|
||||
setSaveEntries([]);
|
||||
@@ -192,16 +216,20 @@ export function useRpgEntryBootstrap(
|
||||
historyResult,
|
||||
saveArchivesResult,
|
||||
] = await Promise.allSettled([
|
||||
isAuthenticated
|
||||
canReadProtectedData
|
||||
? listRpgEntryWorldLibrary()
|
||||
: Promise.resolve([]),
|
||||
isAuthenticated
|
||||
canReadProtectedData
|
||||
? listRpgCreationWorks()
|
||||
: Promise.resolve([]),
|
||||
listRpgEntryWorldGallery(),
|
||||
isAuthenticated ? getProfileDashboard() : Promise.resolve(null),
|
||||
isAuthenticated ? listRpgProfileBrowseHistory() : Promise.resolve([]),
|
||||
isAuthenticated ? listRpgProfileSaveArchives() : Promise.resolve([]),
|
||||
canReadProtectedData ? getProfileDashboard() : Promise.resolve(null),
|
||||
canReadProtectedData
|
||||
? listRpgProfileBrowseHistory()
|
||||
: Promise.resolve([]),
|
||||
canReadProtectedData
|
||||
? listRpgProfileSaveArchives()
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
if (!isActive) {
|
||||
@@ -227,8 +255,10 @@ export function useRpgEntryBootstrap(
|
||||
}
|
||||
|
||||
if (
|
||||
(isAuthenticated && libraryEntriesResult.status === 'rejected') ||
|
||||
(isAuthenticated && workEntriesResult.status === 'rejected') ||
|
||||
(canReadProtectedData &&
|
||||
libraryEntriesResult.status === 'rejected') ||
|
||||
(canReadProtectedData &&
|
||||
workEntriesResult.status === 'rejected') ||
|
||||
galleryEntriesResult.status === 'rejected'
|
||||
) {
|
||||
const platformFailure =
|
||||
@@ -246,7 +276,7 @@ export function useRpgEntryBootstrap(
|
||||
|
||||
if (dashboardResult.status === 'fulfilled') {
|
||||
setProfileDashboard(dashboardResult.value);
|
||||
} else if (isAuthenticated) {
|
||||
} else if (canReadProtectedData) {
|
||||
setProfileDashboard(null);
|
||||
setDashboardError(
|
||||
resolveRpgEntryErrorMessage(
|
||||
@@ -258,7 +288,7 @@ export function useRpgEntryBootstrap(
|
||||
|
||||
if (historyResult.status === 'fulfilled') {
|
||||
setHistoryEntries(historyResult.value);
|
||||
} else if (isAuthenticated) {
|
||||
} else if (canReadProtectedData) {
|
||||
setHistoryError(
|
||||
resolveRpgEntryErrorMessage(historyResult.reason, '读取浏览历史失败。'),
|
||||
);
|
||||
@@ -266,7 +296,7 @@ export function useRpgEntryBootstrap(
|
||||
|
||||
if (saveArchivesResult.status === 'fulfilled') {
|
||||
setSaveEntries(saveArchivesResult.value);
|
||||
} else if (isAuthenticated) {
|
||||
} else if (canReadProtectedData) {
|
||||
setSaveEntries([]);
|
||||
setSaveError(
|
||||
resolveRpgEntryErrorMessage(
|
||||
@@ -276,20 +306,19 @@ export function useRpgEntryBootstrap(
|
||||
);
|
||||
}
|
||||
|
||||
const nextPlatformBootstrapUserId = user?.id ?? null;
|
||||
if (
|
||||
platformTabBootstrapUserIdRef.current !== nextPlatformBootstrapUserId
|
||||
shouldApplyInitialPlatformTab &&
|
||||
!hasInitialAgentSession &&
|
||||
!hasExplicitPlatformTabSelectionRef.current
|
||||
) {
|
||||
platformTabBootstrapUserIdRef.current = nextPlatformBootstrapUserId;
|
||||
if (!hasInitialAgentSession) {
|
||||
setPlatformTab(
|
||||
isAuthenticated &&
|
||||
saveArchivesResult.status === 'fulfilled' &&
|
||||
saveArchivesResult.value.length > 0
|
||||
? 'saves'
|
||||
: 'home',
|
||||
);
|
||||
}
|
||||
setPlatformTabState(
|
||||
isAuthenticated &&
|
||||
canReadProtectedData &&
|
||||
saveArchivesResult.status === 'fulfilled' &&
|
||||
saveArchivesResult.value.length > 0
|
||||
? 'saves'
|
||||
: 'home',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (isActive) {
|
||||
@@ -302,10 +331,17 @@ export function useRpgEntryBootstrap(
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [getProfileDashboard, hasInitialAgentSession, isAuthenticated, user]);
|
||||
}, [
|
||||
canReadProtectedData,
|
||||
getProfileDashboard,
|
||||
hasInitialAgentSession,
|
||||
isAuthenticated,
|
||||
user,
|
||||
]);
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
canReadProtectedData,
|
||||
platformTab,
|
||||
setPlatformTab,
|
||||
savedCustomWorldEntries,
|
||||
|
||||
Reference in New Issue
Block a user