483 lines
15 KiB
TypeScript
483 lines
15 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
|
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
|
import type { JumpHopGalleryCardResponse } 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 { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
|
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
|
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
|
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
|
import {
|
|
buildBarkBattlePublicWorkCode,
|
|
buildBigFishPublicWorkCode,
|
|
buildJumpHopPublicWorkCode,
|
|
buildMatch3DPublicWorkCode,
|
|
buildPuzzlePublicWorkCode,
|
|
buildSquareHolePublicWorkCode,
|
|
buildVisualNovelPublicWorkCode,
|
|
buildWoodenFishPublicWorkCode,
|
|
} from '../../services/publicWorkCode';
|
|
import type { CustomWorldProfile } from '../../types';
|
|
import {
|
|
mapRpgPublicCodeSearchDetailToGalleryCard,
|
|
type PlatformPublicCodeSearchStep,
|
|
resolveBabyObjectMatchPublicCodeSearchMatch,
|
|
resolveBarkBattlePublicCodeSearchMatch,
|
|
resolveBigFishPublicCodeSearchMatch,
|
|
resolveJumpHopPublicCodeSearchMatch,
|
|
resolveMatch3DPublicCodeSearchMatch,
|
|
resolvePlatformPublicCodeSearchPlan,
|
|
resolvePuzzlePublicCodeSearchMatch,
|
|
resolveSquareHolePublicCodeSearchMatch,
|
|
resolveVisualNovelPublicCodeSearchMatch,
|
|
resolveWoodenFishPublicCodeSearchMatch,
|
|
} from './platformPublicCodeSearchModel';
|
|
|
|
function expectSearchSteps(
|
|
keyword: string,
|
|
steps: readonly PlatformPublicCodeSearchStep[],
|
|
) {
|
|
expect(resolvePlatformPublicCodeSearchPlan(keyword)?.steps).toEqual(steps);
|
|
}
|
|
|
|
describe('platformPublicCodeSearchModel', () => {
|
|
test('ignores empty public code search input', () => {
|
|
expect(resolvePlatformPublicCodeSearchPlan(' ')).toBeNull();
|
|
});
|
|
|
|
test('normalizes public code search keyword before planning', () => {
|
|
expect(resolvePlatformPublicCodeSearchPlan(' PZ-00000001 ')).toEqual({
|
|
normalizedKeyword: 'PZ-00000001',
|
|
steps: ['puzzle-work'],
|
|
});
|
|
});
|
|
|
|
test('searches internal user ids directly without work fallback', () => {
|
|
expectSearchSteps('user_00000001', ['user-id']);
|
|
expectSearchSteps('USER-profile-1', ['user-id']);
|
|
});
|
|
|
|
test('routes known public work prefixes to their play-specific lookup', () => {
|
|
const cases: Array<
|
|
[keyword: string, step: PlatformPublicCodeSearchStep]
|
|
> = [
|
|
['PZ-EPUBLIC1', 'puzzle-work'],
|
|
['BF-NPUBLIC1', 'big-fish-work'],
|
|
['JH-EPUBLIC1', 'jump-hop-work'],
|
|
['WF-EPUBLIC1', 'wooden-fish-work'],
|
|
['BO-EPUBLIC1', 'baby-object-match-work'],
|
|
['M3-EPUBLIC1', 'match3d-work'],
|
|
['M3D-LEGACY1', 'match3d-work'],
|
|
['SH-EPUBLIC1', 'square-hole-work'],
|
|
['VN-EPUBLIC1', 'visual-novel-work'],
|
|
['BB-EPUBLIC1', 'bark-battle-work'],
|
|
];
|
|
|
|
for (const [keyword, step] of cases) {
|
|
expectSearchSteps(keyword, [step]);
|
|
}
|
|
});
|
|
|
|
test('searches RPG public works before public user codes for CW and numeric codes', () => {
|
|
expectSearchSteps('CW-00000001', ['rpg-work', 'public-user-code']);
|
|
expectSearchSteps('12345678', ['rpg-work', 'public-user-code']);
|
|
});
|
|
|
|
test('keeps legacy user-code-first fallback for SY and ordinary keywords', () => {
|
|
const legacyFallbackSteps = [
|
|
'public-user-code',
|
|
'rpg-work',
|
|
'bark-battle-work',
|
|
'public-user-code',
|
|
] as const;
|
|
|
|
expectSearchSteps('SY-00000001', legacyFallbackSteps);
|
|
expectSearchSteps('月井守望', legacyFallbackSteps);
|
|
});
|
|
|
|
test('maps RPG detail responses to gallery cards with count defaults', () => {
|
|
expect(
|
|
mapRpgPublicCodeSearchDetailToGalleryCard(
|
|
buildRpgDetailEntry({
|
|
playCount: undefined,
|
|
remixCount: undefined,
|
|
likeCount: undefined,
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
profileId: 'rpg-profile-1',
|
|
visibility: 'published',
|
|
worldName: '潮雾世界',
|
|
playCount: 0,
|
|
remixCount: 0,
|
|
likeCount: 0,
|
|
});
|
|
});
|
|
|
|
test('resolves public code matches for every play-specific gallery type', () => {
|
|
const puzzle = buildPuzzleWork({ profileId: 'puzzle-profile-12345678' });
|
|
const bigFish = buildBigFishWork({
|
|
sourceSessionId: 'big-fish-session-12345678',
|
|
});
|
|
const jumpHop = buildJumpHopCard({ profileId: 'jump-hop-profile-12345678' });
|
|
const woodenFish = buildWoodenFishCard({
|
|
profileId: 'wooden-fish-profile-12345678',
|
|
});
|
|
const babyObjectMatch = buildBabyObjectMatchDraft({
|
|
profileId: 'baby-object-profile-12345678',
|
|
});
|
|
const match3d = buildMatch3DWork({ profileId: 'match3d-profile-12345678' });
|
|
const squareHole = buildSquareHoleWork({
|
|
profileId: 'square-hole-profile-12345678',
|
|
});
|
|
const visualNovel = buildVisualNovelWork({
|
|
profileId: 'visual-novel-profile-12345678',
|
|
});
|
|
const barkBattle = buildBarkBattleWork({
|
|
workId: 'bark-battle-work-12345678',
|
|
});
|
|
|
|
expect(
|
|
resolvePuzzlePublicCodeSearchMatch(
|
|
[puzzle],
|
|
buildPuzzlePublicWorkCode(puzzle.profileId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'puzzle' });
|
|
expect(
|
|
resolveBigFishPublicCodeSearchMatch(
|
|
[bigFish],
|
|
buildBigFishPublicWorkCode(bigFish.sourceSessionId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'big-fish' });
|
|
expect(
|
|
resolveJumpHopPublicCodeSearchMatch(
|
|
[jumpHop],
|
|
buildJumpHopPublicWorkCode(jumpHop.profileId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'jump-hop' });
|
|
expect(
|
|
resolveWoodenFishPublicCodeSearchMatch(
|
|
[woodenFish],
|
|
buildWoodenFishPublicWorkCode(woodenFish.profileId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'wooden-fish' });
|
|
expect(
|
|
resolveBabyObjectMatchPublicCodeSearchMatch(
|
|
[babyObjectMatch],
|
|
`BO-${babyObjectMatch.profileId.slice(-8)}`,
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'edutainment' });
|
|
expect(
|
|
resolveMatch3DPublicCodeSearchMatch(
|
|
[match3d],
|
|
buildMatch3DPublicWorkCode(match3d.profileId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'match3d' });
|
|
expect(
|
|
resolveSquareHolePublicCodeSearchMatch(
|
|
[squareHole],
|
|
buildSquareHolePublicWorkCode(squareHole.profileId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'square-hole' });
|
|
expect(
|
|
resolveVisualNovelPublicCodeSearchMatch(
|
|
[visualNovel],
|
|
buildVisualNovelPublicWorkCode(visualNovel.profileId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'visual-novel' });
|
|
expect(
|
|
resolveBarkBattlePublicCodeSearchMatch(
|
|
[barkBattle],
|
|
buildBarkBattlePublicWorkCode(barkBattle.workId),
|
|
)?.detail,
|
|
).toMatchObject({ sourceType: 'bark-battle' });
|
|
});
|
|
|
|
test('public code search matchers skip entries hidden by visibility policy', () => {
|
|
const hiddenPuzzle = buildPuzzleWork({
|
|
profileId: 'hidden-profile-12345678',
|
|
});
|
|
|
|
expect(
|
|
resolvePuzzlePublicCodeSearchMatch(
|
|
[hiddenPuzzle],
|
|
buildPuzzlePublicWorkCode(hiddenPuzzle.profileId),
|
|
() => false,
|
|
),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
function buildRpgDetailEntry(
|
|
overrides: Partial<CustomWorldLibraryEntry<CustomWorldProfile>> = {},
|
|
): CustomWorldLibraryEntry<CustomWorldProfile> {
|
|
return {
|
|
ownerUserId: 'rpg-owner-1',
|
|
profileId: 'rpg-profile-1',
|
|
publicWorkCode: 'CW-00000001',
|
|
authorPublicUserCode: 'SY-00000001',
|
|
profile: {} as CustomWorldProfile,
|
|
visibility: 'published',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
authorDisplayName: '测试作者',
|
|
worldName: '潮雾世界',
|
|
subtitle: '潮雾港',
|
|
summaryText: '潮雾世界说明。',
|
|
coverImageSrc: null,
|
|
themeMode: 'tide',
|
|
playableNpcCount: 1,
|
|
landmarkCount: 1,
|
|
playCount: 1,
|
|
remixCount: 1,
|
|
likeCount: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildPuzzleWork(
|
|
overrides: Partial<PuzzleWorkSummary> = {},
|
|
): PuzzleWorkSummary {
|
|
return {
|
|
workId: 'puzzle-work-1',
|
|
profileId: 'puzzle-profile-1',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'puzzle-session-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '潮雾拼图',
|
|
workDescription: '潮雾拼图说明。',
|
|
levelName: '潮雾拼图',
|
|
summary: '潮雾拼图说明。',
|
|
themeTags: [],
|
|
coverImageSrc: null,
|
|
coverAssetId: null,
|
|
publicationStatus: 'published',
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
playCount: 0,
|
|
remixCount: 0,
|
|
likeCount: 0,
|
|
publishReady: true,
|
|
levels: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildBigFishWork(
|
|
overrides: Partial<BigFishWorkSummary> = {},
|
|
): BigFishWorkSummary {
|
|
return {
|
|
workId: 'big-fish-work-1',
|
|
sourceSessionId: 'big-fish-session-1',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
title: '潮雾大鱼',
|
|
subtitle: '潮雾港',
|
|
summary: '潮雾大鱼说明。',
|
|
coverImageSrc: null,
|
|
status: 'published',
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
playCount: 0,
|
|
remixCount: 0,
|
|
likeCount: 0,
|
|
publishReady: true,
|
|
levelCount: 1,
|
|
levelMainImageReadyCount: 1,
|
|
levelMotionReadyCount: 1,
|
|
backgroundReady: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildJumpHopCard(
|
|
overrides: Partial<JumpHopGalleryCardResponse> = {},
|
|
): JumpHopGalleryCardResponse {
|
|
const profileId = overrides.profileId ?? 'jump-hop-profile-1';
|
|
return {
|
|
publicWorkCode: buildJumpHopPublicWorkCode(profileId),
|
|
workId: 'jump-hop-work-1',
|
|
profileId,
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '潮雾跳一跳',
|
|
workDescription: '潮雾跳一跳说明。',
|
|
coverImageSrc: null,
|
|
themeTags: [],
|
|
difficulty: 'standard',
|
|
stylePreset: 'minimal-blocks',
|
|
publicationStatus: 'published',
|
|
playCount: 0,
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
generationStatus: 'ready',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildWoodenFishCard(
|
|
overrides: Partial<WoodenFishGalleryCardResponse> = {},
|
|
): WoodenFishGalleryCardResponse {
|
|
const profileId = overrides.profileId ?? 'wooden-fish-profile-1';
|
|
return {
|
|
publicWorkCode: buildWoodenFishPublicWorkCode(profileId),
|
|
workId: 'wooden-fish-work-1',
|
|
profileId,
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '潮雾木鱼',
|
|
workDescription: '潮雾木鱼说明。',
|
|
coverImageSrc: null,
|
|
themeTags: ['敲木鱼'],
|
|
publicationStatus: 'published',
|
|
playCount: 0,
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
generationStatus: 'ready',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildBabyObjectMatchDraft(
|
|
overrides: Partial<BabyObjectMatchDraft> = {},
|
|
): BabyObjectMatchDraft {
|
|
return {
|
|
draftId: 'baby-draft-1',
|
|
profileId: 'baby-object-profile-1',
|
|
templateId: 'baby-object-match',
|
|
templateName: '宝贝识物',
|
|
workTitle: '潮雾识物',
|
|
workDescription: '潮雾识物说明。',
|
|
itemNames: ['苹果', '香蕉'],
|
|
itemAssets: [
|
|
buildBabyObjectMatchItemAsset('item-a', '苹果'),
|
|
buildBabyObjectMatchItemAsset('item-b', '香蕉'),
|
|
],
|
|
visualPackage: null,
|
|
themeTags: ['寓教于乐'],
|
|
publicationStatus: 'published',
|
|
createdAt: '2026-06-04T00:00:00.000Z',
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildBabyObjectMatchItemAsset(itemId: string, itemName: string) {
|
|
return {
|
|
itemId,
|
|
itemName,
|
|
imageSrc: `/media/${itemId}.png`,
|
|
assetObjectId: null,
|
|
generationProvider: 'placeholder' as const,
|
|
prompt: itemName,
|
|
};
|
|
}
|
|
|
|
function buildMatch3DWork(
|
|
overrides: Partial<Match3DWorkSummary> = {},
|
|
): Match3DWorkSummary {
|
|
return {
|
|
workId: 'match3d-work-1',
|
|
profileId: 'match3d-profile-1',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'match3d-session-1',
|
|
gameName: '潮雾抓大鹅',
|
|
themeText: '潮雾港',
|
|
summary: '潮雾抓大鹅说明。',
|
|
tags: [],
|
|
coverImageSrc: null,
|
|
referenceImageSrc: null,
|
|
clearCount: 0,
|
|
difficulty: 1,
|
|
publicationStatus: 'published',
|
|
playCount: 0,
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
publishReady: true,
|
|
generationStatus: 'ready',
|
|
generatedItemAssets: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildSquareHoleWork(
|
|
overrides: Partial<SquareHoleWorkSummary> = {},
|
|
): SquareHoleWorkSummary {
|
|
return {
|
|
workId: 'square-hole-work-1',
|
|
profileId: 'square-hole-profile-1',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'square-hole-session-1',
|
|
gameName: '潮雾方洞',
|
|
themeText: '潮雾港',
|
|
twistRule: '避开雾门',
|
|
summary: '潮雾方洞说明。',
|
|
tags: [],
|
|
coverImageSrc: null,
|
|
backgroundPrompt: '潮雾港',
|
|
backgroundImageSrc: null,
|
|
shapeOptions: [],
|
|
holeOptions: [],
|
|
shapeCount: 1,
|
|
difficulty: 1,
|
|
publicationStatus: 'published',
|
|
playCount: 0,
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
publishReady: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildVisualNovelWork(
|
|
overrides: Partial<VisualNovelWorkSummary> = {},
|
|
): VisualNovelWorkSummary {
|
|
return {
|
|
runtimeKind: 'visual-novel',
|
|
profileId: 'visual-novel-profile-1',
|
|
ownerUserId: 'user-1',
|
|
title: '潮雾视觉小说',
|
|
description: '潮雾视觉小说说明。',
|
|
coverImageSrc: null,
|
|
tags: [],
|
|
publishStatus: 'published',
|
|
publishReady: true,
|
|
playCount: 0,
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildBarkBattleWork(
|
|
overrides: Partial<BarkBattleWorkSummary> = {},
|
|
): BarkBattleWorkSummary {
|
|
return {
|
|
workId: 'bark-battle-work-1',
|
|
draftId: 'bark-battle-draft-1',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
title: '潮雾声浪',
|
|
summary: '潮雾声浪说明。',
|
|
themeDescription: '潮雾港',
|
|
playerImageDescription: '小狗',
|
|
opponentImageDescription: '对手',
|
|
onomatopoeia: ['汪'],
|
|
playerCharacterImageSrc: null,
|
|
opponentCharacterImageSrc: null,
|
|
uiBackgroundImageSrc: null,
|
|
difficultyPreset: 'normal',
|
|
status: 'published',
|
|
generationStatus: 'ready',
|
|
publishReady: true,
|
|
playCount: 0,
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|