import assert from 'node:assert/strict'; import test from 'node:test'; import type { CharacterChatSuggestionsRequest, } from '../../../../packages/shared/src/contracts/story.js'; import { createTestPlayerCharacter } from '../../testFixtures/runtimeCharacter.js'; import { CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT } from './chatPromptBuilders.js'; import { SYSTEM_PROMPT } from './storyPromptBuilders.js'; import { generateCharacterChatSuggestionsFromOrchestrator, } from './chatOrchestrator.js'; import { generateInitialStoryFromOrchestrator } from './storyOrchestrator.js'; type TestStoryContext = Parameters[4]; type TestStoryOption = Awaited< ReturnType >['options'][number]; const TEST_WORLD = 'WUXIA' as Parameters< typeof generateInitialStoryFromOrchestrator >[1]; type TestCharacter = Parameters[2]; function createTestCharacter(overrides: Partial = {}) { return { ...createTestPlayerCharacter(), ...overrides, }; } function createStoryContext(): TestStoryContext { return { playerHp: 120, playerMaxHp: 120, playerMana: 40, playerMaxMana: 40, inBattle: false, playerX: 320, playerFacing: 'right', playerAnimation: 'idle', skillCooldowns: {}, sceneId: 'inn_room', sceneName: '客栈内室', sceneDescription: '昏黄灯火照着刚刚停下脚步的木桌。', pendingSceneEncounter: false, }; } function createAvailableOptions(context: TestStoryContext) { void context; return [ { functionId: 'idle_explore_forward', actionText: '继续向前探索前路', text: '继续向前探索前路', visuals: { playerAnimation: 'idle', playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }, { functionId: 'idle_observe_signs', actionText: '停步观察附近的风吹草动', text: '停步观察附近的风吹草动', visuals: { playerAnimation: 'idle', playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }, ] as TestStoryOption[]; } test('story orchestrator repairs mixed-language narrative on the server side', async () => { const context = createStoryContext(); const availableOptions = createAvailableOptions(context); const capturedPrompts: Array<{ systemPrompt: string; userPrompt: string }> = []; const llmClient = { requestMessageContent: async ({ systemPrompt, userPrompt, }: { systemPrompt: string; userPrompt: string; }) => { capturedPrompts.push({ systemPrompt, userPrompt }); if (capturedPrompts.length === 1) { return JSON.stringify({ storyText: 'The room falls quiet for a moment.', encounter: null, options: availableOptions.map((option) => ({ functionId: option.functionId, actionText: option.actionText, })), }); } return JSON.stringify({ storyText: '房间里短暂安静了一瞬,你能听见灯火轻轻噼啪作响。', encounter: null, options: availableOptions.map((option) => ({ functionId: option.functionId, actionText: option.actionText, })), }); }, } as const; const response = await generateInitialStoryFromOrchestrator( llmClient as never, TEST_WORLD, createTestCharacter(), [], context, { availableOptions, }, ); assert.equal(capturedPrompts.length, 2); assert.equal(capturedPrompts[0]?.systemPrompt, SYSTEM_PROMPT); assert.match(capturedPrompts[0]?.userPrompt ?? '', /客栈内室/u); assert.equal( response.storyText, '房间里短暂安静了一瞬,你能听见灯火轻轻噼啪作响。', ); assert.deepEqual( response.options.map((option) => option.functionId), availableOptions.map((option) => option.functionId), ); }); test('chat orchestrator builds character suggestion prompts on the server side', async () => { const payload = { worldType: TEST_WORLD, playerCharacter: createTestCharacter(), targetCharacter: createTestCharacter({ id: 'test-companion', name: '测试同伴', title: '听风客', }), storyHistory: [], context: createStoryContext(), conversationHistory: [ { speaker: 'player', text: '刚才那阵风是不是也不太对劲?' }, { speaker: 'character', text: '像是有人故意把门帘掀起来了一样。' }, ], conversationSummary: '两人刚在客栈里察觉到不寻常的动静。', targetStatus: { roleLabel: '同行角色', hp: 95, maxHp: 120, mana: 28, maxMana: 40, affinity: 18, }, } satisfies CharacterChatSuggestionsRequest; const capturedPrompts: Array<{ systemPrompt: string; userPrompt: string }> = []; const llmClient = { requestMessageContent: async ({ systemPrompt, userPrompt, }: { systemPrompt: string; userPrompt: string; }) => { capturedPrompts.push({ systemPrompt, userPrompt }); return '先别急,我们再听一轮。\n你刚才看见谁动门帘了吗?\n要不我先去门边探一眼。'; }, } as const; const text = await generateCharacterChatSuggestionsFromOrchestrator( llmClient as never, payload, ); assert.equal(text.split('\n').length, 3); assert.equal( capturedPrompts[0]?.systemPrompt, CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT, ); assert.match(capturedPrompts[0]?.userPrompt ?? '', /客栈/u); assert.match(capturedPrompts[0]?.userPrompt ?? '', /两人刚在客栈里察觉到不寻常的动静/u); assert.match(capturedPrompts[0]?.userPrompt ?? '', new RegExp(payload.targetCharacter.name, 'u')); });