This commit is contained in:
2026-04-25 22:19:04 +08:00
parent 2ebfd1cf55
commit 8404081d7b
149 changed files with 10508 additions and 2732 deletions

View File

@@ -0,0 +1,278 @@
/** @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);
});
});