596 lines
16 KiB
TypeScript
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: '海岛抓大鹅',
|
|
});
|
|
});
|
|
});
|