import { beforeEach, describe, expect, it, vi } from 'vitest'; const apiClientMocks = vi.hoisted(() => ({ requestJson: vi.fn(), })); vi.mock('./apiClient', async () => { const actual = await vi.importActual('./apiClient'); return { ...actual, requestJson: apiClientMocks.requestJson, }; }); import { finishBarkBattleRun, getBarkBattleRuntimeConfig, startBarkBattleRun, } from './bark-battle-runtime/barkBattleRuntimeClient'; import { getBigFishRun, recordBigFishPlay, startBigFishRun, submitBigFishInput, } from './big-fish-runtime/bigFishRuntimeClient'; import { startJumpHopRuntimeRun } from './jump-hop/jumpHopClient'; import { clickMatch3DItem, finishMatch3DTimeUp, getMatch3DRun, restartMatch3DRun, startMatch3DRun, stopMatch3DRun, } from './match3d-runtime/match3dRuntimeClient'; import { advancePuzzleNextLevel, dragPuzzlePieceOrGroup, getPuzzleRun, startPuzzleRun, submitPuzzleLeaderboard, swapPuzzlePieces, updatePuzzleRunPause, usePuzzleRuntimeProp, } from './puzzle-runtime/puzzleRuntimeClient'; import { puzzleClearClient } from './puzzle-clear/puzzleClearClient'; import { dropSquareHoleShape, finishSquareHoleTimeUp, getSquareHoleRun, restartSquareHoleRun, startSquareHoleRun, stopSquareHoleRun, } from './square-hole-runtime/squareHoleRuntimeClient'; import { checkpointWoodenFishRun, finishWoodenFishRun, startWoodenFishRuntimeRun, } from './wooden-fish/woodenFishClient'; import { getVisualNovelHistory, getVisualNovelRun, regenerateVisualNovelRun, startVisualNovelRun, } from './visual-novel-runtime/visualNovelRuntimeClient'; describe('recommended runtime guest launch clients', () => { beforeEach(() => { vi.clearAllMocks(); apiClientMocks.requestJson.mockResolvedValue({ run: {} }); }); function expectRuntimeGuestRequest(expectedUrl: string, expectedMethod: string) { const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0]; expect(url).toBe(expectedUrl); expect(init).toEqual( expect.objectContaining({ method: expectedMethod, headers: expect.objectContaining({ Authorization: 'Bearer runtime-guest-token', }), }), ); expect(options).toEqual( expect.objectContaining({ skipAuth: true, skipRefresh: true, }), ); } it.each([ { name: 'jump-hop', start: () => startJumpHopRuntimeRun('jump-hop-profile-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/jump-hop/runs', }, { name: 'visual-novel', start: () => startVisualNovelRun( 'visual-novel-profile-1', { profileId: 'visual-novel-profile-1', mode: 'play' }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/visual-novel/works/visual-novel-profile-1/runs', }, { name: 'match3d', start: () => startMatch3DRun('match3d-profile-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/match3d/works/match3d-profile-1/runs', }, { name: 'square-hole', start: () => startSquareHoleRun('square-hole-profile-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/square-hole/works/square-hole-profile-1/runs', }, { name: 'big-fish', start: () => startBigFishRun('big-fish-session-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/big-fish/sessions/big-fish-session-1/runs', }, { name: 'bark-battle', start: () => startBarkBattleRun('bark-battle-work-1', {}, { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/bark-battle/works/bark-battle-work-1/runs', }, { name: 'wooden-fish', start: () => startWoodenFishRuntimeRun('wooden-fish-profile-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/wooden-fish/runs', }, { name: 'puzzle', start: () => startPuzzleRun( { profileId: 'puzzle-profile-1', levelId: 'level-1' }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/puzzle/runs', }, { name: 'puzzle leaderboard', start: () => submitPuzzleLeaderboard( 'run-puzzle-1', { profileId: 'puzzle-profile-1', gridSize: 3, elapsedMs: 18_000, nickname: '玩家', }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/leaderboard', }, ])( '$name start request uses the runtime guest bearer token without touching login auth', async ({ start, expectedUrl }) => { await start(); const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0]; expect(url).toBe(expectedUrl); expect(init).toEqual( expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer runtime-guest-token', }), }), ); expect(options).toEqual( expect.objectContaining({ skipAuth: true, skipRefresh: true, }), ); }, ); it('puzzle next level keeps the default current-run handoff without a request body', async () => { await advancePuzzleNextLevel( 'run-puzzle-1', {}, { runtimeGuestToken: 'runtime-guest-token' }, ); const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0]; expect(url).toBe('/api/runtime/puzzle/runs/run-puzzle-1/next-level'); expect(init).toEqual( expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer runtime-guest-token', }), }), ); expect(init.body).toBeUndefined(); expect(options).toEqual( expect.objectContaining({ skipAuth: true, skipRefresh: true, }), ); }); it('puzzle leaderboard submission does not retry unsafe writes', async () => { await submitPuzzleLeaderboard( 'run-puzzle-1', { profileId: 'puzzle-profile-1', gridSize: 3, elapsedMs: 18_000, nickname: '玩家', }, { runtimeGuestToken: 'runtime-guest-token' }, ); const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0]; expect(url).toBe('/api/runtime/puzzle/runs/run-puzzle-1/leaderboard'); expect(init).toEqual( expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer runtime-guest-token', 'Content-Type': 'application/json', }), }), ); expect(options).toEqual( expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 0, }), }), ); }); it.each([ { name: 'puzzle get run', run: () => getPuzzleRun('run-puzzle-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1', expectedMethod: 'GET', }, { name: 'puzzle swap', run: () => swapPuzzlePieces( 'run-puzzle-1', { firstPieceId: 'piece-a', secondPieceId: 'piece-b' }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/swap', expectedMethod: 'POST', }, { name: 'puzzle drag', run: () => dragPuzzlePieceOrGroup( 'run-puzzle-1', { pieceId: 'piece-a', targetRow: 1, targetCol: 2 }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/drag', expectedMethod: 'POST', }, { name: 'puzzle pause', run: () => updatePuzzleRunPause( 'run-puzzle-1', { paused: true }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/pause', expectedMethod: 'POST', }, { name: 'puzzle prop', run: () => usePuzzleRuntimeProp( 'run-puzzle-1', { propKind: 'extendTime' }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/props', expectedMethod: 'POST', }, { name: 'puzzle-clear get run', run: () => puzzleClearClient.getRun('puzzle-clear-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1', expectedMethod: 'GET', }, { name: 'puzzle-clear swap', run: () => puzzleClearClient.swapCards( 'puzzle-clear-run-1', { fromRow: 0, fromCol: 0, toRow: 0, toCol: 1 }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/swap', expectedMethod: 'POST', }, { name: 'puzzle-clear retry', run: () => puzzleClearClient.retryLevel('puzzle-clear-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/retry-level', expectedMethod: 'POST', }, { name: 'puzzle-clear next', run: () => puzzleClearClient.advanceNextLevel('puzzle-clear-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/next-level', expectedMethod: 'POST', }, { name: 'puzzle-clear time up', run: () => puzzleClearClient.markTimeUp('puzzle-clear-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/time-up', expectedMethod: 'POST', }, { name: 'square-hole get run', run: () => getSquareHoleRun('square-hole-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1', expectedMethod: 'GET', }, { name: 'square-hole drop', run: () => dropSquareHoleShape( 'square-hole-run-1', { holeId: 'hole-1', clientSnapshotVersion: 1, clientEventId: 'event-1', droppedAtMs: 1_700_000_000_000, }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/drop', expectedMethod: 'POST', }, { name: 'square-hole stop', run: () => stopSquareHoleRun( 'square-hole-run-1', { clientActionId: 'stop-1' }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/stop', expectedMethod: 'POST', }, { name: 'square-hole restart', run: () => restartSquareHoleRun('square-hole-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/restart', expectedMethod: 'POST', }, { name: 'square-hole time up', run: () => finishSquareHoleTimeUp('square-hole-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/time-up', expectedMethod: 'POST', }, { name: 'big-fish get run', run: () => getBigFishRun('big-fish-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/big-fish/runs/big-fish-run-1', expectedMethod: 'GET', }, { name: 'big-fish input', run: () => submitBigFishInput( 'big-fish-run-1', { x: 0.25, y: 0.75 }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/big-fish/runs/big-fish-run-1/input', expectedMethod: 'POST', }, { name: 'big-fish play report', run: () => recordBigFishPlay( 'big-fish-session-1', { elapsedMs: 1_000 }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/big-fish/sessions/big-fish-session-1/play', expectedMethod: 'POST', }, { name: 'bark-battle config', run: () => getBarkBattleRuntimeConfig('bark-battle-work-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/bark-battle/works/bark-battle-work-1/config', expectedMethod: 'GET', }, { name: 'bark-battle finish', run: () => finishBarkBattleRun( 'bark-battle-run-1', { runId: 'bark-battle-run-1', runToken: 'run-token', workId: 'bark-battle-work-1', configVersion: 1, rulesetVersion: 'v1', difficultyPreset: 'normal', clientStartedAt: '2026-06-10T00:00:00Z', clientFinishedAt: '2026-06-10T00:00:10Z', durationMs: 10_000, derivedMetrics: { triggerCount: 1, maxVolume: 0.8, averageVolume: 0.4, finalEnergy: 10, comboMax: 1, }, }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/bark-battle/runs/bark-battle-run-1/finish', expectedMethod: 'POST', }, { name: 'wooden-fish checkpoint', run: () => checkpointWoodenFishRun( 'wooden-fish-run-1', { totalTapCount: 8, wordCounters: [] }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/wooden-fish/runs/wooden-fish-run-1/checkpoint', expectedMethod: 'POST', }, { name: 'wooden-fish finish', run: () => finishWoodenFishRun( 'wooden-fish-run-1', { totalTapCount: 8, wordCounters: [] }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/wooden-fish/runs/wooden-fish-run-1/finish', expectedMethod: 'POST', }, { name: 'visual-novel get run', run: () => getVisualNovelRun('visual-novel-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/visual-novel/runs/visual-novel-run-1', expectedMethod: 'GET', }, { name: 'visual-novel history', run: () => getVisualNovelHistory('visual-novel-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/visual-novel/runs/visual-novel-run-1/history', expectedMethod: 'GET', }, { name: 'visual-novel regenerate', run: () => regenerateVisualNovelRun( 'visual-novel-run-1', { historyEntryId: 'history-1', clientEventId: 'event-1' }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/visual-novel/runs/visual-novel-run-1/regenerate', expectedMethod: 'POST', }, ])( '$name uses the shared runtime guest bearer token without touching login auth', async ({ run, expectedUrl, expectedMethod }) => { await run(); expectRuntimeGuestRequest(expectedUrl, expectedMethod); }, ); it.each([ { name: 'get run', run: () => getMatch3DRun('match3d-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/match3d/runs/match3d-run-1', expectedMethod: 'GET', }, { name: 'restart', run: () => restartMatch3DRun('match3d-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/match3d/runs/match3d-run-1/restart', expectedMethod: 'POST', }, { name: 'stop', run: () => stopMatch3DRun( 'match3d-run-1', { clientActionId: 'stop-1' }, { runtimeGuestToken: 'runtime-guest-token' }, ), expectedUrl: '/api/runtime/match3d/runs/match3d-run-1/stop', expectedMethod: 'POST', }, { name: 'time up', run: () => finishMatch3DTimeUp('match3d-run-1', { runtimeGuestToken: 'runtime-guest-token', }), expectedUrl: '/api/runtime/match3d/runs/match3d-run-1/time-up', expectedMethod: 'POST', }, ])( 'match3d $name uses the runtime guest bearer token without touching login auth', async ({ run, expectedUrl, expectedMethod }) => { await run(); const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0]; expect(url).toBe(expectedUrl); expect(init).toEqual( expect.objectContaining({ method: expectedMethod, headers: expect.objectContaining({ Authorization: 'Bearer runtime-guest-token', }), }), ); expect(options).toEqual( expect.objectContaining({ skipAuth: true, skipRefresh: true, }), ); }, ); it('match3d click uses the runtime guest bearer token without touching login auth', async () => { apiClientMocks.requestJson.mockResolvedValueOnce({ confirmation: { accepted: true, run: {}, clearedItemInstanceIds: [], }, }); await clickMatch3DItem( 'match3d-run-1', { runId: 'match3d-run-1', itemInstanceId: 'item-1', clientActionId: 'action-1', clientEventId: 'event-1', clickedAtMs: 1_700_000_000_000, clientSnapshotVersion: 1, }, { runtimeGuestToken: 'runtime-guest-token' }, ); const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0]; expect(url).toBe('/api/runtime/match3d/runs/match3d-run-1/click'); expect(init).toEqual( expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer runtime-guest-token', }), }), ); expect(options).toEqual( expect.objectContaining({ skipAuth: true, skipRefresh: true, }), ); }); });