This commit is contained in:
2026-04-10 15:37:02 +08:00
parent 161cd32277
commit f19e482c8f
233 changed files with 43987 additions and 5127 deletions

View 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'));
});