Files
youle_cocos/cocoscreator_projects/framework-tests/protocol/router.test.ts
T

203 lines
6.5 KiB
TypeScript

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);
});