Files
Genarrative/packages/shared/src/contracts/rpgContracts.test.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

147 lines
6.2 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from './customWorldAgentSession';
import type { CustomWorldResultPreviewEnvelope } from './customWorldResultPreview';
import type { CustomWorldWorkSummary } from './customWorldWorkSummary';
import {
createRpgAgentFoundationDraftProfileFixture,
createRpgAgentSupportedActionsFixture,
createRpgAgentSessionFixture,
createRpgCreationAnchorContentFixture,
createRpgCreationPreviewEnvelopeFixture,
createRpgCreationPublishedProfileFixture,
createRpgCreationWorksResponseFixture,
createRpgWorldLibraryEntryFixture,
} from './rpgCreationFixtures';
describe('RPG 创作共享契约 fixture', () => {
test('旧命名兼容分文件可以直接承接新 fixture 的类型消费', () => {
const legacySession: CustomWorldAgentSessionSnapshot =
createRpgAgentSessionFixture();
const legacyPreview: CustomWorldResultPreviewEnvelope =
createRpgCreationPreviewEnvelopeFixture();
const legacyWork: CustomWorldWorkSummary =
createRpgCreationWorksResponseFixture().items[0]!;
expect(legacySession.stage).toBe('ready_to_publish');
expect(legacySession.resultPreview?.source).toBe(legacyPreview.source);
expect(legacyWork.status).toBe('draft');
});
test('anchor fixture 与 foundation draft fixture 保持最小创作真相源对应关系', () => {
const anchors = createRpgCreationAnchorContentFixture();
const draftProfile = createRpgAgentFoundationDraftProfileFixture();
expect(anchors.worldPromise).toContain('旧航路群岛');
expect(draftProfile.worldHook).toContain('旧航路群岛');
expect(draftProfile.playableNpcs).toHaveLength(1);
expect(draftProfile.storyNpcs).toHaveLength(1);
expect(draftProfile.sceneChapters[0]?.acts[0]?.backgroundImageSrc).toContain(
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
);
});
test('session fixture 同时暴露 supportedActions 与 resultPreview', () => {
const session = createRpgAgentSessionFixture();
expect(session.sessionId).toBe('rpg-session-fixture');
expect(session.stage).toBe('ready_to_publish');
expect(session.checkpoints?.[0]?.checkpointId).toBe(
'checkpoint-foundation-v1',
);
expect(session.supportedActions?.map((entry) => entry.action)).toEqual(
expect.arrayContaining(['draft_foundation', 'generate_role_assets', 'publish_world']),
);
expect(session.resultPreview?.source).toBe('session_preview');
expect(session.resultPreview?.blockers).toEqual([]);
});
test('preview fixture 保持预览来源、质量结论与 profile 载体三层边界', () => {
const preview = createRpgCreationPreviewEnvelopeFixture();
expect(preview.source).toBe('session_preview');
expect(preview.preview.previewId).toBe('preview-fixture-1');
expect(preview.preview.sessionId).toBe('rpg-session-fixture');
expect(preview.qualityFindings?.[0]).toMatchObject({
severity: 'info',
code: 'scene_asset_ready',
});
});
test('supported actions fixture 明确区分可执行能力矩阵,而不是让前端自行猜测按钮状态', () => {
const supportedActions = createRpgAgentSupportedActionsFixture();
expect(supportedActions).toEqual([
{ action: 'draft_foundation', enabled: true },
{ action: 'generate_role_assets', enabled: true },
{ action: 'publish_world', enabled: true },
]);
});
test('published profile fixture 能稳定承载作品库与结果页所需的封面、场景幕与角色资产字段', () => {
const profile = createRpgCreationPublishedProfileFixture();
expect(profile.id).toBe('rpg-profile-fixture');
expect(profile.playableNpcs).toHaveLength(1);
expect(profile.landmarks).toHaveLength(1);
expect(profile.sceneChapterBlueprints).toHaveLength(1);
expect(
(profile.sceneChapterBlueprints as Array<{ acts?: Array<{ backgroundImageSrc?: string }> }>)[0]
?.acts?.[0]?.backgroundImageSrc,
).toContain('/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png');
});
test('regression: session preview 与 published profile 需要共同保留角色动作资产和分幕背景字段', () => {
const session = createRpgAgentSessionFixture();
const publishedProfile = createRpgCreationPublishedProfileFixture();
const preview = createRpgCreationPreviewEnvelopeFixture();
expect(
((session.draftProfile as { playableNpcs?: Array<{ animationMap?: { run?: { basePath?: string } } }> })
.playableNpcs?.[0]?.animationMap?.run?.basePath ?? ''),
).toContain('/generated-characters/playable-1/animations/run');
expect(
((preview.preview.playableNpcs as Array<{ generatedAnimationSetId?: string }>)[0]
?.generatedAnimationSetId ?? ''),
).toBe('animation-set-playable-1');
expect(
((publishedProfile.sceneChapterBlueprints as Array<{
acts?: Array<{ backgroundAssetId?: string }>;
}>)[0]?.acts?.[0]?.backgroundAssetId ?? ''),
).toBe('scene-asset-runtime');
});
test('works fixture 与 library fixture 对齐同一 published profile', () => {
const works = createRpgCreationWorksResponseFixture();
const libraryEntry = createRpgWorldLibraryEntryFixture();
const publishedWork = works.items.find((entry) => entry.status === 'published');
expect(publishedWork?.profileId).toBe(libraryEntry.profileId);
expect(publishedWork?.title).toBe(libraryEntry.worldName);
expect(publishedWork?.canEnterWorld).toBe(true);
expect(libraryEntry.profile.id).toBe(libraryEntry.profileId);
});
test('regression: works fixture 需要稳定保留草稿与发布态的作品门槛字段', () => {
const works = createRpgCreationWorksResponseFixture();
const draftWork = works.items.find((entry) => entry.status === 'draft');
const publishedWork = works.items.find((entry) => entry.status === 'published');
expect(draftWork).toMatchObject({
stage: 'ready_to_publish',
stageLabel: '准备发布',
canResume: true,
canEnterWorld: false,
roleVisualReadyCount: 2,
roleAnimationReadyCount: 2,
});
expect(publishedWork).toMatchObject({
stage: 'published',
stageLabel: '已发布',
canResume: false,
canEnterWorld: true,
});
});
});