Files
Genarrative/src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx
2026-04-25 22:19:04 +08:00

279 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** @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 { WorldType, type CustomWorldProfile } from '../../types';
import {
executeRpgCreationAction,
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> = {},
): CustomWorldAgentSessionSnapshot {
return {
sessionId: 'agent-session-1',
currentTurn: 1,
anchorContent: {
worldPromise: null,
playerFantasy: null,
themeBoundary: null,
playerEntryPoint: null,
coreConflict: null,
keyRelationships: [],
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,
};
}
describe('RPG Agent 草稿恢复', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('作品摘要已有对象数量但 session 没有 draftProfile 时恢复 Agent 页面', async () => {
const syncAgentSessionSnapshot = vi.fn(async () =>
buildSession({
stage: 'clarifying',
draftProfile: null,
}),
);
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<void>)
| 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,
syncAgentSessionSnapshot,
buildDraftResultProfile: (session) =>
(session?.draftProfile as CustomWorldProfile | null) ?? null,
suppressAgentDraftResultAutoOpen,
releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
resetAutoSaveTrackingToIdle: vi.fn(),
markAutoSavedProfile: vi.fn(),
}).handleOpenCreationWork;
return null;
}
render(<Harness />);
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(syncAgentSessionSnapshot).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 draftProfile不触发 sync_result_profile', async () => {
const oldProfile = buildProfile('旧前端快照');
const latestProfile = {
...buildProfile('服务端草稿快照'),
summary: '自动保存应保存这份 session 最新草稿。',
};
const latestSession = buildSession({
stage: 'object_refining',
draftProfile: latestProfile as unknown as Record<string, unknown>,
});
const syncAgentSessionSnapshot = vi.fn(async () => latestSession);
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,
},
entries: [],
});
function Harness() {
useRpgCreationResultAutosave({
selectionStage: 'custom-world-result',
activeAgentSessionId: 'agent-session-1',
agentSession: buildSession({
stage: 'object_refining',
draftProfile: oldProfile as unknown as Record<string, unknown>,
resultPreview: {
publishReady: false,
blockers: [],
qualityFindings: [],
sourceLabel: '旧预览',
} as never,
}),
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,
buildDraftResultProfile: (session) =>
(session?.draftProfile as CustomWorldProfile | null) ?? null,
});
return null;
}
vi.useFakeTimers();
render(<Harness />);
await act(async () => {
await vi.advanceTimersByTimeAsync(650);
});
vi.useRealTimers();
expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1');
expect(upsertRpgWorldProfile).toHaveBeenCalledWith(latestProfile, {
sourceAgentSessionId: 'agent-session-1',
});
expect(
vi.mocked(executeRpgCreationAction).mock.calls.some(
([, payload]) => payload?.action === 'sync_result_profile',
),
).toBe(false);
});
});