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

@@ -23,6 +23,8 @@ vi.mock('../services/rpg-entry', () => ({
}));
vi.mock('../services/rpg-runtime', () => ({
getRpgRuntimeSessionId: (gameState: Pick<GameState, 'runtimeSessionId'>) =>
gameState.runtimeSessionId?.trim() || 'runtime-main',
rpgSnapshotClient: {
getSnapshot: storageMocks.getSaveSnapshot,
putSnapshot: storageMocks.putSaveSnapshot,
@@ -30,7 +32,11 @@ vi.mock('../services/rpg-runtime', () => ({
},
}));
function SettingsHarness({ authenticatedUserId }: { authenticatedUserId: string | null }) {
function SettingsHarness({
authenticatedUserId,
}: {
authenticatedUserId: string | null;
}) {
const settings = useGameSettings(authenticatedUserId);
return (
@@ -50,14 +56,18 @@ function SettingsHarness({ authenticatedUserId }: { authenticatedUserId: string
function PersistenceHarness({
authenticatedUserId,
gameState = {} as GameState,
currentStory = null as StoryMoment | null,
}: {
authenticatedUserId: string | null;
gameState?: GameState;
currentStory?: StoryMoment | null;
}) {
const persistence = useRpgSessionPersistence({
authenticatedUserId,
gameState: {} as GameState,
gameState,
bottomTab: 'adventure' as BottomTab,
currentStory: null as StoryMoment | null,
currentStory,
isLoading: false,
setGameState: () => {},
setBottomTab: () => {},
@@ -67,7 +77,9 @@ function PersistenceHarness({
return (
<div>
<div data-testid="saved-game">{persistence.hasSavedGame ? 'yes' : 'no'}</div>
<div data-testid="saved-game">
{persistence.hasSavedGame ? 'yes' : 'no'}
</div>
<div data-testid="hydrating">
{persistence.isHydratingSnapshot ? 'yes' : 'no'}
</div>
@@ -161,3 +173,64 @@ test('unauthenticated runtime skips remote snapshot hydration', async () => {
expect(screen.getByTestId('saved-game').textContent).toBe('no');
expect(storageMocks.getSaveSnapshot).not.toHaveBeenCalled();
});
test('runtime autosave requests backend checkpoint without uploading local state', async () => {
vi.useFakeTimers();
storageMocks.getSaveSnapshot.mockResolvedValue(null);
storageMocks.putSaveSnapshot.mockResolvedValue({
version: 2,
savedAt: '2026-04-28T10:00:00.000Z',
bottomTab: 'adventure',
currentStory: null,
gameState: {
runtimeSessionId: 'runtime-main',
currentScene: 'Story',
},
});
const gameState = {
runtimeSessionId: 'runtime-main',
runtimePersistenceDisabled: false,
runtimeMode: 'play',
currentScene: 'Story',
worldType: 'CUSTOM',
playerCharacter: { id: 'hero_001' },
runtimeStats: {
playTimeMs: 0,
lastPlayTickAt: null,
hostileNpcsDefeated: 0,
questsAccepted: 0,
itemsUsed: 0,
scenesTraveled: 0,
},
} as unknown as GameState;
const story = { text: '开场', options: [], streaming: false } as StoryMoment;
render(
<PersistenceHarness
authenticatedUserId="user-1"
gameState={gameState}
currentStory={story}
/>,
);
await act(async () => {
await Promise.resolve();
});
expect(storageMocks.getSaveSnapshot).toHaveBeenCalledTimes(1);
await act(async () => {
vi.advanceTimersByTime(400);
await Promise.resolve();
await Promise.resolve();
});
expect(storageMocks.putSaveSnapshot).toHaveBeenCalledWith(
{
sessionId: 'runtime-main',
bottomTab: 'adventure',
},
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
);
});