'use strict'; const test = require('node:test'); const assert = require('node:assert'); const { PacketCapture, DEFAULT_MAX_PACKETS } = require('../src/core/packetCapture'); /** 造一个假的 mineflayer client:只记下 write 被调用了什么 */ function fakeClient() { const sent = []; return { sent, write(name, params) { sent.push({ name, params }); return true; } }; } /** 一个类实例形式的向量(用来验证「类实例只留坐标」) */ class FakeVec3 { constructor(x, y, z) { this.x = x; this.y = y; this.z = z; } clone() { return new FakeVec3(this.x, this.y, this.z); } } const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); test('没有连接时拒绝开始抓包', () => { const capture = new PacketCapture(); const result = capture.start(1); assert.strictEqual(result.ok, false); assert.match(result.message, /没有可录制的连接/); assert.strictEqual(capture.running, false); }); test('录制期间记录出站包,同时原样发包', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); const original = client.write; assert.strictEqual(capture.start(1).ok, true); assert.strictEqual(capture.running, true); assert.notStrictEqual(client.write, original, '录制期间 write 应被挂钩'); client.write('position_look', { x: 1, y: 2, z: 3, onGround: true }); client.write('player_input', { inputs: { forward: true } }); // 发包链路不受影响:原 write 照样被调用 assert.strictEqual(client.sent.length, 2); assert.deepStrictEqual(client.sent[0], { name: 'position_look', params: { x: 1, y: 2, z: 3, onGround: true } }); const snap = capture.snapshot(); assert.strictEqual(snap.count, 2); assert.deepStrictEqual(snap.byType, { position_look: 1, player_input: 1 }); assert.strictEqual(snap.packets[0].name, 'position_look'); assert.strictEqual(snap.packets[0].params.onGround, true); assert.ok(snap.packets[1].t >= snap.packets[0].t, 't 应为相对开始时间的非递减偏移'); }); test('position_look 的参数是普通对象,yaw / pitch / onGround / flags 都必须保留', () => { // 回归:曾按「有 x/y/z 就当向量」处理,会把 position_look 的 // yaw / pitch / onGround / flags 全部吞掉 —— 而那正是最需要对照的字段 const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); capture.start(); client.write('position_look', { x: 1.23456789, y: 64, z: -3.5, yaw: 90.5, pitch: -12.25, onGround: true, flags: { onGround: true, hasHorizontalCollision: undefined } }); const params = capture.snapshot().packets[0].params; assert.strictEqual(params.yaw, 90.5); assert.strictEqual(params.pitch, -12.25); assert.strictEqual(params.onGround, true); assert.deepStrictEqual(params.flags, { onGround: true, hasHorizontalCollision: null }); assert.strictEqual(params.x, 1.23457, '浮点保留 5 位'); }); test('类实例形式的向量只留坐标', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); capture.start(); client.write('custom_payload', { pos: new FakeVec3(1, 2, 3), name: 'x' }); const params = capture.snapshot().packets[0].params; assert.deepStrictEqual(params.pos, { x: 1, y: 2, z: 3 }); assert.strictEqual(params.name, 'x'); }); test('停止后摘掉钩子并不再记录', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); const original = client.write; capture.start(); client.write('position', { x: 0 }); const result = capture.stop(); assert.strictEqual(result.ok, true); assert.strictEqual(capture.running, false); assert.strictEqual(client.write, original, '停止后 write 应恢复原函数'); client.write('position', { x: 1 }); assert.strictEqual(capture.snapshot().count, 1, '停止后不应再记录'); assert.strictEqual(client.sent.length, 2, '停止后仍然正常发包'); }); test('重复 stop 不会报错', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); capture.start(); assert.strictEqual(capture.stop().ok, true); assert.strictEqual(capture.stop().ok, true); assert.match(capture.stop().message, /没有在抓包/); }); test('到达上限后丢弃并计数,不无限吃内存', () => { const client = fakeClient(); const capture = new PacketCapture({ maxPackets: 3 }); capture.setClient(client); capture.start(); for (let i = 0; i < 10; i++) client.write('position', { x: i }); const snap = capture.snapshot(); assert.strictEqual(snap.count, 3); assert.strictEqual(snap.dropped, 7); assert.strictEqual(snap.packets[2].params.x, 2, '保留的是最早的那几个包'); }); test('snapshot 支持按包名过滤与 limit', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); capture.start(); client.write('position_look', { x: 1 }); client.write('look', { yaw: 1 }); client.write('position_look', { x: 2 }); client.write('position_look', { x: 3 }); const filtered = capture.snapshot({ types: 'position_look,look' }); assert.strictEqual(filtered.count, 4, 'count 是过滤前的总数'); assert.strictEqual(filtered.returned, 4); const onlyLook = capture.snapshot({ types: ['look'] }); assert.strictEqual(onlyLook.returned, 1); assert.strictEqual(onlyLook.packets[0].name, 'look'); const limited = capture.snapshot({ limit: 2 }); assert.strictEqual(limited.returned, 2); assert.strictEqual(limited.count, 4); }); test('给出时长后到点自动停止并摘钩子', async () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); const original = client.write; capture.start(0.05); assert.strictEqual(capture.running, true); await sleep(150); assert.strictEqual(capture.running, false, '到点应自动停止'); assert.strictEqual(client.write, original, '自动停止也要摘钩子'); assert.ok(capture.snapshot().stoppedAt > 0); }); test('录制时长被上限截断', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); const result = capture.start(9999); assert.match(result.message, /60 秒/); capture.stop(); }); test('换连接时先收尾旧录制', () => { const first = fakeClient(); const second = fakeClient(); const capture = new PacketCapture(); capture.setClient(first); capture.start(); first.write('position', { x: 1 }); const firstOriginal = first.write; assert.notStrictEqual(firstOriginal, undefined); capture.setClient(second); assert.strictEqual(capture.running, false, '换连接应停止录制'); assert.strictEqual(capture.snapshot().count, 1, '旧连接录到的内容保留'); first.write('position', { x: 2 }); assert.strictEqual(capture.snapshot().count, 1, '旧连接不再被记录'); }); test('clear 清空已录内容', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); capture.start(); client.write('position', { x: 1 }); assert.strictEqual(capture.snapshot().count, 1); capture.clear(); const snap = capture.snapshot(); assert.strictEqual(snap.count, 0); assert.deepStrictEqual(snap.byType, {}); assert.strictEqual(snap.running, true, 'clear 不影响录制状态'); }); test('抓包自身出异常也绝不影响发包', () => { const client = fakeClient(); const capture = new PacketCapture(); capture.setClient(client); capture.start(); // 造一个序列化时会抛的对象(getter 抛错) const evil = {}; Object.defineProperty(evil, 'boom', { enumerable: true, get() { throw new Error('getter 炸了'); } }); assert.doesNotThrow(() => client.write('custom_payload', evil)); assert.strictEqual(client.sent.length, 1, '异常包仍然要发出去'); }); test('默认上限是一个合理的正数', () => { assert.ok(DEFAULT_MAX_PACKETS > 0); assert.strictEqual(new PacketCapture().maxPackets, DEFAULT_MAX_PACKETS); });