242 lines
7.4 KiB
TypeScript
242 lines
7.4 KiB
TypeScript
import { expect, test } from 'vitest';
|
||
|
||
import type {
|
||
CustomWorldAgentOperationRecord,
|
||
CustomWorldAgentSessionSnapshot,
|
||
} from '../../packages/shared/src/contracts/customWorldAgent';
|
||
import {
|
||
buildAgentDraftFoundationGenerationProgress,
|
||
buildAgentDraftFoundationSettingText,
|
||
isDraftFoundationOperationRunning,
|
||
} from './customWorldAgentGenerationProgress';
|
||
|
||
const baseOperation: CustomWorldAgentOperationRecord = {
|
||
operationId: 'operation-1',
|
||
type: 'draft_foundation',
|
||
status: 'running',
|
||
phaseLabel: '生成场景角色',
|
||
phaseDetail: '正在生成场景角色第 1 / 1 批,当前已完成 0/4。',
|
||
progress: 38,
|
||
error: null,
|
||
};
|
||
|
||
const baseSession: CustomWorldAgentSessionSnapshot = {
|
||
sessionId: 'session-1',
|
||
currentTurn: 8,
|
||
anchorContent: {
|
||
worldPromise: {
|
||
hook: '海雾、旧灯塔和失控航路交织的边缘群岛',
|
||
differentiator: '每次借路都要向海雾付出新的代价。',
|
||
desiredExperience: '压抑、悬疑、潮湿',
|
||
},
|
||
playerFantasy: {
|
||
playerRole: '玩家刚回到群岛,准备调查父亲沉船的真相。',
|
||
corePursuit: '查清沉船夜和禁航区异动的因果。',
|
||
fearOfLoss: '再失去唯一还敢接近真相的人。',
|
||
},
|
||
themeBoundary: null,
|
||
playerEntryPoint: null,
|
||
coreConflict: null,
|
||
keyRelationships: [],
|
||
hiddenLines: null,
|
||
iconicElements: {
|
||
iconicMotifs: ['会移动的海雾'],
|
||
institutionsOrArtifacts: ['旧灯塔'],
|
||
hardRules: [],
|
||
},
|
||
},
|
||
progressPercent: 100,
|
||
lastAssistantReply: '八锚点已经收束完成,可以进入游戏设定草稿生成。',
|
||
stage: 'foundation_review',
|
||
focusCardId: null,
|
||
creatorIntent: {
|
||
sourceMode: 'card',
|
||
worldHook: '海雾、旧灯塔和失控航路交织的边缘群岛',
|
||
themeKeywords: ['海雾', '灯塔', '旧航路'],
|
||
toneDirectives: ['压抑', '悬疑'],
|
||
playerPremise: '玩家刚回到群岛,准备调查父亲沉船的真相。',
|
||
openingSituation: '首夜就有陌生船只在禁航区点灯。',
|
||
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
|
||
keyFactions: [],
|
||
keyCharacters: [],
|
||
keyLandmarks: [],
|
||
iconicElements: ['会移动的海雾'],
|
||
forbiddenDirectives: [],
|
||
rawSettingText: '',
|
||
},
|
||
creatorIntentReadiness: {
|
||
isReady: true,
|
||
completedKeys: [],
|
||
missingKeys: [],
|
||
},
|
||
anchorPack: null,
|
||
lockState: null,
|
||
draftProfile: null,
|
||
messages: [
|
||
{
|
||
id: 'message-1',
|
||
role: 'user',
|
||
kind: 'chat',
|
||
text: '我想做一个被海雾吞没的旧航路世界。',
|
||
createdAt: '2026-04-14T10:00:00.000Z',
|
||
relatedOperationId: null,
|
||
},
|
||
],
|
||
draftCards: [],
|
||
pendingClarifications: [],
|
||
suggestedActions: [],
|
||
recommendedReplies: [],
|
||
qualityFindings: [],
|
||
assetCoverage: {
|
||
roleAssets: [],
|
||
sceneAssets: [],
|
||
allRoleAssetsReady: false,
|
||
allSceneAssetsReady: false,
|
||
},
|
||
updatedAt: '2026-04-14T10:00:00.000Z',
|
||
};
|
||
|
||
test('maps running draft_foundation operation to refined generation progress steps', () => {
|
||
const progress = buildAgentDraftFoundationGenerationProgress(
|
||
baseOperation,
|
||
1_000,
|
||
5_000,
|
||
);
|
||
|
||
expect(progress).not.toBeNull();
|
||
expect(progress?.phaseId).toBe('story-outline');
|
||
expect(progress?.batchLabel).toBe('生成场景角色');
|
||
expect(progress?.overallProgress).toBe(38);
|
||
expect(progress?.elapsedMs).toBe(4_000);
|
||
expect(progress?.estimatedRemainingMs).toBeGreaterThan(0);
|
||
expect(progress?.steps).toHaveLength(13);
|
||
expect(progress?.steps.map((step) => step.status)).toEqual([
|
||
'completed',
|
||
'completed',
|
||
'completed',
|
||
'active',
|
||
'pending',
|
||
'pending',
|
||
'pending',
|
||
'pending',
|
||
'pending',
|
||
'pending',
|
||
'pending',
|
||
'pending',
|
||
'pending',
|
||
]);
|
||
expect(isDraftFoundationOperationRunning(baseOperation)).toBe(true);
|
||
});
|
||
|
||
test('calculates elapsed time from operation startedAt before local fallback', () => {
|
||
const progress = buildAgentDraftFoundationGenerationProgress(
|
||
{
|
||
...baseOperation,
|
||
startedAt: '1970-01-01T00:00:01.000Z',
|
||
},
|
||
4_000,
|
||
6_000,
|
||
);
|
||
|
||
expect(progress?.elapsedMs).toBe(5_000);
|
||
});
|
||
|
||
test('maps auto asset phases to refined generation progress steps', () => {
|
||
const progress = buildAgentDraftFoundationGenerationProgress(
|
||
{
|
||
...baseOperation,
|
||
phaseLabel: '生成幕背景图',
|
||
phaseDetail: '正在生成幕背景图 3/6:潮汐码头 · 封锁加压。',
|
||
progress: 99,
|
||
},
|
||
1_000,
|
||
5_000,
|
||
);
|
||
|
||
expect(progress?.phaseId).toBe('act-backgrounds');
|
||
expect(progress?.batchLabel).toBe('生成幕背景图');
|
||
expect(progress?.steps.filter((step) => step.status === 'completed')).toHaveLength(
|
||
10,
|
||
);
|
||
expect(progress?.steps[10]?.status).toBe('active');
|
||
});
|
||
|
||
test('marks all refined progress steps complete when draft foundation finishes', () => {
|
||
const progress = buildAgentDraftFoundationGenerationProgress(
|
||
{
|
||
...baseOperation,
|
||
status: 'completed',
|
||
phaseLabel: '世界底稿已生成',
|
||
phaseDetail: '第一版世界底稿和 6 张草稿卡已经整理完成。',
|
||
progress: 100,
|
||
},
|
||
1_000,
|
||
5_000,
|
||
);
|
||
|
||
expect(progress?.phaseId).toBe('workspace');
|
||
expect(progress?.estimatedRemainingMs).toBe(0);
|
||
expect(progress?.steps.every((step) => step.status === 'completed')).toBe(
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('keeps failed draft foundation progress on explicit failure state instead of pretending it is still compiling cards', () => {
|
||
const progress = buildAgentDraftFoundationGenerationProgress(
|
||
{
|
||
...baseOperation,
|
||
status: 'failed',
|
||
phaseLabel: '底稿生成失败',
|
||
phaseDetail: '角色主形象补齐失败,但世界底稿尚未完成写回。',
|
||
progress: 100,
|
||
error: 'dashscope timeout',
|
||
},
|
||
1_000,
|
||
5_000,
|
||
);
|
||
|
||
expect(progress?.phaseId).toBe('failed');
|
||
expect(progress?.phaseLabel).toBe('底稿生成失败');
|
||
expect(progress?.phaseDetail).toContain('角色主形象补齐失败');
|
||
expect(progress?.overallProgress).toBeLessThan(100);
|
||
expect(progress?.estimatedRemainingMs).toBeNull();
|
||
expect(progress?.steps.some((step) => step.label === '编译草稿卡')).toBe(true);
|
||
expect(progress?.steps.some((step) => step.status === 'active')).toBe(false);
|
||
expect(progress?.steps.filter((step) => step.status === 'completed').length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('estimates draft generation wait time from phase duration model instead of linear progress', () => {
|
||
const progress = buildAgentDraftFoundationGenerationProgress(
|
||
{
|
||
...baseOperation,
|
||
phaseLabel: '生成幕背景图',
|
||
phaseDetail: '正在生成幕背景图 1/6:潮汐码头。',
|
||
progress: 98,
|
||
updatedAt: '1970-01-01T00:00:01.000Z',
|
||
},
|
||
1_000,
|
||
6_000,
|
||
);
|
||
|
||
expect(progress?.estimatedRemainingMs).toBeGreaterThan(80_000);
|
||
expect(progress?.estimatedRemainingMs).toBeLessThan(140_000);
|
||
});
|
||
|
||
test('builds readable draft setting text from creator intent first', () => {
|
||
const settingText = buildAgentDraftFoundationSettingText(baseSession);
|
||
|
||
expect(settingText).toContain('世界一句话');
|
||
expect(settingText).toContain('玩家开局');
|
||
expect(settingText).toContain('标志元素');
|
||
});
|
||
|
||
test('falls back to anchor content when creator intent is unavailable', () => {
|
||
const settingText = buildAgentDraftFoundationSettingText({
|
||
...baseSession,
|
||
creatorIntent: null,
|
||
});
|
||
|
||
expect(settingText).toContain('世界承诺');
|
||
expect(settingText).toContain('玩家幻想');
|
||
});
|