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 { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js'; import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js'; import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js'; import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js'; import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js'; function createRuntimeRepositoryStub(): RuntimeRepositoryPort { const sessionsByUser = new Map< string, Map >(); const profilesByUser = new Map[]>(); const getSessionBucket = (userId: string) => { const existing = sessionsByUser.get(userId); if (existing) { return existing; } const nextBucket = new Map(); 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, platformTheme: 'light', }; }, 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 listProfileSaveArchives() { return []; }, async resumeProfileSaveArchive() { return null; }, 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 < 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 runtimeRepository = createRuntimeRepositoryStub(); const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository); 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 generate_characters appends story npcs and updates work summary counts', async () => { const runtimeRepository = createRuntimeRepositoryStub(); const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository); 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 listCustomWorldWorkSummaries(userId, { runtimeRepository, customWorldAgentSessions: sessionStore, }); 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 runtimeRepository = createRuntimeRepositoryStub(); const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository); 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); });