refactor(platform): unify routing and session dispatch
This commit is contained in:
@@ -1,75 +1,166 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts';
|
||||
import { ActiveGame } from '../../YouleNexus/assets/framework/protocol/active-game.ts';
|
||||
import { test } from 'node:test';
|
||||
import type { InboundMessage } from '../../YouleNexus/assets/framework/core/types/envelope.ts';
|
||||
import type { IGameModule, GameContext } from '../../YouleNexus/assets/framework/sdk/index.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';
|
||||
|
||||
class MockGame implements IGameModule {
|
||||
readonly route = 'mock';
|
||||
events: string[] = [];
|
||||
onEnter(_ctx: GameContext): void {}
|
||||
onExit(): void {}
|
||||
onReceive(rpc: string, _d: unknown): void { this.events.push(`onReceive:${rpc}`); }
|
||||
onReconnect(_d: unknown): void {}
|
||||
class LoginGateFixture {
|
||||
private pending = false;
|
||||
|
||||
get loginPending(): boolean { return this.pending; }
|
||||
markLoginPending(): void { this.pending = true; }
|
||||
completeLogin(): void { this.pending = false; }
|
||||
}
|
||||
|
||||
const msg = (route: string, rpc: string, data: unknown = {}): InboundMessage =>
|
||||
({ route, rpc, data } as InboundMessage);
|
||||
|
||||
test('platform route → 平台 handler(已注册)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const calls: Array<[string, unknown]> = [];
|
||||
const router = new Router({
|
||||
activeGame: slot,
|
||||
platformHandlers: { player_login: (d) => calls.push(['player_login', d]) },
|
||||
});
|
||||
router.dispatch(msg('agent', 'player_login', { state: 0 }));
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][0], 'player_login');
|
||||
});
|
||||
|
||||
test('platform route → 平台 handler 未注册 → 显式抛错(第二准则)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('agent', 'unknown_rpc')), /unknown_rpc/);
|
||||
});
|
||||
|
||||
test('非 platform route 但 activeGame 已注册 → onReceive', () => {
|
||||
const slot = new ActiveGame();
|
||||
const game = new MockGame();
|
||||
slot.set(game);
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
router.dispatch(msg('mock', 'deal_card', { n: 1 }));
|
||||
assert.deepEqual(game.events, ['onReceive:deal_card']);
|
||||
});
|
||||
|
||||
test('非 platform route 但 activeGame 未注册 → 显式抛错', () => {
|
||||
const slot = new ActiveGame();
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('mock', 'deal_card')), /mock.*deal_card/);
|
||||
});
|
||||
|
||||
test('route 与 activeGame.route 不匹配 → 显式抛错', () => {
|
||||
const slot = new ActiveGame();
|
||||
slot.set(new MockGame()); // route = 'mock'
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('other-game', 'rpc')), /other-game/);
|
||||
});
|
||||
|
||||
test('platform/agent/room 三 route 全部走平台 handler(对称)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const calls: string[] = [];
|
||||
const router = new Router({
|
||||
activeGame: slot,
|
||||
platformHandlers: {
|
||||
r1: () => calls.push('r1'),
|
||||
r2: () => calls.push('r2'),
|
||||
r3: () => calls.push('r3'),
|
||||
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,
|
||||
});
|
||||
router.dispatch(msg('platform', 'r1'));
|
||||
router.dispatch(msg('agent', 'r2'));
|
||||
router.dispatch(msg('room', 'r3'));
|
||||
assert.deepEqual(calls, ['r1', 'r2', 'r3']);
|
||||
});
|
||||
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 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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user