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 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({ data: 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({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) })); await t.flush(); assert.equal(loginResp.playerid, 9); // 门控已清,后续业务包放行 t.serverPush(JSON.stringify({ data: 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({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) })); await t.flush(); t.serverPush(JSON.stringify({ data: '@toconconABC' })); // 握手 t.serverPush(JSON.stringify({ data: 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({ data: 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({ data: 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({ data: JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 1 } }) })); await t1.flush(); assert.equal(messages.length, 0); });