203 lines
6.5 KiB
TypeScript
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);
|
|
});
|