706 lines
25 KiB
TypeScript
706 lines
25 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js';
|
|
import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js';
|
|
import { createInMemoryRpgWorldRepositoryPorts } from './customWorldAgentRepositoryTestHelpers.js';
|
|
import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
|
|
import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js';
|
|
import { RpgWorldWorkSummaryService } from './RpgWorldWorkSummaryService.js';
|
|
|
|
async function waitForOperation(
|
|
orchestrator: CustomWorldAgentOrchestrator,
|
|
userId: string,
|
|
sessionId: string,
|
|
operationId: string,
|
|
) {
|
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
const operation = await orchestrator.getOperation(
|
|
userId,
|
|
sessionId,
|
|
operationId,
|
|
);
|
|
|
|
if (operation?.status === 'completed' || operation?.status === 'failed') {
|
|
return operation;
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
}
|
|
|
|
throw new Error('operation did not finish in time');
|
|
}
|
|
|
|
async function createObjectRefiningSession(
|
|
orchestrator: CustomWorldAgentOrchestrator,
|
|
userId: string,
|
|
) {
|
|
const createdSession = await orchestrator.createSession(userId, {
|
|
seedText: '一个被潮雾切开的列岛世界。',
|
|
});
|
|
|
|
const message1 = await orchestrator.submitMessage(userId, createdSession.sessionId, {
|
|
clientMessageId: 'phase4-ready-1',
|
|
text: '玩家是被迫返乡的失职守灯人,开局时正站在即将熄灭的旧灯塔上。',
|
|
focusCardId: null,
|
|
selectedCardIds: [],
|
|
});
|
|
await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
createdSession.sessionId,
|
|
message1.operation.operationId,
|
|
);
|
|
|
|
const message2 = await orchestrator.submitMessage(userId, createdSession.sessionId, {
|
|
clientMessageId: 'phase4-ready-2',
|
|
text: '整体主题是海岛悬疑,气质冷峻克制。核心冲突是守灯会与沉船商盟争夺航道解释权。关键人物叫沈砺,是玩家的旧友兼宿敌,其实暗地里在为沉船商盟引路。标志性元素是潮雾钟声、盐火灯塔。',
|
|
focusCardId: null,
|
|
selectedCardIds: [],
|
|
});
|
|
await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
createdSession.sessionId,
|
|
message2.operation.operationId,
|
|
);
|
|
|
|
const foundationOperation = await orchestrator.executeAction(
|
|
userId,
|
|
createdSession.sessionId,
|
|
{
|
|
action: 'draft_foundation',
|
|
},
|
|
);
|
|
await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
createdSession.sessionId,
|
|
foundationOperation.operation.operationId,
|
|
);
|
|
|
|
return (await orchestrator.getSessionSnapshot(
|
|
userId,
|
|
createdSession.sessionId,
|
|
))!;
|
|
}
|
|
|
|
test('phase4 update_draft_card writes back draft profile and recompiles summaries', async () => {
|
|
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-edit';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
const characterCard = session.draftCards.find((card) => card.kind === 'character');
|
|
|
|
assert.ok(characterCard);
|
|
|
|
const response = await orchestrator.executeAction(userId, session.sessionId, {
|
|
action: 'update_draft_card',
|
|
cardId: characterCard!.id,
|
|
sections: [
|
|
{
|
|
sectionId: 'publicMask',
|
|
value: '表面上仍是守灯会里最懂旧航道的人。',
|
|
},
|
|
{
|
|
sectionId: 'relationToPlayer',
|
|
value: '和玩家共享一段无法轻易翻篇的旧灯塔往事。',
|
|
},
|
|
{
|
|
sectionId: 'summary',
|
|
value: '他像旧友,也像最早知道航道秘密的人。',
|
|
},
|
|
],
|
|
});
|
|
const operation = await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
session.sessionId,
|
|
response.operation.operationId,
|
|
);
|
|
const snapshot = await orchestrator.getSessionSnapshot(userId, session.sessionId);
|
|
const profile = normalizeFoundationDraftProfile(snapshot?.draftProfile);
|
|
const editedCharacter = [...(profile?.playableNpcs ?? []), ...(profile?.storyNpcs ?? [])].find(
|
|
(entry) => entry.id === characterCard!.id,
|
|
);
|
|
const editedCard = snapshot?.draftCards.find((card) => card.id === characterCard!.id);
|
|
|
|
assert.equal(operation?.status, 'completed');
|
|
assert.equal(
|
|
editedCharacter?.publicMask,
|
|
'表面上仍是守灯会里最懂旧航道的人。',
|
|
);
|
|
assert.equal(
|
|
editedCharacter?.relationToPlayer,
|
|
'和玩家共享一段无法轻易翻篇的旧灯塔往事。',
|
|
);
|
|
assert.equal(editedCard?.summary, '他像旧友,也像最早知道航道秘密的人。');
|
|
assert.ok(
|
|
snapshot?.messages.some(
|
|
(message) =>
|
|
message.kind === 'action_result' && message.text.includes('已更新'),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('phase4 sync_result_profile writes result-page snapshot back into session draft chain', async () => {
|
|
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-sync-result-profile';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
|
|
const response = await orchestrator.executeAction(userId, session.sessionId, {
|
|
action: 'sync_result_profile',
|
|
profile: {
|
|
id: `agent-draft-${session.sessionId}`,
|
|
settingText: '被海雾吞没的旧航路群岛',
|
|
name: '潮雾列岛·结果页精修版',
|
|
subtitle: '旧灯塔与失控航路',
|
|
summary: '结果页已经把世界概述继续往沉船夜暗线收紧。',
|
|
tone: '压抑、潮湿、悬疑',
|
|
playerGoal: '查清沉船夜与假航灯的真正操盘者。',
|
|
templateWorldType: 'WUXIA',
|
|
majorFactions: ['守灯会', '航运公会'],
|
|
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
|
attributeSchema: {
|
|
id: 'schema:test',
|
|
worldId: 'CUSTOM',
|
|
schemaVersion: 1,
|
|
schemaName: '测试',
|
|
generatedFrom: {
|
|
worldType: 'CUSTOM',
|
|
worldName: '潮雾列岛·结果页精修版',
|
|
settingSummary: '测试',
|
|
tone: '测试',
|
|
conflictCore: '测试',
|
|
},
|
|
slots: [],
|
|
},
|
|
playableNpcs: [],
|
|
storyNpcs: [],
|
|
items: [],
|
|
landmarks: [],
|
|
generationMode: 'full',
|
|
generationStatus: 'complete',
|
|
},
|
|
});
|
|
const operation = await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
session.sessionId,
|
|
response.operation.operationId,
|
|
);
|
|
const snapshot = await orchestrator.getSessionSnapshot(userId, session.sessionId);
|
|
const profile = normalizeFoundationDraftProfile(snapshot?.draftProfile);
|
|
const draftRecord = snapshot?.draftProfile as Record<string, unknown> | null;
|
|
const legacyResultProfile = draftRecord?.legacyResultProfile as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
|
|
assert.equal(operation?.status, 'completed');
|
|
assert.equal(profile?.name, '潮雾列岛·结果页精修版');
|
|
assert.equal(
|
|
profile?.summary,
|
|
'结果页已经把世界概述继续往沉船夜暗线收紧。',
|
|
);
|
|
assert.equal(snapshot?.resultPreview?.source, 'session_preview');
|
|
assert.equal(
|
|
snapshot?.resultPreview?.preview.name,
|
|
'潮雾列岛·结果页精修版',
|
|
);
|
|
assert.equal(
|
|
snapshot?.resultPreview?.preview.playerGoal,
|
|
'查清沉船夜与假航灯的真正操盘者。',
|
|
);
|
|
assert.equal(legacyResultProfile?.name, '潮雾列岛·结果页精修版');
|
|
assert.equal(
|
|
legacyResultProfile?.playerGoal,
|
|
'查清沉船夜与假航灯的真正操盘者。',
|
|
);
|
|
assert.ok(
|
|
snapshot?.messages.some(
|
|
(message) =>
|
|
message.kind === 'action_result' &&
|
|
message.text.includes('结果页里的最新世界结构已经同步回当前草稿'),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('phase4 sync_result_profile keeps existing foundation structure while updating summary snapshot', async () => {
|
|
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-sync-result-profile-structure';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
const baselineProfile = normalizeFoundationDraftProfile(session.draftProfile);
|
|
const baselinePlayableName = baselineProfile?.playableNpcs[0]?.name;
|
|
const baselineStoryName = baselineProfile?.storyNpcs[0]?.name;
|
|
const baselineLandmarkName = baselineProfile?.landmarks[0]?.name;
|
|
|
|
assert.ok(baselinePlayableName);
|
|
assert.ok(baselineStoryName);
|
|
assert.ok(baselineLandmarkName);
|
|
|
|
const response = await orchestrator.executeAction(userId, session.sessionId, {
|
|
action: 'sync_result_profile',
|
|
profile: {
|
|
id: `agent-draft-${session.sessionId}`,
|
|
settingText: '被海雾吞没的旧航路群岛',
|
|
name: '潮雾列岛·结果页精修版',
|
|
subtitle: '旧灯塔与失控航路',
|
|
summary: '结果页已经把世界概述继续往沉船夜暗线收紧。',
|
|
tone: '压抑、潮湿、悬疑',
|
|
playerGoal: '查清沉船夜与假航灯的真正操盘者。',
|
|
templateWorldType: 'WUXIA',
|
|
majorFactions: ['守灯会', '航运公会'],
|
|
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
|
attributeSchema: {
|
|
id: 'schema:test',
|
|
worldId: 'CUSTOM',
|
|
schemaVersion: 1,
|
|
schemaName: '测试',
|
|
generatedFrom: {
|
|
worldType: 'CUSTOM',
|
|
worldName: '潮雾列岛·结果页精修版',
|
|
settingSummary: '测试',
|
|
tone: '测试',
|
|
conflictCore: '测试',
|
|
},
|
|
slots: [],
|
|
},
|
|
playableNpcs: [
|
|
{
|
|
id: 'playable-runtime-only',
|
|
name: '结果页临时角色',
|
|
title: '运行时角色',
|
|
role: '测试角色',
|
|
description: '不应该直接覆盖 foundation draft。',
|
|
backstory: '仅用于验证 sync 边界。',
|
|
personality: '谨慎',
|
|
motivation: '验证同步边界',
|
|
combatStyle: '观察',
|
|
initialAffinity: 0,
|
|
relationshipHooks: [],
|
|
tags: [],
|
|
},
|
|
],
|
|
storyNpcs: [
|
|
{
|
|
id: 'story-runtime-only',
|
|
name: '结果页临时场景角色',
|
|
title: '运行时场景角色',
|
|
role: '测试角色',
|
|
description: '不应该直接覆盖 foundation draft。',
|
|
backstory: '仅用于验证 sync 边界。',
|
|
personality: '克制',
|
|
motivation: '验证同步边界',
|
|
combatStyle: '观察',
|
|
initialAffinity: 0,
|
|
relationshipHooks: [],
|
|
tags: [],
|
|
},
|
|
],
|
|
items: [],
|
|
landmarks: [
|
|
{
|
|
id: 'landmark-runtime-only',
|
|
name: '结果页临时地点',
|
|
description: '不应该直接覆盖 foundation draft。',
|
|
dangerLevel: '低',
|
|
sceneNpcIds: [],
|
|
connections: [],
|
|
},
|
|
],
|
|
generationMode: 'full',
|
|
generationStatus: 'complete',
|
|
},
|
|
});
|
|
const operation = await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
session.sessionId,
|
|
response.operation.operationId,
|
|
);
|
|
const snapshot = await orchestrator.getSessionSnapshot(userId, session.sessionId);
|
|
const profile = normalizeFoundationDraftProfile(snapshot?.draftProfile);
|
|
const draftRecord = snapshot?.draftProfile as Record<string, unknown> | null;
|
|
const legacyResultProfile = draftRecord?.legacyResultProfile as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
|
|
assert.equal(operation?.status, 'completed');
|
|
assert.equal(profile?.name, '潮雾列岛·结果页精修版');
|
|
assert.equal(profile?.playableNpcs[0]?.name, baselinePlayableName);
|
|
assert.equal(profile?.storyNpcs[0]?.name, baselineStoryName);
|
|
assert.equal(profile?.landmarks[0]?.name, baselineLandmarkName);
|
|
assert.equal(legacyResultProfile?.name, '潮雾列岛·结果页精修版');
|
|
assert.equal(
|
|
(legacyResultProfile?.playableNpcs as Array<{ name?: string }> | undefined)?.[0]
|
|
?.name,
|
|
'结果页临时角色',
|
|
);
|
|
});
|
|
|
|
test('phase4 sync_result_profile also writes latest role and scene assets back into draft profile', async () => {
|
|
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-sync-result-profile-assets';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
const baselineProfile = normalizeFoundationDraftProfile(session.draftProfile)!;
|
|
const playableRole = baselineProfile.playableNpcs[0]!;
|
|
const storyRole = baselineProfile.storyNpcs[0]!;
|
|
const landmark = baselineProfile.landmarks[0]!;
|
|
|
|
const response = await orchestrator.executeAction(userId, session.sessionId, {
|
|
action: 'sync_result_profile',
|
|
profile: {
|
|
id: `agent-draft-${session.sessionId}`,
|
|
settingText: '被海雾吞没的旧航路群岛',
|
|
name: '潮雾列岛·结果页精修版',
|
|
subtitle: '旧灯塔与失控航路',
|
|
summary: '结果页已经把最新图与动作一起确认。 ',
|
|
tone: '压抑、潮湿、悬疑',
|
|
playerGoal: '查清沉船夜与假航灯的真正操盘者。',
|
|
templateWorldType: 'WUXIA',
|
|
majorFactions: ['守灯会', '航运公会'],
|
|
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
|
attributeSchema: {
|
|
id: 'schema:test',
|
|
worldId: 'CUSTOM',
|
|
schemaVersion: 1,
|
|
schemaName: '测试',
|
|
generatedFrom: {
|
|
worldType: 'CUSTOM',
|
|
worldName: '潮雾列岛·结果页精修版',
|
|
settingSummary: '测试',
|
|
tone: '测试',
|
|
conflictCore: '测试',
|
|
},
|
|
slots: [],
|
|
},
|
|
playableNpcs: [
|
|
{
|
|
id: playableRole.id,
|
|
name: playableRole.name,
|
|
title: '结果页角色',
|
|
role: '关键同行者',
|
|
description: '结果页确认的最新角色资产。',
|
|
backstory: '测试',
|
|
personality: '冷静',
|
|
motivation: '验证资产回写',
|
|
combatStyle: '观察',
|
|
initialAffinity: 12,
|
|
relationshipHooks: [],
|
|
tags: [],
|
|
imageSrc: '/generated/playable/latest-master.png',
|
|
generatedVisualAssetId: 'visual-playable-latest',
|
|
generatedAnimationSetId: 'anim-playable-latest',
|
|
animationMap: {
|
|
idle: {
|
|
spriteSheetPath: '/generated/playable/idle.png',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
storyNpcs: [
|
|
{
|
|
id: storyRole.id,
|
|
name: storyRole.name,
|
|
title: '结果页场景角色',
|
|
role: '场景关键角色',
|
|
description: '结果页确认的最新场景角色资产。',
|
|
backstory: '测试',
|
|
personality: '克制',
|
|
motivation: '验证资产回写',
|
|
combatStyle: '观察',
|
|
initialAffinity: 6,
|
|
relationshipHooks: [],
|
|
tags: [],
|
|
imageSrc: '/generated/story/latest-master.png',
|
|
generatedVisualAssetId: 'visual-story-latest',
|
|
},
|
|
],
|
|
items: [],
|
|
landmarks: [
|
|
{
|
|
id: landmark.id,
|
|
name: landmark.name,
|
|
description: '结果页确认的最新地点图。',
|
|
dangerLevel: '中',
|
|
sceneNpcIds: [],
|
|
connections: [],
|
|
imageSrc: '/generated/landmark/latest-scene.png',
|
|
},
|
|
],
|
|
sceneChapterBlueprints: [
|
|
{
|
|
id: 'scene-chapter-1',
|
|
sceneId: landmark.id,
|
|
title: '灯塔初章',
|
|
summary: '结果页确认最新分幕图。',
|
|
linkedThreadIds: [],
|
|
linkedLandmarkIds: [landmark.id],
|
|
acts: [
|
|
{
|
|
id: `${landmark.id}-act-1`,
|
|
sceneId: landmark.id,
|
|
title: '第一幕',
|
|
summary: '第一幕',
|
|
stageCoverage: ['opening'],
|
|
backgroundImageSrc: '/generated/scene/act-1-latest.png',
|
|
backgroundAssetId: 'scene-asset-latest',
|
|
encounterNpcIds: [],
|
|
primaryNpcId: '',
|
|
linkedThreadIds: [],
|
|
advanceRule: 'after_primary_contact',
|
|
actGoal: '验证分幕图回写',
|
|
transitionHook: '进入下一幕',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
generationMode: 'full',
|
|
generationStatus: 'complete',
|
|
},
|
|
});
|
|
const operation = await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
session.sessionId,
|
|
response.operation.operationId,
|
|
);
|
|
const snapshot = await orchestrator.getSessionSnapshot(userId, session.sessionId);
|
|
const profile = normalizeFoundationDraftProfile(snapshot?.draftProfile)!;
|
|
const syncedPlayable = profile.playableNpcs.find(
|
|
(entry) => entry.id === playableRole.id,
|
|
);
|
|
const syncedStory = profile.storyNpcs.find((entry) => entry.id === storyRole.id);
|
|
const syncedLandmark = profile.landmarks.find((entry) => entry.id === landmark.id);
|
|
const syncedSceneAct = profile.sceneChapters[0]?.acts[0];
|
|
|
|
assert.equal(operation?.status, 'completed');
|
|
assert.equal(syncedPlayable?.imageSrc, '/generated/playable/latest-master.png');
|
|
assert.equal(syncedPlayable?.generatedVisualAssetId, 'visual-playable-latest');
|
|
assert.equal(syncedPlayable?.generatedAnimationSetId, 'anim-playable-latest');
|
|
assert.deepEqual(syncedPlayable?.animationMap, {
|
|
idle: {
|
|
spriteSheetPath: '/generated/playable/idle.png',
|
|
},
|
|
});
|
|
assert.equal(syncedStory?.imageSrc, '/generated/story/latest-master.png');
|
|
assert.equal(syncedStory?.generatedVisualAssetId, 'visual-story-latest');
|
|
assert.equal(syncedLandmark?.imageSrc, '/generated/landmark/latest-scene.png');
|
|
assert.equal(syncedSceneAct?.backgroundImageSrc, '/generated/scene/act-1-latest.png');
|
|
assert.equal(syncedSceneAct?.backgroundAssetId, 'scene-asset-latest');
|
|
});
|
|
|
|
test('phase4 generate_characters appends story npcs and updates work summary counts', async () => {
|
|
const { rpgAgentSessionRepository, rpgWorldProfileRepository } =
|
|
createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-characters';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
const baselineProfile = normalizeFoundationDraftProfile(session.draftProfile)!;
|
|
const baselineCharacterCount = [
|
|
...new Set(
|
|
[...baselineProfile.playableNpcs, ...baselineProfile.storyNpcs].map(
|
|
(entry) => entry.id,
|
|
),
|
|
),
|
|
].length;
|
|
|
|
const response = await orchestrator.executeAction(userId, session.sessionId, {
|
|
action: 'generate_characters',
|
|
count: 2,
|
|
promptText: '补两位更贴近旧航道线的边缘角色。',
|
|
anchorCardIds: [session.draftCards.find((card) => card.kind === 'thread')!.id],
|
|
});
|
|
const operation = await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
session.sessionId,
|
|
response.operation.operationId,
|
|
);
|
|
const snapshot = await orchestrator.getSessionSnapshot(userId, session.sessionId);
|
|
const profile = normalizeFoundationDraftProfile(snapshot?.draftProfile)!;
|
|
const nextCharacterCount = [
|
|
...new Set(
|
|
[...profile.playableNpcs, ...profile.storyNpcs].map((entry) => entry.id),
|
|
),
|
|
].length;
|
|
const workItems = await new RpgWorldWorkSummaryService(
|
|
rpgWorldProfileRepository,
|
|
sessionStore,
|
|
).list(userId);
|
|
const draftItem = workItems.find((item) => item.sessionId === session.sessionId);
|
|
|
|
assert.equal(operation?.status, 'completed');
|
|
assert.ok(profile.storyNpcs.length >= 2);
|
|
assert.ok(nextCharacterCount >= baselineCharacterCount + 2);
|
|
assert.ok(snapshot?.draftCards.filter((card) => card.kind === 'character').length);
|
|
assert.ok(snapshot?.focusCardId);
|
|
assert.ok(
|
|
snapshot?.messages.some(
|
|
(message) =>
|
|
message.kind === 'action_result' && message.text.includes('新角色'),
|
|
),
|
|
);
|
|
assert.ok((draftItem?.playableNpcCount ?? 0) >= baselineCharacterCount + 2);
|
|
});
|
|
|
|
test('phase4 generate_landmarks appends new landmark cards and checkpoints', async () => {
|
|
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-landmarks';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
const baselineProfile = normalizeFoundationDraftProfile(session.draftProfile)!;
|
|
const baselineLandmarkCount = baselineProfile.landmarks.length;
|
|
|
|
const response = await orchestrator.executeAction(userId, session.sessionId, {
|
|
action: 'generate_landmarks',
|
|
count: 2,
|
|
promptText: '补两个适合藏旧航道秘密的地点。',
|
|
anchorCardIds: [session.draftCards.find((card) => card.kind === 'character')!.id],
|
|
});
|
|
const operation = await waitForOperation(
|
|
orchestrator,
|
|
userId,
|
|
session.sessionId,
|
|
response.operation.operationId,
|
|
);
|
|
const snapshot = await orchestrator.getSessionSnapshot(userId, session.sessionId);
|
|
const profile = normalizeFoundationDraftProfile(snapshot?.draftProfile)!;
|
|
const latestSessionRecord = await sessionStore.get(userId, session.sessionId);
|
|
|
|
assert.equal(operation?.status, 'completed');
|
|
assert.ok(profile.landmarks.length >= baselineLandmarkCount + 2);
|
|
assert.ok(
|
|
snapshot?.draftCards.filter((card) => card.kind === 'landmark').length,
|
|
);
|
|
assert.ok(
|
|
snapshot?.messages.some(
|
|
(message) =>
|
|
message.kind === 'action_result' && message.text.includes('新地点'),
|
|
),
|
|
);
|
|
assert.ok((latestSessionRecord?.checkpoints.length ?? 0) >= 2);
|
|
});
|
|
|
|
test('phase4 work summaries exclude library draft entries after phase3 downgrade', async () => {
|
|
const { rpgAgentSessionRepository, rpgWorldProfileRepository } =
|
|
createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-work-summary-phase3';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
|
|
await rpgWorldProfileRepository.upsertOwnProfile(
|
|
userId,
|
|
'library-draft-1',
|
|
{
|
|
id: 'library-draft-1',
|
|
name: '旧兼容草稿',
|
|
subtitle: '仍保留在作品库',
|
|
summary: '不应该继续出现在创作中心 works 聚合里。',
|
|
playableNpcs: [],
|
|
landmarks: [],
|
|
},
|
|
'玩家',
|
|
);
|
|
|
|
const workItems = await new RpgWorldWorkSummaryService(
|
|
rpgWorldProfileRepository,
|
|
sessionStore,
|
|
).list(userId);
|
|
|
|
assert.ok(workItems.some((item) => item.sessionId === session.sessionId));
|
|
assert.equal(
|
|
workItems.some((item) => item.profileId === 'library-draft-1'),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('phase4 work summaries hide published agent sessions from draft lane and keep published entry enterable', async () => {
|
|
const { rpgAgentSessionRepository, rpgWorldProfileRepository } =
|
|
createInMemoryRpgWorldRepositoryPorts();
|
|
const sessionStore = new CustomWorldAgentSessionStore(
|
|
rpgAgentSessionRepository,
|
|
);
|
|
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
|
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
|
});
|
|
const userId = 'user-phase4-work-summary-published';
|
|
const session = await createObjectRefiningSession(orchestrator, userId);
|
|
|
|
await sessionStore.replaceDerivedState(userId, session.sessionId, {
|
|
stage: 'published',
|
|
qualityFindings: [],
|
|
});
|
|
await rpgWorldProfileRepository.upsertOwnProfile(
|
|
userId,
|
|
`agent-draft-${session.sessionId}`,
|
|
{
|
|
id: `agent-draft-${session.sessionId}`,
|
|
name: '潮雾列岛',
|
|
subtitle: '旧灯塔与失控航路',
|
|
summary: '已发布版本。',
|
|
playableNpcs: [],
|
|
landmarks: [],
|
|
},
|
|
'玩家',
|
|
);
|
|
await rpgWorldProfileRepository.publishOwnProfile(
|
|
userId,
|
|
`agent-draft-${session.sessionId}`,
|
|
'玩家',
|
|
);
|
|
|
|
const workItems = await new RpgWorldWorkSummaryService(
|
|
rpgWorldProfileRepository,
|
|
sessionStore,
|
|
).list(userId);
|
|
const draftItem = workItems.find((item) => item.sessionId === session.sessionId);
|
|
const publishedItem = workItems.find(
|
|
(item) => item.profileId === `agent-draft-${session.sessionId}`,
|
|
);
|
|
|
|
assert.equal(draftItem, undefined);
|
|
assert.equal(publishedItem?.status, 'published');
|
|
assert.equal(publishedItem?.canEnterWorld, true);
|
|
assert.equal(publishedItem?.publishReady, true);
|
|
assert.equal(publishedItem?.blockerCount, 0);
|
|
});
|