Files
Genarrative/src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx
2026-04-29 11:51:04 +08:00

339 lines
10 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 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> = {},
): 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> = {},
): 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<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,
syncAgentCreationResultView,
buildDraftResultProfile: (view) =>
(view?.profile 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(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<string, unknown>,
});
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(<Harness />);
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',
},
);
});
});