import assert from 'node:assert/strict'; import { test } from 'node:test'; import type { InboundMessage } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; import type { GameEntry, GameHost, GameModule, PlatformGameSnapshot, } from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; import { GameSessionHost } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts'; import { FIRST_SLICE_RPC_ROUTE } from '../../YouleNexus/assets/framework/protocol/first-slice-routes.ts'; class LoginGateFixture { private pending = false; get loginPending(): boolean { return this.pending; } markLoginPending(): void { this.pending = true; } completeLogin(): void { this.pending = false; } } function makeGameSession(received: InboundMessage[]): GameSessionHost { const entry: GameEntry = { key: 'fixture', gameId: 41, route: 'fixture-game', resolveSeatCount: () => 4, createModule: (): GameModule => ({ attach() {}, handlePlatformEvent() {}, handleGameMessage(message) { received.push({ route: entry.route, rpc: message.rpc, data: message.data }); }, restore() {}, dispose() {}, }), }; const snapshot: PlatformGameSnapshot = { connection: { phase: 'logged-in' }, self: { playerId: 1, seat: 0 }, room: { roomcode: '100001', roomtype: [], stage: 0, needprepare: 1, infinite: 0, }, seats: [], }; const host: GameHost = { seat: { toView: (seat) => seat, toServer: (seat) => seat }, getSnapshot: () => snapshot, subscribe: () => () => {}, sendGameMessage() {}, execute() {}, }; const session = new GameSessionHost(entry); session.open(host); return session; } test('Router applies the exact login gate before its single dispatch split', () => { const gate = new LoginGateFixture(); const platformCalls: InboundMessage[] = []; const gameCalls: InboundMessage[] = []; const gameSession = makeGameSession(gameCalls); const router = new Router({ session: gate, platform: { dispatch(message: InboundMessage) { platformCalls.push(message); if (message.rpc === 'player_login') gate.completeLogin(); return 'handled-platform' as const; }, }, gameSession, }); const updateBeanMessage: InboundMessage = { route: 'agent', rpc: 'update_bean', data: { bean: 10 }, }; const playerLoginMessage: InboundMessage = { route: 'agent', rpc: 'player_login', data: { state: 0 }, }; const gameMessage: InboundMessage = { route: 'fixture-game', rpc: 'deal', data: { card: 7 }, }; gate.markLoginPending(); assert.equal(router.dispatch(updateBeanMessage), 'ignored-login-gate'); assert.equal(router.dispatch(gameMessage), 'ignored-login-gate'); assert.equal(router.dispatch(playerLoginMessage), 'handled-platform'); assert.equal(router.dispatch(gameMessage), 'handled-game'); assert.deepEqual(platformCalls, [playerLoginMessage]); assert.deepEqual(gameCalls, [gameMessage]); }); test('Router lets kick_server through the login gate but ignores every other rpc', () => { const gate = new LoginGateFixture(); const calls: InboundMessage[] = []; const router = new Router({ session: gate, platform: { dispatch(message: InboundMessage) { calls.push(message); return 'handled-platform' as const; }, }, gameSession: makeGameSession([]), }); const kick: InboundMessage = { route: 'agent', rpc: 'kick_server', data: { msg: 'bye' } }; gate.markLoginPending(); assert.equal(router.dispatch(kick), 'handled-platform'); assert.deepEqual(calls, [kick]); }); test('Router login gate enforces the complete route by rpc cross matrix', () => { const platformRoutes = ['platform', 'agent', 'room'] as const; const routes = [...platformRoutes, 'fixture-game'] as const; const rpcs = [...Object.keys(FIRST_SLICE_RPC_ROUTE), 'unknown_rpc']; for (const route of routes) { for (const rpc of rpcs) { const gate = new LoginGateFixture(); const platformCalls: InboundMessage[] = []; const gameCalls: InboundMessage[] = []; const router = new Router({ session: gate, platform: { dispatch(message: InboundMessage) { platformCalls.push(message); return 'handled-platform' as const; }, }, gameSession: makeGameSession(gameCalls), }); const message: InboundMessage = { route, rpc, data: { marker: `${route}:${rpc}` } }; gate.markLoginPending(); const passes = route === 'agent' && (rpc === 'player_login' || rpc === 'kick_server'); assert.equal( router.dispatch(message), passes ? 'handled-platform' : 'ignored-login-gate', `${route}:${rpc}`, ); assert.deepEqual(platformCalls, passes ? [message] : [], `${route}:${rpc}`); assert.deepEqual(gameCalls, [], `${route}:${rpc}`); } } }); test('Router sends all platform routes to PlatformHandlers exactly once', () => { const gate = new LoginGateFixture(); const calls: InboundMessage[] = []; const router = new Router({ session: gate, platform: { dispatch(message: InboundMessage) { calls.push(message); return 'handled-platform' as const; }, }, gameSession: makeGameSession([]), }); const messages: InboundMessage[] = [ { route: 'platform', rpc: 'p', data: {} }, { route: 'agent', rpc: 'a', data: {} }, { route: 'room', rpc: 'r', data: {} }, ]; for (const message of messages) { assert.equal(router.dispatch(message), 'handled-platform'); } assert.deepEqual(calls, messages); }); test('Router preserves game payload identity and mismatched routes have no side effects', () => { const gate = new LoginGateFixture(); const gameCalls: InboundMessage[] = []; const platformCalls: InboundMessage[] = []; const router = new Router({ session: gate, platform: { dispatch(message: InboundMessage) { platformCalls.push(message); return 'handled-platform' as const; }, }, gameSession: makeGameSession(gameCalls), }); const data = { cards: [1, 2, 3] }; assert.throws( () => router.dispatch({ route: 'other-game', rpc: 'deal', data }), /route.*fixture-game.*other-game|route.*other-game/i, ); assert.equal(platformCalls.length, 0); assert.equal(gameCalls.length, 0); assert.equal( router.dispatch({ route: 'fixture-game', rpc: 'deal', data }), 'handled-game', ); assert.equal(gameCalls[0]?.data, data); });