统一推荐页游客运行态与切换队列
统一推荐页各玩法正式 runtime 的游客鉴权透传。 收口推荐页首页展示队列和嵌入运行态切换队列。 补齐未登录读档、签名资产和个人数据读取的游客态处理。 新增运行态 HUD 小尺寸 logo 资源并更新拼图与抓鹅展示。 补充推荐切换、runtime guest 启动和客户端请求回归测试。 更新玩法链路、后端契约和团队记忆文档。
This commit is contained in:
@@ -13,17 +13,56 @@ vi.mock('./apiClient', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { startBarkBattleRun } from './bark-battle-runtime/barkBattleRuntimeClient';
|
||||
import { startBigFishRun } from './big-fish-runtime/bigFishRuntimeClient';
|
||||
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 { startMatch3DRun } from './match3d-runtime/match3dRuntimeClient';
|
||||
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 { startSquareHoleRun } from './square-hole-runtime/squareHoleRuntimeClient';
|
||||
import { startVisualNovelRun } from './visual-novel-runtime/visualNovelRuntimeClient';
|
||||
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(() => {
|
||||
@@ -31,6 +70,25 @@ describe('recommended runtime guest launch clients', () => {
|
||||
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',
|
||||
@@ -82,6 +140,14 @@ describe('recommended runtime guest launch clients', () => {
|
||||
}),
|
||||
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: () =>
|
||||
@@ -187,4 +253,397 @@ describe('recommended runtime guest launch clients', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user