import assert from 'node:assert/strict'; import test from 'node:test'; import type { CustomWorldSessionRecord } from '../../../packages/shared/src/contracts/runtime.js'; import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js'; import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js'; import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js'; import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js'; import type { CustomWorldSessionCapability, CustomWorldWorkSummaryCapability, } from './runtimeCapabilities.js'; function createRuntimeRepositoryStub(): CustomWorldSessionCapability & CustomWorldWorkSummaryCapability { 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 listCustomWorldProfiles(userId) { return [...(profilesByUser.get(userId) ?? [])]; }, 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, null, { singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(), }); 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)?.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, null, { singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(), }); const userId = 'user-phase3-not-ready'; const createdSession = await orchestrator.createSession(userId, { seedText: '一个被潮雾切开的列岛世界。', }); await assert.rejects( () => orchestrator.executeAction(userId, createdSession.sessionId, { action: 'draft_foundation', }), /progressPercent >= 100|draft_foundation/u, ); }); test('phase3 work summaries prefer compiled foundation draft fields', async () => { const runtimeRepository = createRuntimeRepositoryStub(); const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository); const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, { singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(), }); 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); });