fix: stabilize rpg publish and launch

This commit is contained in:
kdletters
2026-05-21 20:20:06 +08:00
parent 224a26d318
commit a9d23a8a44
14 changed files with 614 additions and 82 deletions

View File

@@ -4,6 +4,8 @@ import { act, render } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
import type { CustomWorldProfileRecord } from '../../../packages/shared/src/contracts/runtime';
import { type CustomWorldProfile, WorldType } from '../../types';
import { useRpgCreationEnterWorld } from './useRpgCreationEnterWorld';
@@ -69,7 +71,9 @@ function buildProfile(params: {
};
}
function buildSession(): CustomWorldAgentSessionSnapshot {
function buildSession(
stage: CustomWorldAgentSessionSnapshot['stage'] = 'ready_to_publish',
): CustomWorldAgentSessionSnapshot {
return {
sessionId: 'session-1',
currentTurn: 1,
@@ -85,7 +89,7 @@ function buildSession(): CustomWorldAgentSessionSnapshot {
},
progressPercent: 100,
lastAssistantReply: '',
stage: 'ready_to_publish',
stage,
focusCardId: null,
creatorIntent: null,
creatorIntentReadiness: {
@@ -113,6 +117,31 @@ function buildSession(): CustomWorldAgentSessionSnapshot {
};
}
function buildResultView(params: {
stage?: CustomWorldAgentSessionSnapshot['stage'];
profile: CustomWorldProfile | null;
canEnterWorld?: boolean;
}): RpgCreationResultView {
const stage = params.stage ?? 'ready_to_publish';
const profileRecord = params.profile
? (structuredClone(params.profile) as unknown as CustomWorldProfileRecord)
: null;
return {
session: buildSession(stage),
profile: profileRecord,
profileSource: profileRecord ? 'result_preview' : 'none',
targetStage: 'custom-world-result',
generationViewSource: null,
resultViewSource: profileRecord ? 'agent-draft' : null,
canAutosaveLibrary: true,
canSyncResultProfile: stage !== 'published',
publishReady: true,
canEnterWorld: params.canEnterWorld ?? stage === 'published',
blockerCount: 0,
recoveryAction: 'open_result',
};
}
describe('useRpgCreationEnterWorld', () => {
it('Agent 草稿测试进入游戏时优先使用结果页当前 profile而不是回退到会话快照', async () => {
const resultProfile = buildProfile({
@@ -167,4 +196,148 @@ describe('useRpgCreationEnterWorld', () => {
handleCustomWorldSelect.mock.calls[0]?.[0].playableNpcs[0]?.imageSrc,
).toBe('/generated-characters/draft-role/portrait.png');
});
it('Agent 草稿发布时先保存当前结果页 profile再发送 publish_world 并回读结果页', async () => {
const resultProfile = buildProfile({
id: 'draft-profile',
name: '发布前填写内容',
imageSrc: '/generated-characters/draft-role/portrait.png',
});
const syncedProfile = buildProfile({
id: 'draft-profile',
name: '已保存的填写内容',
imageSrc: '/generated-characters/draft-role/synced.png',
});
const publishedProfile = buildProfile({
id: 'draft-profile',
name: '已发布世界',
imageSrc: '/generated-characters/draft-role/published.png',
});
const callOrder: string[] = [];
const handleCustomWorldSelect = vi.fn();
const setGeneratedCustomWorldProfile = vi.fn();
const syncAgentDraftResultProfile = vi.fn(async () => {
callOrder.push('save');
return {
profile: syncedProfile,
view: buildResultView({
stage: 'ready_to_publish',
profile: syncedProfile,
canEnterWorld: false,
}),
};
});
const executePublishWorld = vi.fn(async () => {
callOrder.push('publish');
return buildSession('published');
});
const syncAgentCreationResultView = vi.fn(async () => {
callOrder.push('reload');
return buildResultView({
stage: 'published',
profile: publishedProfile,
canEnterWorld: true,
});
});
function Harness() {
const { publishCurrentResult } = useRpgCreationEnterWorld({
isAgentDraftResultView: true,
activeAgentSessionId: 'session-1',
currentAgentSessionStage: 'ready_to_publish',
generatedCustomWorldProfile: resultProfile,
handleCustomWorldSelect,
syncAgentDraftResultProfile,
executePublishWorld,
syncAgentCreationResultView,
setGeneratedCustomWorldProfile,
});
return (
<button type="button" onClick={() => void publishCurrentResult()}>
</button>
);
}
const { getByText } = render(<Harness />);
await act(async () => {
getByText('发布').click();
});
expect(callOrder).toEqual(['save', 'publish', 'reload']);
expect(syncAgentDraftResultProfile).toHaveBeenCalledWith(resultProfile);
expect(executePublishWorld).toHaveBeenCalledTimes(1);
expect(syncAgentCreationResultView).toHaveBeenCalledWith('session-1');
expect(setGeneratedCustomWorldProfile).toHaveBeenCalledWith(syncedProfile);
expect(
setGeneratedCustomWorldProfile.mock.calls.at(-1)?.[0]?.id,
).toBe('draft-profile');
expect(
setGeneratedCustomWorldProfile.mock.calls.at(-1)?.[0]?.playableNpcs[0]
?.imageSrc,
).toBe('/generated-characters/draft-role/published.png');
expect(handleCustomWorldSelect).not.toHaveBeenCalled();
});
it('Agent 会话已发布后点击进入世界不再重复发送 publish_world', async () => {
const resultProfile = buildProfile({
id: 'published-profile',
name: '已发布世界',
imageSrc: '/generated-characters/published-role/portrait.png',
});
const publishedView = buildResultView({
stage: 'published',
profile: resultProfile,
canEnterWorld: true,
});
const handleCustomWorldSelect = vi.fn();
const setGeneratedCustomWorldProfile = vi.fn();
const executePublishWorld = vi.fn(async () => buildSession('published'));
const syncAgentCreationResultView = vi.fn(async () => publishedView);
const syncAgentDraftResultProfile = vi.fn(async () => ({
profile: resultProfile,
view: null,
}));
function Harness() {
const { enterWorldFromCurrentResult } = useRpgCreationEnterWorld({
isAgentDraftResultView: true,
activeAgentSessionId: 'session-1',
currentAgentSessionStage: 'published',
generatedCustomWorldProfile: resultProfile,
handleCustomWorldSelect,
syncAgentDraftResultProfile,
executePublishWorld,
syncAgentCreationResultView,
setGeneratedCustomWorldProfile,
});
return (
<button
type="button"
onClick={() => void enterWorldFromCurrentResult()}
>
</button>
);
}
const { getByText } = render(<Harness />);
await act(async () => {
getByText('进入世界').click();
});
expect(syncAgentDraftResultProfile).not.toHaveBeenCalled();
expect(executePublishWorld).not.toHaveBeenCalled();
expect(syncAgentCreationResultView).toHaveBeenCalledWith('session-1');
expect(setGeneratedCustomWorldProfile).toHaveBeenCalledTimes(1);
expect(setGeneratedCustomWorldProfile.mock.calls[0]?.[0]?.id).toBe(
'published-profile',
);
expect(handleCustomWorldSelect).toHaveBeenCalledTimes(1);
expect(handleCustomWorldSelect.mock.calls[0]?.[0]?.id).toBe(
'published-profile',
);
});
});