Integrate role asset studio into custom world agent flow
This commit is contained in:
259
server-node/src/services/customWorldAgentPhase3.test.ts
Normal file
259
server-node/src/services/customWorldAgentPhase3.test.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import type { CustomWorldSessionRecord } from '../../../packages/shared/src/contracts/runtime.js';
|
||||
import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.js';
|
||||
import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js';
|
||||
import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
|
||||
import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js';
|
||||
|
||||
function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
|
||||
const sessionsByUser = new Map<
|
||||
string,
|
||||
Map<string, CustomWorldSessionRecord>
|
||||
>();
|
||||
const profilesByUser = new Map<string, Record<string, unknown>[]>();
|
||||
|
||||
const getSessionBucket = (userId: string) => {
|
||||
const existing = sessionsByUser.get(userId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const nextBucket = new Map<string, CustomWorldSessionRecord>();
|
||||
sessionsByUser.set(userId, nextBucket);
|
||||
return nextBucket;
|
||||
};
|
||||
|
||||
return {
|
||||
async getSnapshot(_userId) {
|
||||
return null;
|
||||
},
|
||||
async putSnapshot(_userId, _payload) {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
async deleteSnapshot(_userId) {
|
||||
return undefined;
|
||||
},
|
||||
async getSettings() {
|
||||
return {
|
||||
musicVolume: 0.42,
|
||||
};
|
||||
},
|
||||
async putSettings(_userId, settings) {
|
||||
return settings;
|
||||
},
|
||||
async listCustomWorldProfiles(userId) {
|
||||
return [...(profilesByUser.get(userId) ?? [])];
|
||||
},
|
||||
async upsertCustomWorldProfile(userId, profileId, profile) {
|
||||
const current = [...(profilesByUser.get(userId) ?? [])].filter(
|
||||
(item) => String(item.id ?? '') !== profileId,
|
||||
);
|
||||
current.unshift({
|
||||
...profile,
|
||||
id: profileId,
|
||||
});
|
||||
profilesByUser.set(userId, current);
|
||||
return current;
|
||||
},
|
||||
async deleteCustomWorldProfile(userId, profileId) {
|
||||
const current = [...(profilesByUser.get(userId) ?? [])].filter(
|
||||
(item) => String(item.id ?? '') !== profileId,
|
||||
);
|
||||
profilesByUser.set(userId, current);
|
||||
return current;
|
||||
},
|
||||
async listCustomWorldSessions(userId) {
|
||||
return [...getSessionBucket(userId).values()];
|
||||
},
|
||||
async getCustomWorldSession(userId, sessionId) {
|
||||
return getSessionBucket(userId).get(sessionId) ?? null;
|
||||
},
|
||||
async upsertCustomWorldSession(userId, sessionId, session) {
|
||||
getSessionBucket(userId).set(
|
||||
sessionId,
|
||||
JSON.parse(JSON.stringify(session)),
|
||||
);
|
||||
return JSON.parse(JSON.stringify(session));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForOperation(
|
||||
orchestrator: CustomWorldAgentOrchestrator,
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
operationId: string,
|
||||
) {
|
||||
for (let attempt = 0; attempt < 50; 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 createReadySession(
|
||||
orchestrator: CustomWorldAgentOrchestrator,
|
||||
userId: string,
|
||||
) {
|
||||
const createdSession = await orchestrator.createSession(userId, {
|
||||
seedText: '一个被潮雾切开的列岛世界。',
|
||||
});
|
||||
|
||||
const message1 = await orchestrator.submitMessage(userId, createdSession.sessionId, {
|
||||
clientMessageId: 'phase3-ready-1',
|
||||
text: '玩家是被迫返乡的失职守灯人,开局时正站在即将熄灭的旧灯塔上。',
|
||||
focusCardId: null,
|
||||
selectedCardIds: [],
|
||||
});
|
||||
await waitForOperation(
|
||||
orchestrator,
|
||||
userId,
|
||||
createdSession.sessionId,
|
||||
message1.operation.operationId,
|
||||
);
|
||||
|
||||
const message2 = await orchestrator.submitMessage(userId, createdSession.sessionId, {
|
||||
clientMessageId: 'phase3-ready-2',
|
||||
text: '整体主题是海岛悬疑,气质冷峻克制。核心冲突是守灯会与沉船商盟争夺航道解释权。关键人物叫沈砺,是玩家的旧友兼宿敌,其实暗地里在为沉船商盟引路。标志性元素是潮雾钟声、盐火灯塔。',
|
||||
focusCardId: null,
|
||||
selectedCardIds: [],
|
||||
});
|
||||
await waitForOperation(
|
||||
orchestrator,
|
||||
userId,
|
||||
createdSession.sessionId,
|
||||
message2.operation.operationId,
|
||||
);
|
||||
|
||||
const readySession = await orchestrator.getSessionSnapshot(
|
||||
userId,
|
||||
createdSession.sessionId,
|
||||
);
|
||||
|
||||
assert.equal(readySession?.stage, 'foundation_review');
|
||||
assert.equal(readySession?.creatorIntentReadiness.isReady, true);
|
||||
|
||||
return readySession!;
|
||||
}
|
||||
|
||||
test('phase3 ready session can execute draft_foundation and expose card detail', async () => {
|
||||
const runtimeRepository = createRuntimeRepositoryStub();
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore);
|
||||
const userId = 'user-phase3-draft';
|
||||
const readySession = await createReadySession(orchestrator, userId);
|
||||
|
||||
const response = await orchestrator.executeAction(
|
||||
userId,
|
||||
readySession.sessionId,
|
||||
{
|
||||
action: 'draft_foundation',
|
||||
},
|
||||
);
|
||||
const operation = await waitForOperation(
|
||||
orchestrator,
|
||||
userId,
|
||||
readySession.sessionId,
|
||||
response.operation.operationId,
|
||||
);
|
||||
const snapshot = await orchestrator.getSessionSnapshot(userId, readySession.sessionId);
|
||||
|
||||
assert.equal(operation?.status, 'completed');
|
||||
assert.equal(snapshot?.stage, 'object_refining');
|
||||
assert.ok(snapshot?.draftCards.length);
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'world'));
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'faction'));
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'character'));
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'landmark'));
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'thread'));
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'chapter'));
|
||||
assert.equal(
|
||||
typeof (snapshot?.draftProfile as Record<string, unknown>)?.name,
|
||||
'string',
|
||||
);
|
||||
assert.ok(
|
||||
snapshot?.messages.some(
|
||||
(message) =>
|
||||
message.role === 'assistant' &&
|
||||
message.text.includes('第一版世界底稿整理出来了'),
|
||||
),
|
||||
);
|
||||
|
||||
const worldCard = snapshot?.draftCards.find((card) => card.kind === 'world');
|
||||
assert.ok(worldCard);
|
||||
|
||||
const detail = await orchestrator.getCardDetail(
|
||||
userId,
|
||||
readySession.sessionId,
|
||||
worldCard!.id,
|
||||
);
|
||||
|
||||
assert.ok(detail);
|
||||
assert.equal(detail?.kind, 'world');
|
||||
assert.ok(detail?.sections.length);
|
||||
assert.ok(detail?.sections.some((section) => section.label === '世界一句话'));
|
||||
});
|
||||
|
||||
test('phase3 draft_foundation rejects not-ready session', async () => {
|
||||
const runtimeRepository = createRuntimeRepositoryStub();
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore);
|
||||
const userId = 'user-phase3-not-ready';
|
||||
const createdSession = await orchestrator.createSession(userId, {
|
||||
seedText: '一个被潮雾切开的列岛世界。',
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
orchestrator.executeAction(userId, createdSession.sessionId, {
|
||||
action: 'draft_foundation',
|
||||
}),
|
||||
/ready session|foundation_review/u,
|
||||
);
|
||||
});
|
||||
|
||||
test('phase3 work summaries prefer compiled foundation draft fields', async () => {
|
||||
const runtimeRepository = createRuntimeRepositoryStub();
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore);
|
||||
const userId = 'user-phase3-summary';
|
||||
const readySession = await createReadySession(orchestrator, userId);
|
||||
|
||||
const response = await orchestrator.executeAction(
|
||||
userId,
|
||||
readySession.sessionId,
|
||||
{
|
||||
action: 'draft_foundation',
|
||||
},
|
||||
);
|
||||
await waitForOperation(
|
||||
orchestrator,
|
||||
userId,
|
||||
readySession.sessionId,
|
||||
response.operation.operationId,
|
||||
);
|
||||
|
||||
const items = await listCustomWorldWorkSummaries(userId, {
|
||||
runtimeRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const draft = items.find((item) => item.sessionId === readySession.sessionId);
|
||||
|
||||
assert.ok(draft);
|
||||
assert.ok((draft?.playableNpcCount ?? 0) >= 3);
|
||||
assert.ok((draft?.landmarkCount ?? 0) >= 4);
|
||||
assert.match(draft?.summary ?? '', /潮雾|守灯|航道/u);
|
||||
assert.match(draft?.subtitle ?? '', /守灯|冲突|列岛/u);
|
||||
});
|
||||
Reference in New Issue
Block a user