Files
Genarrative/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts
T
kdletters 25761b84a3 合并原生壳桥接统一分支
合并 Expo 移动壳与 Tauri 桌面壳工程
保留创作主页、项目入口和现有生产发布脚本
同步 HostBridge 文档、依赖锁文件和原生壳门禁
2026-06-22 22:28:31 +08:00

903 lines
25 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import { buildCreationWorkShelfItems } from '../custom-world-home/creationWorkShelf';
import {
buildCreationWorkShelfRuntimeState,
buildPendingPuzzleWorks,
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
collectVisibleDraftNoticeKeys,
countUnreadDraftGenerationUpdates,
createPendingDraftShelfState,
type DraftGenerationNoticeMap,
getGenerationNoticeShelfKeys,
hasUnreadDraftGenerationUpdates,
mergeBigFishWorkSummary,
mergePuzzleWorkSummary,
resolveBigFishDraftOpenIntent,
resolveJumpHopDraftOpenIntent,
resolveMatch3DDraftOpenIntent,
resolvePuzzleDraftOpenIntent,
resolveSquareHoleDraftOpenIntent,
resolveVisualNovelDraftOpenIntent,
resolveWoodenFishDraftOpenIntent,
} from './platformDraftGenerationShelfModel';
describe('platformDraftGenerationShelfModel', () => {
test('resolvePuzzleDraftOpenIntent sends published puzzle without session to detail', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork({
sourceSessionId: null,
publicationStatus: 'published',
}),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'open-published-detail',
});
});
test('resolvePuzzleDraftOpenIntent restores failed puzzle generation with notice copy', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork(),
notices: {
'puzzle:puzzle-session-base': {
status: 'failed',
seen: false,
message: '首图生成失败。',
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'failed-generation',
source: 'restored',
errorMessage: '首图生成失败。',
});
});
test('resolvePuzzleDraftOpenIntent prefers active generation before restoring draft', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork(),
notices: {},
generation: emptyGenerationFacts({
activeSessionId: 'puzzle-session-base',
hasActiveGenerationRunning: true,
}),
}),
).toMatchObject({
type: 'active-generation',
});
});
test('resolvePuzzleDraftOpenIntent does not lock a puzzle draft that already has a cover', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork({
coverImageSrc: '/media/puzzle-cover.png',
}),
notices: {
'puzzle:puzzle-session-base': {
status: 'generating',
seen: false,
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-draft',
});
});
test('resolvePuzzleDraftOpenIntent ignores stale generating notice after puzzle draft is ready', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork({
generationStatus: 'ready',
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
levels: [
{
levelId: 'level-1',
levelName: '金色数字舞台',
pictureDescription: '123456',
pictureReference: null,
candidates: [],
selectedCandidateId: null,
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
coverAssetId: 'asset-1',
generationStatus: 'ready',
},
],
}),
notices: {
'puzzle:puzzle-session-base': {
status: 'generating',
seen: false,
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-draft',
});
});
test('resolvePuzzleDraftOpenIntent restores generating page from notice even before persisted status catches up', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork({
generationStatus: 'idle',
}),
notices: {
'puzzle:puzzle-session-base': {
status: 'generating',
seen: false,
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-generating',
});
});
test('resolveMatch3DDraftOpenIntent opens published work detail unless forced into draft', () => {
const item = buildMatch3DWork({
publicationStatus: 'published',
});
expect(
resolveMatch3DDraftOpenIntent({
item,
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'open-published-detail',
});
expect(
resolveMatch3DDraftOpenIntent({
item,
notices: {},
forceDraft: true,
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-draft',
});
});
test('resolveMatch3DDraftOpenIntent starts ready unread draft before failure fallback', () => {
expect(
resolveMatch3DDraftOpenIntent({
item: buildMatch3DWork(),
notices: {
'match3d:match3d-session-base': {
status: 'ready',
seen: false,
},
},
generation: emptyGenerationFacts({
hasBackgroundGenerationFailure: true,
}),
}),
).toMatchObject({
type: 'ready-unread',
});
});
test('resolveMatch3DDraftOpenIntent restores persisted generating draft', () => {
expect(
resolveMatch3DDraftOpenIntent({
item: buildMatch3DWork({
generationStatus: 'generating',
}),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-generating',
});
});
test('resolveBigFishDraftOpenIntent reopens active generating session before restoring draft', () => {
expect(
resolveBigFishDraftOpenIntent({
item: buildBigFishWork(),
activeSessionId: 'big-fish-session-base',
hasActiveGenerationRunning: true,
}),
).toMatchObject({
type: 'active-generation',
sourceSessionId: 'big-fish-session-base',
});
expect(
resolveBigFishDraftOpenIntent({
item: buildBigFishWork(),
activeSessionId: 'other-session',
hasActiveGenerationRunning: true,
}),
).toMatchObject({
type: 'restore-draft',
sourceSessionId: 'big-fish-session-base',
});
});
test('resolveSquareHoleDraftOpenIntent handles published, missing, active and restore states', () => {
expect(
resolveSquareHoleDraftOpenIntent({
item: buildSquareHoleWork({ publicationStatus: 'published' }),
activeSessionId: null,
hasActiveGenerationRunning: false,
isGenerationReady: false,
}),
).toMatchObject({
type: 'open-published-detail',
});
expect(
resolveSquareHoleDraftOpenIntent({
item: buildSquareHoleWork({ sourceSessionId: null }),
forceDraft: true,
activeSessionId: null,
hasActiveGenerationRunning: false,
isGenerationReady: false,
}),
).toMatchObject({
type: 'missing-session',
});
expect(
resolveSquareHoleDraftOpenIntent({
item: buildSquareHoleWork(),
activeSessionId: 'square-hole-session-base',
hasActiveGenerationRunning: true,
isGenerationReady: false,
}),
).toMatchObject({
type: 'active-generation',
sourceSessionId: 'square-hole-session-base',
});
expect(
resolveSquareHoleDraftOpenIntent({
item: buildSquareHoleWork(),
activeSessionId: 'other-session',
hasActiveGenerationRunning: false,
isGenerationReady: false,
}),
).toMatchObject({
type: 'restore-draft',
shouldClearGenerationState: true,
});
});
test('resolveVisualNovelDraftOpenIntent handles published, active, current result and load detail states', () => {
expect(
resolveVisualNovelDraftOpenIntent({
item: buildVisualNovelWork({ publishStatus: 'published' }),
activeSessionId: null,
hasActiveGenerationRunning: false,
hasActiveSessionDraft: false,
}),
).toMatchObject({
type: 'open-published-detail',
});
expect(
resolveVisualNovelDraftOpenIntent({
item: buildVisualNovelWork(),
forceDraft: true,
activeSessionId: 'visual-novel-profile-base',
hasActiveGenerationRunning: true,
hasActiveSessionDraft: false,
}),
).toMatchObject({
type: 'active-generation',
profileId: 'visual-novel-profile-base',
});
expect(
resolveVisualNovelDraftOpenIntent({
item: buildVisualNovelWork(),
forceDraft: true,
activeSessionId: 'visual-novel-profile-base',
hasActiveGenerationRunning: false,
hasActiveSessionDraft: true,
}),
).toMatchObject({
type: 'current-result',
profileId: 'visual-novel-profile-base',
});
expect(
resolveVisualNovelDraftOpenIntent({
item: buildVisualNovelWork(),
forceDraft: true,
activeSessionId: 'other-profile',
hasActiveGenerationRunning: false,
hasActiveSessionDraft: false,
}),
).toMatchObject({
type: 'load-detail',
profileId: 'visual-novel-profile-base',
});
});
test('resolveJumpHopDraftOpenIntent handles published, failed current generation, generating and detail states', () => {
expect(
resolveJumpHopDraftOpenIntent({
item: buildJumpHopWork({ publicationStatus: 'published' }),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'open-published-detail',
});
expect(
resolveJumpHopDraftOpenIntent({
item: buildJumpHopWork(),
notices: {
'jump-hop:jump-hop-session-base': {
status: 'failed',
seen: false,
},
},
generation: emptyGenerationFacts({
activeSessionId: 'jump-hop-session-base',
hasActiveGenerationFailure: true,
}),
}),
).toMatchObject({
type: 'active-failed-generation',
});
expect(
resolveJumpHopDraftOpenIntent({
item: buildJumpHopWork({ generationStatus: 'generating' }),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-generating',
});
expect(
resolveJumpHopDraftOpenIntent({
item: buildJumpHopWork({ generationStatus: 'generating' }),
notices: {
'jump-hop:jump-hop-session-base': {
status: 'failed',
seen: false,
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'load-detail',
});
expect(
resolveJumpHopDraftOpenIntent({
item: buildJumpHopWork({ sourceSessionId: null }),
notices: {
'jump-hop:jump-hop-work-base': {
status: 'failed',
seen: false,
},
},
generation: emptyGenerationFacts({
activeSessionId: null,
hasActiveGenerationFailure: true,
}),
}),
).toMatchObject({
type: 'load-detail',
});
});
test('resolveWoodenFishDraftOpenIntent uses profile fallback and failure fallback stage', () => {
expect(
resolveWoodenFishDraftOpenIntent({
item: buildWoodenFishWork({
sourceSessionId: null,
generationStatus: 'generating',
}),
notices: {
'wooden-fish:wooden-fish-profile-base': {
status: 'failed',
seen: false,
},
},
generation: emptyGenerationFacts({
activeSessionId: 'wooden-fish-profile-base',
hasActiveGenerationFailure: true,
}),
}),
).toMatchObject({
type: 'active-failed-generation',
});
expect(
resolveWoodenFishDraftOpenIntent({
item: buildWoodenFishWork({ generationStatus: 'generating' }),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-generating',
});
expect(
resolveWoodenFishDraftOpenIntent({
item: buildWoodenFishWork(),
notices: {
'wooden-fish:wooden-fish-session-base': {
status: 'failed',
seen: false,
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'load-detail',
failureFallbackStage: 'wooden-fish-workspace',
});
expect(
resolveWoodenFishDraftOpenIntent({
item: buildWoodenFishWork(),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'load-detail',
failureFallbackStage: 'wooden-fish-generating',
});
});
test('buildPendingPuzzleWorks creates failed puzzle placeholder with stable ids and fallback title', () => {
const pending = buildPendingPuzzleWorks(
{
'puzzle-session-ocean': createPendingDraftShelfState(
'failed',
false,
'2026-06-03T08:00:00.000Z',
),
},
[],
);
expect(pending).toHaveLength(1);
expect(pending[0]).toMatchObject({
workId: 'puzzle-work-ocean',
profileId: 'puzzle-profile-ocean',
sourceSessionId: 'puzzle-session-ocean',
workTitle: '拼图草稿',
summary: '拼图草稿生成失败,可重新打开处理。',
generationStatus: 'failed',
});
});
test('buildPendingPuzzleWorks skips pending item when backend shelf already has the session', () => {
const pending = buildPendingPuzzleWorks(
{
'puzzle-session-ocean': createPendingDraftShelfState(
'generating',
false,
'2026-06-03T08:00:00.000Z',
),
},
[buildPuzzleWork({ sourceSessionId: 'puzzle-session-ocean' })],
);
expect(pending).toEqual([]);
});
test('mergePuzzleWorkSummary only replaces the matching profile', () => {
const current = buildPuzzleWork({
profileId: 'puzzle-profile-1',
workTitle: '旧拼图',
});
const updated = buildPuzzleWork({
profileId: 'puzzle-profile-1',
workTitle: '新拼图',
});
const other = buildPuzzleWork({
profileId: 'puzzle-profile-2',
workTitle: '别的拼图',
});
expect(mergePuzzleWorkSummary(current, updated)).toBe(updated);
expect(mergePuzzleWorkSummary(current, other)).toBe(current);
});
test('mergeBigFishWorkSummary only replaces the matching source session', () => {
const current = buildBigFishWork({
sourceSessionId: 'big-fish-session-1',
title: '旧大鱼',
});
const updated = buildBigFishWork({
sourceSessionId: 'big-fish-session-1',
title: '新大鱼',
});
const other = buildBigFishWork({
sourceSessionId: 'big-fish-session-2',
title: '别的大鱼',
});
expect(mergeBigFishWorkSummary(current, updated)).toBe(updated);
expect(mergeBigFishWorkSummary(current, other)).toBe(current);
});
test('buildCreationWorkShelfRuntimeState lets failure notice override persisted generating puzzle copy', () => {
const [item] = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
buildPuzzleWork({
workId: 'puzzle-work-empty',
profileId: 'puzzle-profile-empty',
sourceSessionId: 'puzzle-session-empty',
workTitle: '',
workDescription: '',
levelName: '',
summary: '正在生成拼图草稿。',
generationStatus: 'generating',
}),
],
});
expect(item).toBeTruthy();
const noticeKeys = getGenerationNoticeShelfKeys(item!);
const notices = Object.fromEntries(
noticeKeys.map((key) => [
key,
{ status: 'failed', seen: false },
]),
) as DraftGenerationNoticeMap;
const state = buildCreationWorkShelfRuntimeState({
item: item!,
notices,
pendingShelfItems: {
puzzle: {
'puzzle-session-empty': createPendingDraftShelfState(
'failed',
false,
'2026-06-03T08:00:00.000Z',
{ summary: '图片生成超时,可重新打开处理。' },
),
},
},
});
expect(state).toMatchObject({
isGenerating: false,
hasGenerationFailure: true,
generationFailureSummary: '拼图草稿生成失败,可重新打开处理。',
hasUnreadUpdate: false,
suppressPersistedGenerating: true,
titleOverride: '拼图草稿',
summaryOverride: '图片生成超时,可重新打开处理。',
});
});
test('collectVisibleDraftNoticeKeys and hasUnreadDraftGenerationUpdates share unread dot rule', () => {
const puzzle = buildPuzzleWork({
workId: 'puzzle-work-ocean',
profileId: 'puzzle-profile-ocean',
sourceSessionId: 'puzzle-session-ocean',
});
const visibleKeys = collectVisibleDraftNoticeKeys({
rpgItems: [],
bigFishItems: [],
jumpHopItems: [],
woodenFishItems: [],
match3dItems: [],
squareHoleItems: [],
puzzleItems: [puzzle],
visualNovelItems: [],
barkBattleItems: [],
babyObjectMatchItems: [],
});
expect(visibleKeys).toContain('puzzle:puzzle-work-ocean');
expect(visibleKeys).toContain('puzzle:puzzle-profile-ocean');
expect(visibleKeys).toContain('puzzle:puzzle-session-ocean');
expect(buildPuzzleResultWorkId('puzzle-session-ocean')).toBe(
'puzzle-work-ocean',
);
expect(buildPuzzleResultProfileId('puzzle-session-ocean')).toBe(
'puzzle-profile-ocean',
);
expect(
hasUnreadDraftGenerationUpdates(
{
'puzzle:puzzle-profile-ocean': {
status: 'ready',
seen: false,
},
},
visibleKeys,
),
).toBe(true);
expect(
hasUnreadDraftGenerationUpdates(
{
'puzzle:puzzle-profile-ocean': {
status: 'ready',
seen: true,
},
},
visibleKeys,
),
).toBe(false);
expect(
countUnreadDraftGenerationUpdates(
{
'puzzle:puzzle-work-ocean': {
status: 'ready',
seen: false,
},
'puzzle:puzzle-profile-ocean': {
status: 'ready',
seen: false,
},
'puzzle:puzzle-session-ocean': {
status: 'ready',
seen: false,
},
},
{
rpgItems: [],
bigFishItems: [],
jumpHopItems: [],
woodenFishItems: [],
match3dItems: [],
squareHoleItems: [],
puzzleItems: [puzzle],
visualNovelItems: [],
barkBattleItems: [],
babyObjectMatchItems: [],
},
),
).toBe(1);
expect(
countUnreadDraftGenerationUpdates(
{
'puzzle:puzzle-profile-ocean': {
status: 'failed',
seen: false,
},
'puzzle:puzzle-session-other': {
status: 'ready',
seen: false,
},
},
{
rpgItems: [],
bigFishItems: [],
jumpHopItems: [],
woodenFishItems: [],
match3dItems: [],
squareHoleItems: [],
puzzleItems: [puzzle],
visualNovelItems: [],
barkBattleItems: [],
babyObjectMatchItems: [],
},
),
).toBe(0);
});
});
function emptyGenerationFacts(
overrides: Partial<Parameters<typeof resolvePuzzleDraftOpenIntent>[0]['generation']> = {},
): Parameters<typeof resolvePuzzleDraftOpenIntent>[0]['generation'] {
return {
activeSessionId: null,
hasActiveGenerationFailure: false,
hasActiveGenerationRunning: false,
hasBackgroundGenerationFailure: false,
hasBackgroundGenerationRunning: false,
...overrides,
};
}
function buildPuzzleWork(
overrides: Partial<PuzzleWorkSummary> = {},
): PuzzleWorkSummary {
return {
workId: 'puzzle-work-base',
profileId: 'puzzle-profile-base',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-base',
authorDisplayName: '测试作者',
workTitle: '潮雾拼图',
workDescription: '潮雾港口拼图。',
levelName: '潮雾拼图',
summary: '潮雾港口拼图。',
themeTags: [],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'draft',
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: false,
levels: [],
...overrides,
};
}
function buildMatch3DWork(
overrides: Partial<Match3DWorkSummary> = {},
): Match3DWorkSummary {
return {
workId: 'match3d-work-base',
profileId: 'match3d-profile-base',
ownerUserId: 'user-1',
sourceSessionId: 'match3d-session-base',
gameName: '潮雾抓大鹅',
themeText: '潮雾港口',
summary: '潮雾港口抓大鹅。',
tags: [],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 0,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'ready',
generatedItemAssets: [],
...overrides,
};
}
function buildBigFishWork(
overrides: Partial<BigFishWorkSummary> = {},
): BigFishWorkSummary {
return {
workId: 'big-fish-work-base',
sourceSessionId: 'big-fish-session-base',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
title: '潮雾大鱼',
subtitle: '潮雾港口',
summary: '潮雾港口大鱼吃小鱼。',
coverImageSrc: null,
status: 'draft',
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: false,
levelCount: 1,
levelMainImageReadyCount: 0,
levelMotionReadyCount: 0,
backgroundReady: false,
...overrides,
};
}
function buildSquareHoleWork(
overrides: Partial<SquareHoleWorkSummary> = {},
): SquareHoleWorkSummary {
return {
workId: 'square-hole-work-base',
profileId: 'square-hole-profile-base',
ownerUserId: 'user-1',
sourceSessionId: 'square-hole-session-base',
gameName: '潮雾方洞',
themeText: '潮雾港口',
twistRule: '避开雾门',
summary: '潮雾港口方洞挑战。',
tags: [],
coverImageSrc: null,
backgroundPrompt: '潮雾港口',
backgroundImageSrc: null,
shapeOptions: [],
holeOptions: [],
shapeCount: 1,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
publishReady: false,
...overrides,
};
}
function buildVisualNovelWork(
overrides: Partial<VisualNovelWorkSummary> = {},
): VisualNovelWorkSummary {
return {
runtimeKind: 'visual-novel',
profileId: 'visual-novel-profile-base',
ownerUserId: 'user-1',
title: '潮雾视觉小说',
description: '潮雾港口视觉小说。',
coverImageSrc: null,
tags: [],
publishStatus: 'draft',
publishReady: false,
playCount: 0,
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
...overrides,
};
}
function buildJumpHopWork(
overrides: Partial<JumpHopWorkSummaryResponse> = {},
): JumpHopWorkSummaryResponse {
return {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-base',
profileId: 'jump-hop-profile-base',
ownerUserId: 'user-1',
sourceSessionId: 'jump-hop-session-base',
themeText: '潮雾港口',
workTitle: '潮雾跳一跳',
workDescription: '潮雾港口跳一跳。',
themeTags: [],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'ready',
...overrides,
};
}
function buildWoodenFishWork(
overrides: Partial<WoodenFishWorkSummaryResponse> = {},
): WoodenFishWorkSummaryResponse {
return {
runtimeKind: 'wooden-fish',
workId: 'wooden-fish-work-base',
profileId: 'wooden-fish-profile-base',
ownerUserId: 'user-1',
sourceSessionId: 'wooden-fish-session-base',
workTitle: '潮雾敲木鱼',
workDescription: '潮雾港口敲木鱼。',
themeTags: ['敲木鱼'],
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'ready',
...overrides,
};
}