1
This commit is contained in:
193
server-node/src/modules/ai/orchestrator.test.ts
Normal file
193
server-node/src/modules/ai/orchestrator.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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<typeof generateInitialStoryFromOrchestrator>[4];
|
||||
type TestStoryOption = Awaited<
|
||||
ReturnType<typeof generateInitialStoryFromOrchestrator>
|
||||
>['options'][number];
|
||||
const TEST_WORLD = 'WUXIA' as Parameters<
|
||||
typeof generateInitialStoryFromOrchestrator
|
||||
>[1];
|
||||
type TestCharacter = Parameters<typeof generateInitialStoryFromOrchestrator>[2];
|
||||
|
||||
function createTestCharacter(overrides: Partial<TestCharacter> = {}) {
|
||||
return {
|
||||
...createTestPlayerCharacter<TestCharacter>(),
|
||||
...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'));
|
||||
});
|
||||
Reference in New Issue
Block a user