Files
youle_cocos/cocoscreator_projects/framework-tests/net/net-client.test.ts
T
joywayerandClaude Opus 4.8 883335f656 fix(framework): kick_server 终止不重连(对齐真实客户端)
收到 kick_server 时 emit kicked 并 stop(),不再 onClose→重连→重踢空转。
kick 处理移出登录门控,登录态内外一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:37:16 +08:00

182 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { NetClient } from '../../YouleNexus/assets/framework/net/net-client.ts';
import { FakeTransport } from '../helpers/fake-transport.ts';
import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts';
function fakeClock() {
let seq = 0, now = 0;
const timers = new Map<number, { fn: () => void; at: number }>();
const clock: Clock = {
setTimeout: (fn, ms) => { const id = ++seq; timers.set(id, { fn, at: now + ms }); return id; },
clearTimeout: (id: any) => { timers.delete(id); },
};
const advance = (ms: number) => { now += ms; for (const [id, t] of [...timers]) if (t.at <= now) { timers.delete(id); t.fn(); } };
return { clock, advance };
}
const IDENTITY = { agentid: 1, gameid: 2, openid: 'o', nickname: 'n', avatar: 'a', sex: 0, province: '', city: '', unionid: 'u', version: 1, channelid: 0, marketid: 0 };
function makeClient(transports: FakeTransport[], clock: Clock) {
let i = 0;
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => transports[i++] ?? transports[transports.length - 1],
clock,
});
client.setIdentity(IDENTITY);
return client;
}
test('onOpen 后自动发 player_login(单层信封)', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const client = makeClient([t], clock);
client.start();
await t.flush();
assert.equal(t.sent.length, 1);
const env = JSON.parse(t.sent[0]);
assert.equal(env.app, 'youle');
assert.equal(env.route, 'agent');
assert.equal(env.rpc, 'player_login');
assert.equal(env.data.openid, 'o');
});
test('isSendLoginState 门控:login 响应前的其它包被丢弃,login 响应放行并 emit login', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const client = makeClient([t], clock);
const messages: any[] = [];
let loginResp: any = null;
client.on('message', (m: any) => messages.push(m));
client.on('login', (d: any) => { loginResp = d; });
client.start();
await t.flush();
// 登录响应前推一个业务包 → 应被门控丢弃
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }));
await t.flush();
assert.equal(messages.length, 0);
// 推 login 响应 → 放行,emit login,清门控
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
assert.equal(loginResp.playerid, 9);
// 门控已清,后续业务包放行
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }));
await t.flush();
assert.equal(messages.length, 1);
assert.equal(messages[0].rpc, 'update_bean');
});
test('握手包与心跳包被忽略且不进 message', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const client = makeClient([t], clock);
const messages: any[] = [];
client.on('message', (m: any) => messages.push(m));
client.start();
await t.flush();
// 先完成登录以清门控
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
t.serverPush('@toconconABC'); // 握手(原始串)
t.serverPush(JSON.stringify({ com: '@serverheartbeat' })); // 心跳(单层)
await t.flush();
assert.equal(messages.length, 0);
});
test('收包超时 emit slow 并触发重连(onClose→新连接重发 login)', async () => {
const t1 = new FakeTransport(), t2 = new FakeTransport();
const { clock, advance } = fakeClock();
const client = makeClient([t1, t2], clock);
let slow = 0;
client.on('slow', () => { slow++; });
client.start();
await t1.flush();
// 先完成登录:清 4s 守护、feed 收包看门狗(否则 advance 会先撞上 loginGuard 而非 watchdog)
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t1.flush();
// 30s 无收包 → watchdog → slow + 关闭当前连接
advance(30000);
await t1.flush();
assert.equal(slow, 1);
// 重连定时器到点 → 新连接 t2 建立并重发 login
advance(10000);
await t2.flush();
assert.equal(t2.sent.length, 1);
assert.equal(JSON.parse(t2.sent[0]).rpc, 'player_login');
});
test('TcpID 去重:旧连接的残留包被丢弃', async () => {
const t1 = new FakeTransport(), t2 = new FakeTransport();
const { clock, advance } = fakeClock();
const client = makeClient([t1, t2], clock);
const messages: any[] = [];
client.on('message', (m: any) => messages.push(m));
client.start();
await t1.flush();
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t1.flush();
// 触发重连切到 t2
advance(30000); await t1.flush(); advance(10000); await t2.flush();
// 旧连接 t1 仍推包 → 应被 TcpID 去重丢弃
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 1 } }));
await t1.flush();
assert.equal(messages.length, 0);
});
test('kick_server:emit kicked 且不再自动重连', async () => {
const t1 = new FakeTransport(), t2 = new FakeTransport();
const { clock, advance } = fakeClock();
const client = makeClient([t1, t2], clock);
let kicked = 0;
client.on('kicked', () => { kicked++; });
client.start();
await t1.flush();
// 服务器踢人(登录态期间)
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'kick_server', data: { msg: 'x' } }));
await t1.flush();
assert.equal(kicked, 1);
// 推进重连/超时定时器 → 不应建立新连接或重发 login
advance(30000); advance(10000);
await t2.flush();
assert.equal(t2.sent.length, 0);
});
test('debug=true 记录 send 与 recv(注入 logger)', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const logs: unknown[][] = [];
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => t,
clock,
debug: true,
logger: (...a: unknown[]) => logs.push(a),
});
client.setIdentity(IDENTITY);
client.start();
await t.flush();
assert.ok(logs.some((l) => l[0] === '[net] →')); // 发出 player_login 被记录
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
assert.ok(logs.some((l) => l[0] === '[net] ←')); // 收包被记录
});
test('debug 默认关闭:logger 不被调用', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const logs: unknown[][] = [];
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => t,
clock,
logger: (...a: unknown[]) => logs.push(a),
});
client.setIdentity(IDENTITY);
client.start();
await t.flush();
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
assert.equal(logs.length, 0);
});