fix creation agent session sync and publish gate alignment
This commit is contained in:
@@ -188,10 +188,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSelectionStage,
|
||||
setSelectedDetailEntry,
|
||||
});
|
||||
const { setPlatformTab } = platformBootstrap;
|
||||
|
||||
const enterCreateTab = useCallback(() => {
|
||||
platformBootstrap.setPlatformTab('create');
|
||||
}, [platformBootstrap]);
|
||||
// 只依赖稳定的 setter,避免把 bootstrap 对象的 render 级引用变化
|
||||
// 传导成 Agent session 恢复 effect 的重复触发。
|
||||
setPlatformTab('create');
|
||||
}, [setPlatformTab]);
|
||||
|
||||
const sessionController = useRpgCreationSessionController({
|
||||
userId: authUi?.user?.id,
|
||||
|
||||
@@ -699,7 +699,31 @@ test('create hub exposes direct template entry, keeps AIRP and visual novel lock
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('create tab opens compiled agent draft in result refinement page', async () => {
|
||||
test('opening RPG agent workspace does not refetch session snapshot in a render loop', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openNewRpgCreation(user);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'Agent工作区:custom-world-agent-session-1',
|
||||
{},
|
||||
{ timeout: 5000 },
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
window.setTimeout(resolve, 120);
|
||||
});
|
||||
|
||||
expect(getRpgCreationSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'create tab opens compiled agent draft in result refinement page',
|
||||
async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([
|
||||
@@ -752,7 +776,9 @@ test('create tab opens compiled agent draft in result refinement page', async ()
|
||||
screen.queryByText('Agent工作区:custom-world-agent-session-1'),
|
||||
).toBeNull();
|
||||
expect(screen.getByRole('button', { name: /返回创作/u })).toBeTruthy();
|
||||
});
|
||||
},
|
||||
10000,
|
||||
);
|
||||
|
||||
test('create tab resumes agent workspace when draft has no compiled result yet', async () => {
|
||||
const user = userEvent.setup();
|
||||
@@ -1258,6 +1284,153 @@ test('agent draft result publishes before entering world and uses published prev
|
||||
});
|
||||
});
|
||||
|
||||
test('agent result view does not keep legacy publish blockers when preview uses anchorContent and sceneChapterBlueprints', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([
|
||||
{
|
||||
workId: 'draft:custom-world-agent-session-1',
|
||||
sourceType: 'agent_session',
|
||||
status: 'draft',
|
||||
title: '潮雾列岛',
|
||||
subtitle: '待发布草稿',
|
||||
summary: '当前草稿已经补齐八锚点与第一幕。',
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
stage: 'ready_to_publish',
|
||||
stageLabel: '待发布草稿',
|
||||
playableNpcCount: 3,
|
||||
landmarkCount: 1,
|
||||
roleVisualReadyCount: 1,
|
||||
roleAnimationReadyCount: 0,
|
||||
roleAssetSummaryLabel: null,
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
profileId: null,
|
||||
canResume: true,
|
||||
canEnterWorld: false,
|
||||
},
|
||||
]);
|
||||
vi.mocked(getRpgCreationOperation).mockResolvedValue({
|
||||
operationId: 'operation-draft-foundation-1',
|
||||
type: 'draft_foundation',
|
||||
status: 'completed',
|
||||
phaseLabel: '世界底稿已生成',
|
||||
phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue({
|
||||
...compiledAgentDraftSession,
|
||||
stage: 'ready_to_publish',
|
||||
resultPreview: {
|
||||
...compiledAgentDraftSession.resultPreview!,
|
||||
publishReady: true,
|
||||
blockers: [],
|
||||
preview: {
|
||||
...compiledAgentDraftSession.resultPreview!.preview,
|
||||
settingText: '被海雾吞没的旧航路群岛',
|
||||
anchorContent: {
|
||||
worldPromise: {
|
||||
hook: '被海雾吞没的旧航路群岛',
|
||||
differentiator: '灯塔与禁航令共同决定谁能穿过死潮。',
|
||||
desiredExperience: '压抑、潮湿、悬疑',
|
||||
},
|
||||
playerFantasy: {
|
||||
playerRole: '玩家是被迫返乡的守灯人继承者。',
|
||||
corePursuit: '查清沉船夜与假航灯的关系。',
|
||||
fearOfLoss: '失去家族最后一条可信航线。',
|
||||
},
|
||||
themeBoundary: {
|
||||
toneKeywords: ['压抑', '悬疑'],
|
||||
aestheticDirectives: ['潮湿群岛', '冷雾港口'],
|
||||
forbiddenDirectives: ['轻喜冒险'],
|
||||
},
|
||||
playerEntryPoint: {
|
||||
openingIdentity: '返乡守灯人继承者',
|
||||
openingProblem: '回港首夜撞见禁航区假航灯重亮',
|
||||
entryMotivation: '阻止更多船只误入死潮',
|
||||
},
|
||||
coreConflict: {
|
||||
surfaceConflicts: ['守灯会与航运公会争夺航路解释权'],
|
||||
hiddenCrisis: '有人在借假航灯持续清洗旧案证据',
|
||||
firstTouchedConflict: '玩家返乡当夜就被卷进封航冲突',
|
||||
},
|
||||
keyRelationships: [],
|
||||
hiddenLines: {
|
||||
hiddenTruths: ['沉船夜与假航灯骗局属于同一操盘链条'],
|
||||
misdirectionHints: ['表面像海雾自然失控'],
|
||||
revealPacing: '先见异常,再见旧案,再见操盘者',
|
||||
},
|
||||
iconicElements: {
|
||||
iconicMotifs: ['假航灯', '沉钟回响'],
|
||||
institutionsOrArtifacts: ['旧灯塔', '禁航碑'],
|
||||
hardRules: ['错误航灯会把船引进必死水域'],
|
||||
},
|
||||
},
|
||||
creatorIntent: {
|
||||
sourceMode: 'card',
|
||||
rawSettingText: '',
|
||||
worldHook: '被海雾吞没的旧航路群岛',
|
||||
themeKeywords: ['海雾', '旧航路'],
|
||||
toneDirectives: ['压抑', '悬疑'],
|
||||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||||
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
|
||||
keyFactions: [],
|
||||
keyCharacters: [],
|
||||
keyLandmarks: [],
|
||||
iconicElements: ['会移动的海雾'],
|
||||
forbiddenDirectives: [],
|
||||
},
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'scene-chapter-1',
|
||||
sceneId: 'landmark-1',
|
||||
title: '沉钟栈桥章节',
|
||||
summary: '围绕沉钟栈桥推进的三幕结构。',
|
||||
linkedThreadIds: [],
|
||||
linkedLandmarkIds: ['landmark-1'],
|
||||
acts: [
|
||||
{
|
||||
id: 'scene-act-1',
|
||||
sceneId: 'landmark-1',
|
||||
title: '潮声逼近',
|
||||
summary: '第一幕先把潮声与旧钟压上来。',
|
||||
stageCoverage: ['opening'],
|
||||
encounterNpcIds: ['story-1'],
|
||||
primaryNpcId: 'story-1',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_primary_contact',
|
||||
actGoal: '接住首幕压力',
|
||||
transitionHook: '继续逼近钟楼深处。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
expect(await screen.findByRole('button', { name: /继续完善/u })).toBeTruthy();
|
||||
await user.click(await screen.findByRole('button', { name: /继续完善/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /发布并进入世界/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.queryByText(/当前还有 4 个发布阻断项/u)).toBeNull();
|
||||
const actionButton = screen.getByRole('button', {
|
||||
name: /发布并进入世界/u,
|
||||
});
|
||||
expect((actionButton as HTMLButtonElement).disabled).toBe(false);
|
||||
});
|
||||
|
||||
test('agent draft result back button returns to creation hub without redundant sync when session is already latest', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user