Files
Genarrative/src/services/customWorldAgentGenerationProgress.test.ts

242 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('玩家幻想');
});