Files
Genarrative/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts
T

717 lines
20 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import type {
JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type {
PuzzleAnchorPack,
PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type {
SquareHoleResultDraft,
SquareHoleSessionSnapshot,
} from '../../../packages/shared/src/contracts/squareHoleAgent';
import type {
VisualNovelResultDraft,
VisualNovelWorkDetail,
} from '../../../packages/shared/src/contracts/visualNovel';
import type {
WoodenFishAudioAsset,
WoodenFishImageAsset,
WoodenFishSessionSnapshotResponse,
WoodenFishWorkProfileResponse,
WoodenFishWorkspaceCreateRequest,
WoodenFishWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/woodenFish';
import {
buildJumpHopPendingSession,
buildPuzzleRuntimeWorkFromSession,
buildSquareHoleProfileFromSession,
buildVisualNovelSessionFromWorkDetail,
buildWoodenFishGeneratingWorkSummary,
buildWoodenFishPendingSession,
buildWoodenFishSessionFromWorkDetail,
} from './platformMiniGameSessionMappingModel';
function buildAnchorPack(): PuzzleAnchorPack {
const item = {
key: 'theme',
label: '主题',
value: '星桥机关',
status: 'confirmed' as const,
};
return {
themePromise: item,
visualSubject: item,
visualMood: item,
compositionHooks: item,
tagsAndForbidden: item,
};
}
function buildPuzzleDraft(
overrides: Partial<PuzzleResultDraft> = {},
): PuzzleResultDraft {
const anchorPack = buildAnchorPack();
return {
workTitle: '星桥拼图',
workDescription: '修复星桥机关。',
levelName: '星桥机关',
summary: '把星桥碎片拼回原位。',
themeTags: ['星桥'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack,
candidates: [],
selectedCandidateId: null,
coverImageSrc: '/puzzle-cover.png',
coverAssetId: 'asset-cover',
generationStatus: 'ready',
levels: [
{
levelId: 'level-1',
levelName: '星桥机关',
pictureDescription: '星桥',
candidates: [],
selectedCandidateId: null,
coverImageSrc: '/puzzle-level-cover.png',
coverAssetId: 'asset-level-cover',
generationStatus: 'ready',
},
],
...overrides,
};
}
function buildPuzzleSession(
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
): PuzzleAgentSessionSnapshot {
const draft = buildPuzzleDraft();
return {
sessionId: 'puzzle-session-12345678',
seedText: '星桥',
currentTurn: 1,
progressPercent: 100,
stage: 'ready_to_publish',
anchorPack: draft.anchorPack,
draft,
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
suggestedActions: [],
resultPreview: {
draft,
blockers: [],
qualityFindings: [],
publishReady: true,
},
updatedAt: '2026-06-01T10:00:00.000Z',
...overrides,
};
}
function buildJumpHopSummary(
overrides: Partial<JumpHopWorkSummaryResponse> = {},
): JumpHopWorkSummaryResponse {
return {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-1',
profileId: 'jump-hop-profile-1',
ownerUserId: 'user-1',
sourceSessionId: ' jump-hop-session-1 ',
themeText: '云阶机关',
workTitle: '云阶跳跃',
workDescription: '越过云阶。',
themeTags: ['云阶'],
difficulty: 'standard',
stylePreset: 'paper-toy',
coverImageSrc: '/jump-hop-cover.png',
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-01T11:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'generating',
...overrides,
};
}
function buildSquareHoleDraft(
overrides: Partial<SquareHoleResultDraft> = {},
): SquareHoleResultDraft {
return {
profileId: 'square-hole-profile-1',
gameName: '星桥方洞',
themeText: '星桥机关',
twistRule: '只允许相同颜色形状入洞',
summary: '把星桥机关里的形状送入正确孔洞。',
tags: ['星桥', '机关'],
coverImageSrc: '/square-hole-cover.png',
backgroundPrompt: '星桥机关背景',
backgroundImageSrc: '/square-hole-background.png',
shapeOptions: [
{
optionId: 'shape-1',
shapeKind: 'star',
label: '星形',
targetHoleId: 'hole-1',
imagePrompt: '星形积木',
imageSrc: '/shape-star.png',
},
],
holeOptions: [
{
holeId: 'hole-1',
holeKind: 'star-hole',
label: '星形洞',
imagePrompt: '星形洞口',
imageSrc: '/hole-star.png',
},
],
shapeCount: 6,
difficulty: 3,
publishReady: true,
blockers: [],
...overrides,
};
}
function buildSquareHoleSession(
overrides: Partial<SquareHoleSessionSnapshot> = {},
): SquareHoleSessionSnapshot {
return {
sessionId: 'square-hole-session-1',
currentTurn: 2,
progressPercent: 100,
stage: 'draft_ready',
anchorPack: {
theme: {
key: 'theme',
label: '主题',
value: '星桥机关',
status: 'confirmed',
},
twistRule: {
key: 'twistRule',
label: '扭转规则',
value: '只允许相同颜色形状入洞',
status: 'confirmed',
},
shapeCount: {
key: 'shapeCount',
label: '形状数量',
value: '6',
status: 'confirmed',
},
difficulty: {
key: 'difficulty',
label: '难度',
value: '3',
status: 'confirmed',
},
},
config: {
themeText: '星桥机关',
twistRule: '只允许相同颜色形状入洞',
shapeCount: 6,
difficulty: 3,
shapeOptions: [],
holeOptions: [],
backgroundPrompt: '星桥机关背景',
coverImageSrc: null,
backgroundImageSrc: null,
},
draft: buildSquareHoleDraft(),
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
updatedAt: '2026-06-01T12:30:00.000Z',
...overrides,
};
}
function buildVisualNovelDraft(
overrides: Partial<VisualNovelResultDraft> = {},
): VisualNovelResultDraft {
return {
profileId: 'visual-novel-profile-1',
workTitle: '雪线电台',
workDescription: '旧电台牵出雪夜列车谜案。',
workTags: ['雪夜', '电台'],
coverImageSrc: '/visual-novel-cover.png',
sourceMode: 'idea',
sourceAssetIds: ['asset-source-1'],
world: {
title: '北境终点线',
summary: '边境小城与旧电台。',
background: '十二年前的雪崩留下夜间广播。',
premise: '玩家需要在日出前找出列车停摆的原因。',
literaryStyle: '克制冷光感。',
playerRole: '临时广播员',
defaultTone: '安静紧张',
},
characters: [
{
characterId: 'vn-char-1',
name: '林遥',
gender: '女',
role: 'main',
appearance: '灰色长外套。',
personality: '谨慎敏锐。',
tone: '短句多。',
background: '旧电台夜班实习生。',
relationshipToPlayer: '临时搭档',
imageAssets: [],
defaultExpression: 'calm',
isPlayerVisible: false,
},
],
scenes: [
{
sceneId: 'vn-scene-1',
name: '风雪站台',
description: '站灯忽明忽暗。',
backgroundImageSrc: null,
musicSrc: null,
ambientSoundSrc: null,
availability: 'opening',
phaseIds: ['vn-phase-1'],
},
],
storyPhases: [
{
phaseId: 'vn-phase-1',
title: '重启站台',
goal: '确认列车为何停在废弃站台。',
summary: '玩家抵达风雪站台。',
entryCondition: '开场进入',
exitCondition: '找到车长日志',
sceneIds: ['vn-scene-1'],
characterIds: ['vn-char-1'],
suggestedChoices: ['检查广播柜'],
},
],
opening: {
sceneId: 'vn-scene-1',
narration: '雪落得很慢。',
speakerCharacterId: 'vn-char-1',
firstDialogue: '你听见了吗?',
initialChoices: [
{
choiceId: 'vn-choice-1',
text: '靠近广播柜。',
actionHint: 'inspect_radio',
},
],
},
runtimeConfig: {
textModeEnabled: true,
defaultTextMode: false,
maxHistoryEntries: 80,
maxAssistantStepCountPerTurn: 8,
allowFreeTextAction: true,
allowHistoryRegeneration: true,
attributePanelMode: 'template_config',
saveArchiveEnabled: true,
},
publishReady: true,
validationIssues: [],
updatedAt: '2026-06-01T13:00:00.000Z',
...overrides,
};
}
function buildVisualNovelWorkDetail(
overrides: Partial<VisualNovelWorkDetail> = {},
): VisualNovelWorkDetail {
const draft = buildVisualNovelDraft();
return {
workId: 'visual-novel-work-1',
summary: {
runtimeKind: 'visual-novel',
profileId: 'visual-novel-profile-1',
ownerUserId: 'user-visual-novel-1',
title: draft.workTitle,
description: draft.workDescription,
coverImageSrc: draft.coverImageSrc,
tags: draft.workTags,
publishStatus: 'draft',
publishReady: draft.publishReady,
playCount: 0,
updatedAt: '2026-06-01T13:30:00.000Z',
publishedAt: null,
},
sourceSessionId: ' visual-novel-session-1 ',
authorDisplayName: '视觉小说作者',
sourceAssetIds: draft.sourceAssetIds,
draft,
createdAt: '2026-06-01T12:50:00.000Z',
...overrides,
};
}
const woodenFishImageAsset: WoodenFishImageAsset = {
assetId: 'asset-hit',
imageSrc: '/hit.png',
imageObjectKey: 'hit.png',
assetObjectId: 'asset-object-hit',
generationProvider: 'test',
prompt: '木鱼',
width: 512,
height: 512,
};
const woodenFishAudioAsset: WoodenFishAudioAsset = {
assetId: 'asset-sound',
audioSrc: '/hit.mp3',
audioObjectKey: 'hit.mp3',
assetObjectId: 'asset-object-sound',
source: 'test',
};
function buildWoodenFishSummary(
overrides: Partial<WoodenFishWorkSummaryResponse> = {},
): WoodenFishWorkSummaryResponse {
return {
runtimeKind: 'wooden-fish',
workId: 'wooden-fish-work-1',
profileId: 'wooden-fish-profile-1',
ownerUserId: 'user-1',
sourceSessionId: ' wooden-fish-session-1 ',
workTitle: '星灯木鱼',
workDescription: '敲亮星灯。',
themeTags: ['星灯'],
coverImageSrc: '/wooden-fish-cover.png',
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-01T12:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'generating',
...overrides,
};
}
function buildWoodenFishWorkProfile(
overrides: Partial<WoodenFishWorkProfileResponse> = {},
): WoodenFishWorkProfileResponse {
const summary = buildWoodenFishSummary();
const draft = {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: summary.profileId,
workTitle: summary.workTitle,
workDescription: summary.workDescription,
themeTags: summary.themeTags,
hitObjectPrompt: '星灯',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['功德 +1'],
hitObjectAsset: woodenFishImageAsset,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: woodenFishAudioAsset,
coverImageSrc: summary.coverImageSrc,
generationStatus: summary.generationStatus,
};
return {
summary,
draft,
hitObjectAsset: woodenFishImageAsset,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: woodenFishAudioAsset,
floatingWords: ['功德 +1'],
...overrides,
};
}
function buildWoodenFishSession(
overrides: Partial<WoodenFishSessionSnapshotResponse> = {},
): WoodenFishSessionSnapshotResponse {
const summary = buildWoodenFishSummary();
return {
sessionId: 'wooden-fish-session-1',
ownerUserId: 'user-1',
status: 'generating',
draft: buildWoodenFishWorkProfile({ summary }).draft,
createdAt: '2026-06-01T11:59:00.000Z',
updatedAt: '2026-06-01T12:00:00.000Z',
...overrides,
};
}
function buildWoodenFishCreatePayload(
overrides: Partial<WoodenFishWorkspaceCreateRequest> = {},
): WoodenFishWorkspaceCreateRequest {
return {
templateId: 'wooden-fish',
workTitle: '表单星灯木鱼',
workDescription: '表单里敲亮星灯。',
themeTags: ['表单星灯'],
hitObjectPrompt: '星灯',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['功德 +1'],
...overrides,
};
}
describe('platformMiniGameSessionMappingModel', () => {
test('builds a draft puzzle runtime work from a session', () => {
expect(
buildPuzzleRuntimeWorkFromSession(buildPuzzleSession(), {
userId: 'user-1',
displayName: '玩家一号',
}),
).toMatchObject({
workId: 'puzzle-work-12345678',
profileId: 'puzzle-profile-12345678',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-12345678',
authorDisplayName: '玩家一号',
workTitle: '星桥拼图',
coverImageSrc: '/puzzle-cover.png',
publicationStatus: 'draft',
publishedAt: null,
publishReady: true,
});
});
test('prefers published puzzle profile id when present', () => {
expect(
buildPuzzleRuntimeWorkFromSession(
buildPuzzleSession({
publishedProfileId: 'published-puzzle-profile',
}),
{},
),
).toMatchObject({
profileId: 'published-puzzle-profile',
workId: 'puzzle-work-12345678',
ownerUserId: 'current-user',
authorDisplayName: '玩家',
});
});
test('returns null for puzzle runtime work without draft or cover', () => {
expect(
buildPuzzleRuntimeWorkFromSession(
buildPuzzleSession({
draft: null,
}),
{},
),
).toBeNull();
expect(
buildPuzzleRuntimeWorkFromSession(
buildPuzzleSession({
draft: buildPuzzleDraft({ coverImageSrc: ' ' }),
}),
{},
),
).toBeNull();
});
test('builds jump hop pending session from work summary', () => {
expect(buildJumpHopPendingSession(buildJumpHopSummary())).toEqual({
sessionId: 'jump-hop-session-1',
ownerUserId: 'user-1',
status: 'generating',
draft: {
templateId: 'jump-hop',
templateName: '跳一跳',
profileId: 'jump-hop-profile-1',
themeText: '云阶机关',
workTitle: '云阶跳跃',
workDescription: '越过云阶。',
themeTags: ['云阶'],
difficulty: 'standard',
stylePreset: 'paper-toy',
characterPrompt: '',
tilePrompt: '',
endMoodPrompt: null,
characterAsset: null,
tileAtlasAsset: null,
tileAssets: [],
path: null,
coverComposite: '/jump-hop-cover.png',
generationStatus: 'generating',
},
createdAt: '2026-06-01T11:00:00.000Z',
updatedAt: '2026-06-01T11:00:00.000Z',
});
});
test('builds square hole draft profile from session', () => {
expect(buildSquareHoleProfileFromSession(buildSquareHoleSession())).toEqual({
workId: 'square-hole-profile-1',
profileId: 'square-hole-profile-1',
ownerUserId: 'current-user',
sourceSessionId: 'square-hole-session-1',
gameName: '星桥方洞',
themeText: '星桥机关',
twistRule: '只允许相同颜色形状入洞',
summary: '把星桥机关里的形状送入正确孔洞。',
tags: ['星桥', '机关'],
coverImageSrc: '/square-hole-cover.png',
backgroundPrompt: '星桥机关背景',
backgroundImageSrc: '/square-hole-background.png',
shapeOptions: buildSquareHoleDraft().shapeOptions,
holeOptions: buildSquareHoleDraft().holeOptions,
shapeCount: 6,
difficulty: 3,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-01T12:30:00.000Z',
publishedAt: null,
publishReady: true,
});
});
test('returns null for square hole profile without session draft or profile id', () => {
expect(buildSquareHoleProfileFromSession(null)).toBeNull();
expect(
buildSquareHoleProfileFromSession(
buildSquareHoleSession({
draft: null,
}),
),
).toBeNull();
expect(
buildSquareHoleProfileFromSession(
buildSquareHoleSession({
draft: buildSquareHoleDraft({ profileId: '' }),
}),
),
).toBeNull();
});
test('builds visual novel recovered session from work detail', () => {
const work = buildVisualNovelWorkDetail();
expect(buildVisualNovelSessionFromWorkDetail(work)).toEqual({
sessionId: 'visual-novel-session-1',
ownerUserId: 'user-visual-novel-1',
sourceMode: 'idea',
status: 'ready',
messages: [],
draft: work.draft,
pendingAction: null,
createdAt: '2026-06-01T12:50:00.000Z',
updatedAt: '2026-06-01T13:30:00.000Z',
});
});
test('falls back visual novel recovered session id to work id', () => {
expect(
buildVisualNovelSessionFromWorkDetail(
buildVisualNovelWorkDetail({
sourceSessionId: ' ',
workId: 'visual-novel-work-fallback',
}),
).sessionId,
).toBe('visual-novel-work-fallback');
});
test('builds wooden fish pending session from work summary', () => {
expect(buildWoodenFishPendingSession(buildWoodenFishSummary())).toEqual({
sessionId: 'wooden-fish-session-1',
ownerUserId: 'user-1',
status: 'generating',
draft: {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: 'wooden-fish-profile-1',
workTitle: '星灯木鱼',
workDescription: '敲亮星灯。',
themeTags: ['星灯'],
hitObjectPrompt: '',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['功德 +1'],
hitObjectAsset: null,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: null,
coverImageSrc: '/wooden-fish-cover.png',
generationStatus: 'generating',
},
createdAt: '2026-06-01T12:00:00.000Z',
updatedAt: '2026-06-01T12:00:00.000Z',
});
});
test('builds wooden fish generating work summary from session and payload', () => {
expect(
buildWoodenFishGeneratingWorkSummary(
buildWoodenFishSession(),
buildWoodenFishCreatePayload(),
),
).toEqual({
runtimeKind: 'wooden-fish',
workId: 'wooden-fish-session-1',
profileId: 'wooden-fish-session-1',
ownerUserId: 'user-1',
sourceSessionId: 'wooden-fish-session-1',
workTitle: '表单星灯木鱼',
workDescription: '表单里敲亮星灯。',
themeTags: ['表单星灯'],
coverImageSrc: '/wooden-fish-cover.png',
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-01T12:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'generating',
});
expect(
buildWoodenFishGeneratingWorkSummary(
buildWoodenFishSession({
draft: null,
createdAt: '2026-06-01T11:59:00.000Z',
}),
null,
),
).toMatchObject({
workTitle: '敲木鱼',
workDescription: '',
themeTags: ['敲木鱼'],
coverImageSrc: null,
updatedAt: '2026-06-01T12:00:00.000Z',
});
});
test('builds wooden fish recovered session with summary, fallback and profile id priority', () => {
expect(
buildWoodenFishSessionFromWorkDetail(
buildWoodenFishWorkProfile({
summary: buildWoodenFishSummary({
sourceSessionId: null,
}),
}),
buildWoodenFishSummary({
sourceSessionId: ' fallback-session ',
}),
),
).toMatchObject({
sessionId: 'fallback-session',
ownerUserId: 'user-1',
status: 'generating',
});
expect(
buildWoodenFishSessionFromWorkDetail(
buildWoodenFishWorkProfile({
summary: buildWoodenFishSummary({
sourceSessionId: null,
}),
}),
null,
).sessionId,
).toBe('wooden-fish-profile-1');
});
});