Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../data/affinityLevels';
|
||||
import { getScenePresetsByWorld } from '../data/scenePresets';
|
||||
import type {
|
||||
Character,
|
||||
Encounter,
|
||||
SceneHostileNpc,
|
||||
StoryMoment,
|
||||
StoryOption,
|
||||
} from '../types';
|
||||
import { AnimationState, WorldType } from '../types';
|
||||
|
||||
const {
|
||||
connectivityError,
|
||||
@@ -27,19 +36,12 @@ vi.mock('./llmClient', () => ({
|
||||
streamPlainTextCompletion: streamPlainTextCompletionMock,
|
||||
}));
|
||||
|
||||
import type {
|
||||
Character,
|
||||
Encounter,
|
||||
SceneHostileNpc,
|
||||
StoryMoment,
|
||||
StoryOption,
|
||||
} from '../types';
|
||||
import { AnimationState, WorldType } from '../types';
|
||||
import {
|
||||
generateCharacterPanelChatSuggestions,
|
||||
generateCustomWorldProfile,
|
||||
generateCustomWorldSceneImage,
|
||||
generateInitialStory,
|
||||
generateNextStep,
|
||||
streamCharacterPanelChatReply,
|
||||
streamNpcRecruitDialogue,
|
||||
} from './ai';
|
||||
@@ -393,6 +395,123 @@ describe('ai orchestration fallbacks', () => {
|
||||
expect(response.storyText.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('repairs mixed-language story text before returning the story response', async () => {
|
||||
const availableOptions = [
|
||||
createStoryOption({
|
||||
functionId: 'idle_explore_forward',
|
||||
actionText: '继续沿山道探路。',
|
||||
text: '继续沿山道探路。',
|
||||
}),
|
||||
];
|
||||
requestChatMessageContentMock
|
||||
.mockResolvedValueOnce(
|
||||
JSON.stringify({
|
||||
storyText: 'The forest is quiet. 你听见远处的风声。',
|
||||
encounter: null,
|
||||
options: [
|
||||
{
|
||||
functionId: 'idle_explore_forward',
|
||||
actionText: 'Move forward carefully.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
JSON.stringify({
|
||||
storyText: '林间重新安静下来,你听见远处的风声。',
|
||||
encounter: null,
|
||||
options: [
|
||||
{
|
||||
functionId: 'idle_explore_forward',
|
||||
actionText: '继续沿山道探路。',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await generateInitialStory(
|
||||
WorldType.WUXIA,
|
||||
playerCharacter,
|
||||
monsters,
|
||||
context,
|
||||
{ availableOptions },
|
||||
);
|
||||
|
||||
expect(response.storyText).toBe('林间重新安静下来,你听见远处的风声。');
|
||||
expect(response.options[0]?.actionText).toBe('继续沿山道探路。');
|
||||
expect(requestChatMessageContentMock).toHaveBeenCalledTimes(2);
|
||||
expect(requestChatMessageContentMock.mock.calls[1]?.[2]).toEqual(
|
||||
expect.objectContaining({
|
||||
debugLabel: 'story-language-repair',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores generated encounter payloads during post-battle continuations when no new scene encounter is pending', async () => {
|
||||
const availableOptions = [
|
||||
createStoryOption({
|
||||
functionId: 'idle_explore_forward',
|
||||
actionText: '先稳住呼吸,再看看前面的动静。',
|
||||
text: '先稳住呼吸,再看看前面的动静。',
|
||||
}),
|
||||
];
|
||||
const sceneWithNpc = getScenePresetsByWorld(WorldType.WUXIA).find(
|
||||
(scene) => (scene.npcs?.length ?? 0) > 0,
|
||||
);
|
||||
const targetNpcId = sceneWithNpc?.npcs?.[0]?.id;
|
||||
if (!sceneWithNpc || !targetNpcId) {
|
||||
throw new Error('Expected a wuxia scene with at least one npc preset.');
|
||||
}
|
||||
|
||||
requestChatMessageContentMock.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
storyText: '山道总算安静下来,你收住气息,重新判断前路。',
|
||||
encounter: {
|
||||
kind: 'npc',
|
||||
npcId: targetNpcId,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
functionId: 'idle_explore_forward',
|
||||
actionText: '先稳住呼吸,再看看前面的动静。',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await generateNextStep(
|
||||
WorldType.WUXIA,
|
||||
playerCharacter,
|
||||
[],
|
||||
[
|
||||
{
|
||||
text: '挥刀抢攻',
|
||||
options: [],
|
||||
historyRole: 'action',
|
||||
},
|
||||
{
|
||||
text: '山道客已经败下阵来。',
|
||||
options: [],
|
||||
historyRole: 'result',
|
||||
},
|
||||
],
|
||||
'挥刀抢攻',
|
||||
createContext({
|
||||
sceneId: sceneWithNpc.id,
|
||||
sceneName: sceneWithNpc.name,
|
||||
sceneDescription: sceneWithNpc.description,
|
||||
pendingSceneEncounter: false,
|
||||
}),
|
||||
{ availableOptions },
|
||||
);
|
||||
|
||||
expect(response.encounter).toBeUndefined();
|
||||
expect(response.options).toEqual(availableOptions);
|
||||
const userPrompt = requestChatMessageContentMock.mock.calls.at(-1)?.[1];
|
||||
expect(userPrompt).toContain('encounter 必须为 null');
|
||||
expect(userPrompt).toContain('战斗结束后的续写');
|
||||
});
|
||||
|
||||
it('returns offline character chat suggestions when the plain-text client reports connectivity errors', async () => {
|
||||
requestPlainTextCompletionMock.mockRejectedValue(connectivityError);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user