Files
Genarrative/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts

596 lines
16 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import type {
JumpHopSessionSnapshotResponse,
JumpHopWorkspaceCreateRequest,
} from '../../../packages/shared/src/contracts/jumpHop';
import type {
Match3DAgentSessionSnapshot,
Match3DAnchorPackResponse,
} from '../../../packages/shared/src/contracts/match3dAgent';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
PuzzleAnchorPack,
PuzzleDraftLevel,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type {
CreatePuzzleAgentSessionRequest,
PuzzleAgentSessionSnapshot,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type {
WoodenFishSessionSnapshotResponse,
WoodenFishWorkspaceCreateRequest,
} from '../../../packages/shared/src/contracts/woodenFish';
import {
buildJumpHopDraftActionPayload,
buildMatch3DFormPayloadFromSession,
buildMatch3DFormPayloadFromWork,
buildPendingMatch3DDraftMetadata,
buildPendingPuzzleDraftMetadata,
buildPuzzleCompileActionFromFormPayload,
buildPuzzleFormPayloadFromAction,
buildPuzzleFormPayloadFromSession,
buildPuzzleFormPayloadFromWork,
buildPuzzleWorkUpdatePayloadFromDraft,
buildWoodenFishDraftActionPayload,
isEmptyPuzzleFormOnlyDraft,
isPuzzleFormOnlyDraft,
} from './platformMiniGameDraftPayloadModel';
function buildPuzzleAnchorPack(): PuzzleAnchorPack {
const item = {
key: 'theme',
label: '主题',
value: '星桥机关',
status: 'confirmed' as const,
};
return {
themePromise: item,
visualSubject: item,
visualMood: item,
compositionHooks: item,
tagsAndForbidden: item,
};
}
function buildPuzzleLevel(
overrides: Partial<PuzzleDraftLevel> = {},
): PuzzleDraftLevel {
return {
levelId: 'level-1',
levelName: '星桥机关',
pictureDescription: '关卡画面描述',
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'idle',
...overrides,
};
}
function buildPuzzleWork(
overrides: Partial<PuzzleWorkSummary> = {},
): PuzzleWorkSummary {
return {
workId: 'puzzle-work-1',
profileId: 'puzzle-profile-1',
ownerUserId: 'user-1',
authorDisplayName: '玩家',
workTitle: ' 星桥拼图 ',
workDescription: ' 修复星桥机关。 ',
levelName: '星桥机关',
summary: '把碎片拼回原位。',
themeTags: ['星桥'],
coverImageSrc: '/cover.png',
coverAssetId: null,
publicationStatus: 'draft',
updatedAt: '2026-06-01T10:00:00.000Z',
publishedAt: null,
publishReady: false,
levels: [buildPuzzleLevel()],
...overrides,
};
}
function buildPuzzleSession(
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
): PuzzleAgentSessionSnapshot {
const anchorPack = buildPuzzleAnchorPack();
return {
sessionId: 'puzzle-session-1',
seedText: '种子描述',
currentTurn: 1,
progressPercent: 20,
stage: 'collecting_anchors',
anchorPack,
draft: {
workTitle: '会话标题',
workDescription: '会话描述',
levelName: '星桥机关',
summary: '会话摘要',
themeTags: ['星桥'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack,
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'idle',
levels: [buildPuzzleLevel()],
formDraft: {
workTitle: '表单标题',
workDescription: '表单描述',
pictureDescription: '表单画面',
},
},
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
suggestedActions: [],
resultPreview: null,
updatedAt: '2026-06-01T10:00:00.000Z',
...overrides,
};
}
function buildMatch3DAnchorPack(
overrides: Partial<Match3DAnchorPackResponse> = {},
): Match3DAnchorPackResponse {
return {
theme: {
key: 'theme',
label: '主题',
value: '海岛玩具',
status: 'confirmed',
},
clearCount: {
key: 'clearCount',
label: '消除次数',
value: '12',
status: 'confirmed',
},
difficulty: {
key: 'difficulty',
label: '难度',
value: '3',
status: 'confirmed',
},
...overrides,
};
}
function buildMatch3DSession(
overrides: Partial<Match3DAgentSessionSnapshot> = {},
): Match3DAgentSessionSnapshot {
return {
sessionId: 'match3d-session-1',
currentTurn: 1,
progressPercent: 20,
stage: 'collecting',
anchorPack: buildMatch3DAnchorPack(),
config: null,
draft: null,
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
updatedAt: '2026-06-01T11:00:00.000Z',
...overrides,
};
}
function buildMatch3DWork(
overrides: Partial<Match3DWorkSummary> = {},
): Match3DWorkSummary {
return {
workId: 'match3d-work-1',
profileId: 'match3d-profile-1',
ownerUserId: 'user-1',
gameName: '海岛抓大鹅',
themeText: ' 海岛玩具 ',
summary: '收集海岛玩具。',
tags: ['海岛'],
coverImageSrc: '/match3d-cover.png',
referenceImageSrc: '/match3d-reference.png',
clearCount: 12,
difficulty: 3,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-01T11:00:00.000Z',
publishedAt: null,
publishReady: false,
...overrides,
};
}
function buildJumpHopDraft(
overrides: Partial<NonNullable<JumpHopSessionSnapshotResponse['draft']>> = {},
): NonNullable<JumpHopSessionSnapshotResponse['draft']> {
return {
templateId: 'jump-hop',
templateName: '跳一跳',
profileId: 'jump-hop-profile-1',
themeText: '草稿主题',
workTitle: '草稿跳一跳',
workDescription: '从草稿恢复。',
themeTags: ['草稿'],
difficulty: 'standard',
stylePreset: 'paper-toy',
characterPrompt: '草稿角色',
tilePrompt: '草稿平台',
endMoodPrompt: '草稿终点',
characterAsset: null,
tileAtlasAsset: null,
tileAssets: [],
path: null,
coverComposite: null,
generationStatus: 'draft',
...overrides,
};
}
function buildJumpHopPayload(
overrides: Partial<JumpHopWorkspaceCreateRequest> = {},
): JumpHopWorkspaceCreateRequest {
return {
templateId: 'jump-hop',
themeText: '表单主题',
workTitle: '表单跳一跳',
workDescription: '从表单提交。',
themeTags: ['表单'],
difficulty: 'advanced',
stylePreset: 'neon-glass',
characterPrompt: '表单角色',
tilePrompt: '表单平台',
endMoodPrompt: '表单终点',
...overrides,
};
}
function buildWoodenFishDraft(
overrides: Partial<
NonNullable<WoodenFishSessionSnapshotResponse['draft']>
> = {},
): NonNullable<WoodenFishSessionSnapshotResponse['draft']> {
return {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: 'wooden-fish-profile-1',
workTitle: '草稿木鱼',
workDescription: '从草稿恢复。',
themeTags: ['草稿'],
hitObjectPrompt: '草稿敲击物',
hitObjectReferenceImageSrc: '/draft-hit-ref.png',
hitSoundPrompt: null,
floatingWords: ['草稿 +1'],
hitObjectAsset: null,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: null,
coverImageSrc: null,
generationStatus: 'draft',
...overrides,
};
}
function buildWoodenFishPayload(
overrides: Partial<WoodenFishWorkspaceCreateRequest> = {},
): WoodenFishWorkspaceCreateRequest {
return {
templateId: 'wooden-fish',
workTitle: '表单木鱼',
workDescription: '从表单提交。',
themeTags: ['表单'],
hitObjectPrompt: '表单敲击物',
hitObjectReferenceImageSrc: '/form-hit-ref.png',
hitSoundPrompt: null,
hitSoundAsset: null,
floatingWords: ['表单 +1'],
...overrides,
};
}
describe('platformMiniGameDraftPayloadModel', () => {
test('builds puzzle form payload from work with fallback description priority', () => {
expect(
buildPuzzleFormPayloadFromWork(
buildPuzzleWork({
workDescription: ' ',
summary: ' 摘要描述 ',
levelName: ' 关卡标题 ',
}),
),
).toEqual({
seedText: '摘要描述',
workTitle: '星桥拼图',
workDescription: '摘要描述',
pictureDescription: '摘要描述',
referenceImageSrc: null,
referenceImageSrcs: [],
referenceImageAssetObjectId: null,
referenceImageAssetObjectIds: [],
imageModel: null,
aiRedraw: true,
});
});
test('builds puzzle work update payload from result draft', () => {
const draft = buildPuzzleSession().draft!;
expect(buildPuzzleWorkUpdatePayloadFromDraft(draft)).toEqual({
workTitle: '会话标题',
workDescription: '会话描述',
levelName: '星桥机关',
summary: '会话摘要',
themeTags: ['星桥'],
coverImageSrc: null,
coverAssetId: null,
levels: [buildPuzzleLevel()],
});
expect(
buildPuzzleWorkUpdatePayloadFromDraft({
...draft,
levels: undefined,
}).levels,
).toEqual([]);
});
test('builds jump hop draft action payload from payload or draft', () => {
expect(
buildJumpHopDraftActionPayload('compile-draft', {
payload: buildJumpHopPayload(),
draft: buildJumpHopDraft(),
}),
).toEqual({
actionType: 'compile-draft',
workTitle: '表单跳一跳',
workDescription: '从表单提交。',
themeTags: ['表单'],
difficulty: 'advanced',
stylePreset: 'neon-glass',
characterPrompt: '表单角色',
tilePrompt: '表单平台',
endMoodPrompt: '表单终点',
});
expect(
buildJumpHopDraftActionPayload('regenerate-tiles', {
draft: buildJumpHopDraft(),
}),
).toMatchObject({
actionType: 'regenerate-tiles',
workTitle: '草稿跳一跳',
tilePrompt: '草稿平台',
});
});
test('builds wooden fish draft action payload from payload or draft', () => {
expect(
buildWoodenFishDraftActionPayload('compile-draft', {
payload: buildWoodenFishPayload(),
draft: buildWoodenFishDraft(),
}),
).toEqual({
actionType: 'compile-draft',
workTitle: '表单木鱼',
workDescription: '从表单提交。',
themeTags: ['表单'],
hitObjectPrompt: '表单敲击物',
hitObjectReferenceImageSrc: '/form-hit-ref.png',
hitSoundAsset: null,
floatingWords: ['表单 +1'],
});
expect(
buildWoodenFishDraftActionPayload('regenerate-hit-object', {
draft: buildWoodenFishDraft(),
}),
).toMatchObject({
actionType: 'regenerate-hit-object',
workTitle: '草稿木鱼',
hitObjectPrompt: '草稿敲击物',
floatingWords: ['草稿 +1'],
});
});
test('builds puzzle form payload from session form draft and fallbacks', () => {
expect(buildPuzzleFormPayloadFromSession(buildPuzzleSession())).toEqual({
seedText: '表单画面',
workTitle: '表单标题',
workDescription: '表单描述',
pictureDescription: '表单画面',
referenceImageSrc: null,
referenceImageSrcs: [],
referenceImageAssetObjectId: null,
referenceImageAssetObjectIds: [],
imageModel: null,
aiRedraw: true,
});
expect(
buildPuzzleFormPayloadFromSession(
buildPuzzleSession({
draft: {
...buildPuzzleSession().draft!,
formDraft: null,
levels: [buildPuzzleLevel({ pictureDescription: '关卡优先' })],
},
}),
).pictureDescription,
).toBe('关卡优先');
});
test('resolves puzzle form-only draft state for empty and filled forms', () => {
const baseDraft = buildPuzzleSession().draft!;
const emptySession = buildPuzzleSession({
seedText: ' ',
draft: {
...baseDraft,
formDraft: {
workTitle: ' ',
workDescription: ' ',
pictureDescription: ' ',
},
},
});
expect(isPuzzleFormOnlyDraft(emptySession)).toBe(true);
expect(isEmptyPuzzleFormOnlyDraft(emptySession)).toBe(true);
expect(isPuzzleFormOnlyDraft(buildPuzzleSession())).toBe(true);
expect(isEmptyPuzzleFormOnlyDraft(buildPuzzleSession())).toBe(false);
expect(
isPuzzleFormOnlyDraft(buildPuzzleSession({ stage: 'ready_to_publish' })),
).toBe(false);
});
test('builds puzzle compile action and restores form payload from action', () => {
const payload: CreatePuzzleAgentSessionRequest = {
seedText: '种子',
workTitle: ' 标题 ',
workDescription: '',
pictureDescription: ' 画面 ',
referenceImageSrc: '/ref.png',
referenceImageSrcs: ['/ref-a.png'],
referenceImageAssetObjectId: 'asset-ref',
referenceImageAssetObjectIds: ['asset-ref-a'],
imageModel: 'image-model',
aiRedraw: false,
};
const action = buildPuzzleCompileActionFromFormPayload(payload);
expect(action).toEqual({
action: 'compile_puzzle_draft',
promptText: '画面',
workTitle: '标题',
workDescription: '画面',
pictureDescription: '画面',
referenceImageSrc: '/ref.png',
referenceImageSrcs: ['/ref-a.png'],
referenceImageAssetObjectId: 'asset-ref',
referenceImageAssetObjectIds: ['asset-ref-a'],
imageModel: 'image-model',
aiRedraw: false,
candidateCount: 1,
});
expect(buildPuzzleFormPayloadFromAction(action)).toEqual({
seedText: '画面',
workTitle: '标题',
workDescription: '画面',
pictureDescription: '画面',
referenceImageSrc: '/ref.png',
referenceImageSrcs: ['/ref-a.png'],
referenceImageAssetObjectId: 'asset-ref',
referenceImageAssetObjectIds: ['asset-ref-a'],
imageModel: 'image-model',
aiRedraw: false,
});
expect(
buildPuzzleFormPayloadFromAction({
action: 'publish_puzzle_work',
} as PuzzleAgentActionRequest),
).toBeNull();
});
test('builds pending puzzle metadata from non-empty payload fields', () => {
expect(
buildPendingPuzzleDraftMetadata({
workTitle: ' 标题 ',
workDescription: ' ',
pictureDescription: ' 画面 ',
seedText: '种子',
}),
).toEqual({
title: '标题',
summary: '画面',
});
expect(buildPendingPuzzleDraftMetadata(null)).toEqual({});
});
test('builds match3d form payload from session config, draft and anchors', () => {
expect(
buildMatch3DFormPayloadFromSession(
buildMatch3DSession({
config: {
themeText: ' 配置主题 ',
referenceImageSrc: '/config-ref.png',
clearCount: 9,
difficulty: 4,
assetStyleId: 'style-1',
assetStyleLabel: '手办',
assetStylePrompt: '软陶手办',
generateClickSound: true,
},
draft: {
profileId: 'profile-1',
gameName: '草稿标题',
themeText: '草稿主题',
tags: [],
referenceImageSrc: '/draft-ref.png',
clearCount: 6,
difficulty: 2,
},
}),
),
).toEqual({
seedText: '配置主题',
themeText: '配置主题',
referenceImageSrc: '/config-ref.png',
clearCount: 9,
difficulty: 4,
assetStyleId: 'style-1',
assetStyleLabel: '手办',
assetStylePrompt: '软陶手办',
generateClickSound: true,
});
expect(
buildMatch3DFormPayloadFromSession(
buildMatch3DSession({
anchorPack: buildMatch3DAnchorPack({
clearCount: {
key: 'clearCount',
label: '消除次数',
value: 'not-number',
status: 'confirmed',
},
}),
}),
),
).toMatchObject({
seedText: '海岛玩具',
clearCount: undefined,
difficulty: 3,
});
});
test('builds match3d form payload from work and pending metadata', () => {
expect(
buildMatch3DFormPayloadFromWork(
buildMatch3DWork({
themeText: ' ',
}),
),
).toEqual({
seedText: '海岛抓大鹅',
themeText: '海岛抓大鹅',
referenceImageSrc: '/match3d-reference.png',
clearCount: 12,
difficulty: 3,
});
expect(
buildPendingMatch3DDraftMetadata({
themeText: ' ',
seedText: ' 海岛抓大鹅 ',
}),
).toEqual({
title: '海岛抓大鹅',
summary: '海岛抓大鹅',
});
});
});