This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -15,12 +15,14 @@ vi.mock('../apiClient', async () => {
import { AnimationState } from '../../types';
import {
beginRpgRuntimeStorySession,
buildStoryMomentFromRuntimeOptions,
getRpgRuntimeClientVersion,
getRpgRuntimeSessionId,
getRpgRuntimeStoryState,
isRpgRuntimeServerFunctionId,
isRpgRuntimeTaskFunctionId,
loadRpgRuntimeInventoryView,
resolveRpgRuntimeStoryAction,
resolveRpgRuntimeStoryMoment,
shouldUseRpgRuntimeServerOptions,
@@ -31,6 +33,52 @@ describe('rpgRuntimeStoryClient', () => {
requestJsonMock.mockReset();
});
it('starts runtime sessions through the backend bootstrap endpoint', async () => {
requestJsonMock.mockResolvedValue({
sessionId: 'runtime-server-1',
serverVersion: 1,
snapshot: {
version: 2,
savedAt: '2026-04-28T00:00:00.000Z',
bottomTab: 'adventure',
gameState: {
runtimeSessionId: 'runtime-server-1',
currentScene: 'Story',
playerCharacter: { id: 'role-1', name: '沈砺' },
playerEquipment: { weapon: null, armor: null, relic: null },
},
currentStory: null,
},
});
const result = await beginRpgRuntimeStorySession({
worldType: 'CUSTOM',
customWorldProfile: { id: 'profile-1' } as never,
character: { id: 'role-1', name: '沈砺' } as never,
runtimeMode: 'play',
disablePersistence: false,
});
expect(result.snapshot.gameState.runtimeSessionId).toBe(
'runtime-server-1',
);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/story/sessions',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
worldType: 'CUSTOM',
customWorldProfile: { id: 'profile-1' },
character: { id: 'role-1', name: '沈砺' },
runtimeMode: 'play',
disablePersistence: false,
}),
}),
'初始化运行时开局失败',
expect.any(Object),
);
});
it('builds runtime action requests against the dedicated story endpoint', async () => {
requestJsonMock.mockResolvedValue({
sessionId: 'runtime-main',
@@ -76,7 +124,6 @@ describe('rpgRuntimeStoryClient', () => {
optionText: '继续交谈',
},
},
snapshot: undefined,
}),
}),
'执行运行时动作失败',
@@ -131,7 +178,6 @@ describe('rpgRuntimeStoryClient', () => {
itemId: 'focus-tonic',
},
},
snapshot: undefined,
}),
}),
'执行运行时动作失败',
@@ -139,7 +185,7 @@ describe('rpgRuntimeStoryClient', () => {
);
});
it('submits runtime state resolution with snapshot context to the server', async () => {
it('reads runtime story state by server session id', async () => {
requestJsonMock.mockResolvedValue({
sessionId: 'runtime-main',
serverVersion: 4,
@@ -179,34 +225,103 @@ describe('rpgRuntimeStoryClient', () => {
await getRpgRuntimeStoryState({
sessionId: 'runtime-main',
clientVersion: 7,
snapshot: {
gameState: { currentScene: 'Story' } as never,
bottomTab: 'adventure',
currentStory: {
text: '本地故事',
options: [],
} as never,
},
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/story/state/resolve',
'/api/runtime/story/state/runtime-main',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
sessionId: 'runtime-main',
clientVersion: 7,
snapshot: {
gameState: {
currentScene: 'Story',
method: 'GET',
}),
'读取运行时故事状态失败',
expect.any(Object),
);
});
it('loads backend inventory view from runtime story state', async () => {
requestJsonMock.mockResolvedValue({
sessionId: 'runtime-inventory',
serverVersion: 5,
viewModel: {
player: {
hp: 100,
maxHp: 100,
mana: 20,
maxMana: 20,
},
encounter: null,
companions: [],
inventory: {
playerCurrency: 90,
currencyText: '90 铜钱',
inBattle: false,
backpackItems: [],
equipmentSlots: [],
forgeRecipes: [
{
id: 'synthesis-refined-ingot',
name: '压炼锭材',
kind: 'synthesis',
description: '把零散残片和基础材料压成稳定可用的金属锭材。',
resultLabel: '精炼锭材',
currencyCost: 18,
currencyText: '18 铜钱',
requirements: [
{
id: 'material:any',
label: '任意材料',
quantity: 3,
owned: 3,
},
],
canCraft: true,
action: {
functionId: 'forge_craft',
actionText: '制作精炼锭材',
payload: { recipeId: 'synthesis-refined-ingot' },
enabled: true,
},
},
bottomTab: 'adventure',
currentStory: {
text: '本地故事',
options: [],
},
},
}),
],
},
availableOptions: [],
status: {
inBattle: false,
npcInteractionActive: false,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
},
},
presentation: {
actionText: '',
resultText: '',
storyText: '',
options: [],
},
patches: [],
snapshot: {
version: 2,
savedAt: '2026-04-08T00:00:00.000Z',
bottomTab: 'adventure',
gameState: {
runtimeSessionId: 'runtime-inventory',
runtimeActionVersion: 5,
},
currentStory: null,
},
});
const view = await loadRpgRuntimeInventoryView({
gameState: {
runtimeSessionId: 'runtime-inventory',
runtimeActionVersion: 5,
} as never,
});
expect(view.forgeRecipes[0]?.action.functionId).toBe('forge_craft');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/story/state/runtime-inventory',
expect.objectContaining({
method: 'GET',
}),
'读取运行时故事状态失败',
expect.any(Object),
@@ -336,6 +451,14 @@ describe('rpgRuntimeStoryClient', () => {
player: { hp: 10, maxHp: 10, mana: 5, maxMana: 5 },
encounter: null,
companions: [],
inventory: {
playerCurrency: 0,
currencyText: '0 铜钱',
inBattle: false,
backpackItems: [],
equipmentSlots: [],
forgeRecipes: [],
},
availableOptions: [],
status: {
inBattle: false,