import assert from 'node:assert/strict'; import {EventEmitter} from 'node:events'; import {mkdtemp, mkdir, writeFile, symlink, rm} from 'node:fs/promises'; import {tmpdir} from 'node:os'; import path from 'node:path'; import {performance} from 'node:perf_hooks'; import {test} from 'node:test'; import {register} from 'tsx/esm/api'; // One module graph is essential: separate tsImport namespaces would isolate the shared WeakMap meter. register(); const {instrument_bot, measure_sync, ResourceInfo} = await import('../bot/info/resource_metrics.ts'); const {init: CpuInfo} = await import('../bot/info/instance/used_cpu.ts'); const {NetworkCounter, init: NetworkInfo} = await import('../bot/info/instance/used_network.ts'); const {estimate_memory, init: MemoryInfo} = await import('../bot/info/instance/used_memory.ts'); const {scan_storage, init: StorageInfo} = await import('../bot/info/instance/used_storage.ts'); const {create_info_manager} = await import('../bot/info/init.ts'); const {create_function_manager} = await import('../bot/function/init.ts'); function bot() { const bot = new EventEmitter(); bot._client = new EventEmitter(); return bot; } test('sync meter deduplicates nested bot/client events and preserves invocation semantics', t => { const clock = t.mock.method(performance, 'now'); let now = 0; clock.mock.mockImplementation(() => now); const b = bot(); const original = b.emit; const clientOriginal = b._client.emit; const meter = instrument_bot(b); b._client.on('packet', function(a, c) { assert.equal(this, b._client); assert.deepEqual([a, c], [1, 2]); now += 3; b.emit('physicsTick'); now += 2; }); b.on('physicsTick', () => { now += 5; }); assert.equal(b._client.emit('packet', 1, 2), true); assert.equal(meter.total(), 10); assert.equal(b.emit('absent'), false); const error = new Error('same error'); b.on('failure', () => { now += 7; throw error; }); assert.throws(() => b.emit('failure'), e => e === error); assert.equal(meter.total(), 17); const other = bot(); other.on('x', () => { now += 100; }); other.emit('x'); assert.equal(meter.total(), 17); meter.dispose(); meter.dispose(); assert.equal(b.emit, original); assert.equal(b._client.emit, clientOriginal); assert.equal(Object.hasOwn(b, 'emit'), false); }); test('sync invocation never awaits promises and disposal respects replacement hooks', async t => { let now = 0; t.mock.method(performance, 'now', () => now); const b = bot(); const meter = instrument_bot(b); let release; const pending = new Promise(resolve => { release = resolve; }); assert.equal(measure_sync(b, () => { now += 4; return pending; }), pending); now += 500; release(true); await pending; assert.equal(meter.total(), 4); const replacement = () => 'replacement'; b.emit = replacement; meter.dispose(); assert.equal(b.emit, replacement); }); test('CPU samples actual elapsed deltas and cancels timer on disposal', async t => { t.mock.timers.enable({apis: ['setInterval']}); let now = 0; t.mock.method(performance, 'now', () => now); const b = bot(); const info = new CpuInfo(b); assert.equal(info.get_info().usage_percent, null); b.on('physicsTick', () => { now += 20; }); b.emit('physicsTick'); now = 2000; t.mock.timers.tick(1000); assert.equal(info.get_info().usage_percent, 1); assert.equal(info.get_info().sampled_time_ms, 20); assert.equal(info.get_info().interval_ms, 2000); assert.match(info.get_info().coverage, /协议解析/); let updates = 0; info.on_update(() => updates++); info.dispose(); await Promise.resolve(); t.mock.timers.tick(5000); assert.equal(updates, 0); assert.equal(b.listenerCount('end'), 0); assert.throws(() => info.on_update(() => {}), /disposed/); }); test('function manager meters synchronous run invocation, with no promise wait', async t => { const b = bot(); b.entity = {isValid: true}; b._client.state = 'play'; const manager = await create_function_manager({bot_instance: b, bot_status: 'online'}); const meter = instrument_bot(b); t.after(() => { manager.dispose(); meter.dispose(); }); let now = 0; t.mock.method(performance, 'now', () => now); b.chat = () => { now += 12; }; assert.equal(await manager.run('bot', 'chat', ['hello']), true); assert.equal(meter.total(), 12); }); test('socket counters use elapsed intervals and retain observed totals across resets and gaps', () => { const counter = new NetworkCounter(); const first = {bytesRead: 100, bytesWritten: 50}; assert.equal(counter.sample(undefined, 0).available, false); assert.equal(counter.sample(first, 100).received_bytes_per_second, null); first.bytesRead += 300; first.bytesWritten += 100; const sample = counter.sample(first, 1600); assert.equal(sample.received_bytes_per_second, 200); assert.equal(sample.sent_bytes_per_second, 100 / 1.5); assert.equal(sample.received_bytes, 400); const second = {bytesRead: 20, bytesWritten: 10}; assert.equal(counter.sample(second, 2000).received_bytes_per_second, null); assert.equal(counter.sample(undefined, 2500).received_bytes, 420); second.bytesRead = 30; assert.equal(counter.sample(second, 3000).received_bytes, 430); second.bytesRead = 2; assert.equal(counter.sample(second, 4000).received_bytes_per_second, null); assert.equal(counter.sample(second, 5000).received_bytes, 432); }); test('memory scans own descriptors, real columns, cycles and shared backing buffers without getters', async () => { const backing = new ArrayBuffer(1024); const column = {a: new Uint8Array(backing), b: new DataView(backing)}; Object.defineProperty(column.a, 'buffer', {get() { throw new Error('no getter'); }}); const b = bot(); b.registry = {large: new ArrayBuffer(100000)}; b.world = {getColumns: () => [{chunkX: 0, chunkZ: 0, column}], external: b.registry}; b.inventory = {slots: [column.a], registry: b.registry}; b.currentWindow = b.inventory; b.entities = {one: {name: 'one'}}; b.players = {one: b.entities.one}; b.entities.one.self = b.entities.one; const original = await estimate_memory(b); assert.equal(original.partial, false); assert.ok(original.total_bytes >= 1024 && original.total_bytes < 3000); const duplicated = await estimate_memory({...b, currentWindow: {...b.inventory}}); assert.ok(duplicated.total_bytes > original.total_bytes); Object.defineProperty(b.entities.one, 'danger', {get() { throw new Error('getter executed'); }}); assert.equal((await estimate_memory(b)).partial, true); assert.equal((await estimate_memory(b, () => true)).partial, true); assert.equal((await estimate_memory(b, () => false, {nodes: 1, properties: 10, milliseconds: 1000})).partial, true); b.world.getColumns = () => { throw new Error('unavailable'); }; assert.equal((await estimate_memory(b)).partial, true); }); for (const hugeCategory of ['chunks', 'entities']) { for (const cap of ['nodes', 'properties']) { test(`memory reserves category budgets with huge ${hugeCategory} and a ${cap} cap`, async () => { const b = bot(); // Wide objects exercise ownKeys; nested objects exercise independent node limits. const huge = Object.fromEntries(Array.from({length: 10000}, (_, i) => [`key${i}`, {value: i}])); b.world = {getColumns: () => [{column: hugeCategory === 'chunks' ? huge : {data: new Uint8Array(64)}}]}; b.entities = hugeCategory === 'entities' ? huge : {one: {name: 'entity'}}; b.inventory = {slots: [{name: 'stone'}]}; b.time = {age: 100n}; const limits = {nodes: 100000, properties: 100000, milliseconds: 10000, [cap]: 80}; const result = await estimate_memory(b, () => false, limits); for (const category of ['chunks', 'entities', 'inventory', 'other']) { assert.ok(result[`${category}_bytes`] > 0, category); assert.equal(result.categories_complete[category], category !== hugeCategory, category); } assert.equal(result.partial, true); assert.equal(result.total_bytes, result.chunks_bytes + result.entities_bytes + result.inventory_bytes + result.other_bytes); }); } } test('memory deduplicates shared objects and backing stores across categories deterministically', async () => { const sample = async size => { const b = bot(); const backing = new ArrayBuffer(size); const shared = {view: new Uint8Array(backing)}; shared.self = shared; b.world = {getColumns: () => [{column: {shared, view: new DataView(backing)}}]}; b.entities = {shared, view: new Uint8Array(backing)}; b.inventory = {shared, view: new DataView(backing)}; b.game = {shared, backing}; return estimate_memory(b, () => false, {nodes: 1000, properties: 1000, milliseconds: 10000}); }; const small = await sample(100); const large = await sample(1100); assert.equal(large.total_bytes - small.total_bytes, 1000); assert.deepEqual(large, await sample(1100)); assert.deepEqual(large.categories_complete, {chunks: true, entities: true, inventory: true, other: true}); assert.equal(large.partial, false); }); test('memory scans only allowlisted other roots and reports omissions without calling getters', async () => { const b = bot(); const roots = ['game', 'time', 'settings', 'scoreboards', 'teams', 'tablist']; for (const key of roots) { const result = await estimate_memory(Object.assign(bot(), {[key]: {data: 'owned'}})); assert.ok(result.other_bytes > 0, key); assert.equal(result.categories_complete.other, true); } b.unlisted = {data: new ArrayBuffer(1000000)}; assert.equal((await estimate_memory(b)).other_bytes, 0); let getters = 0; Object.defineProperty(b, 'game', {get() { getters++; return {}; }}); b.world = {}; Object.defineProperty(b.world, 'getColumns', {get() { getters++; return () => []; }}); b.entities = {one: {name: 'one'}}; Object.defineProperty(b.entities.one, 'danger', {get() { getters++; return 123; }}); const result = await estimate_memory(b); assert.equal(getters, 0); assert.deepEqual(result.categories_complete, {chunks: false, entities: false, inventory: true, other: false}); assert.equal(result.partial, true); for (const limits of [{nodes: 0, properties: 100, milliseconds: 1000}, {nodes: 100, properties: 0, milliseconds: 1000}, {nodes: 100, properties: 100, milliseconds: 0}]) { const capped = await estimate_memory(b, () => false, limits); assert.equal(capped.categories_complete.entities, false); assert.equal(capped.entities_bytes, 0); assert.equal(capped.partial, true); } const cancelled = await estimate_memory(b, () => true); assert.deepEqual(cancelled.categories_complete, {chunks: false, entities: false, inventory: false, other: false}); }); test('memory marks column and unsupported collection truncation per category', async () => { const b = bot(); b.world = {getColumns: () => Array.from({length: 4097}, () => ({column: {}}))}; b.game = new Map([['data', 'omitted']]); const result = await estimate_memory(b, () => false, {nodes: 50000, properties: 50000, milliseconds: 10000}); assert.deepEqual(result.categories_complete, {chunks: false, entities: true, inventory: true, other: false}); assert.equal(result.partial, true); }); test('storage scans nested files without following symlinks and reports missing, error and bounds', async t => { const directory = await mkdtemp(path.join(tmpdir(), 'mf-resource-')); t.after(() => rm(directory, {recursive: true, force: true})); await mkdir(path.join(directory, 'nested')); await writeFile(path.join(directory, 'a'), 'abc'); await writeFile(path.join(directory, 'nested', 'b'), '12345'); await symlink(directory, path.join(directory, 'nested', 'loop')); await symlink(path.join(directory, 'a'), path.join(directory, 'alias')); assert.deepEqual(await scan_storage(directory), {available: true, total_bytes: 8, file_count: 2, partial: false}); assert.deepEqual(await scan_storage(path.join(directory, 'missing')), {available: true, total_bytes: 0, file_count: 0, partial: false}); assert.equal((await scan_storage(path.join(directory, 'a'))).available, false); assert.equal((await scan_storage(directory, () => true)).partial, true); assert.equal((await scan_storage(directory, () => false, {entries: 2, depth: 32, milliseconds: 1000})).partial, true); assert.equal((await scan_storage(directory, () => false, {entries: 100, depth: 1, milliseconds: 1000})).partial, true); }); test('resource modules have complete field metadata, no end listeners and safe async disposal', async t => { t.mock.timers.enable({apis: ['setInterval']}); const b = bot(); const infos = [new CpuInfo(b), new NetworkInfo(b), new MemoryInfo(b), new StorageInfo(b)]; assert.deepEqual(infos[2].get_info().categories_complete, {chunks: false, entities: false, inventory: false, other: false}); assert.equal(infos[2].get_info().partial, true); let updates = 0; for (const info of infos) { assert.equal(info.i18n.lang, 'zh-CN'); assert.match(info.i18n.name, /[\u4e00-\u9fff]/u); assert.match(info.i18n.description, /[\u4e00-\u9fff]/u); for (const key of Object.keys(info.get_info())) { const field = info.i18n.fields[key]; assert.ok(field, key); assert.equal(field.lang, 'zh-CN'); assert.notEqual(field.name, key); assert.match(field.name, /[\u4e00-\u9fff]/u); assert.match(field.description, /[\u4e00-\u9fff]/u); } const checkFields = (value, metadata) => { for (const [key, entry] of Object.entries(value)) { const field = metadata.fields[key]; assert.ok(field, key); assert.equal(field.lang, 'zh-CN'); assert.match(field.name, /[\u4e00-\u9fff]/u); assert.match(field.description, /[\u4e00-\u9fff]/u); if (entry && typeof entry === 'object') checkFields(entry, field); } }; checkFields(info.get_info(), info.i18n); info.on_update(() => updates++); } assert.equal(b.listenerCount('end'), 0); for (const info of infos) { info.dispose(); info.dispose(); } await Promise.resolve(); t.mock.timers.tick(60000); assert.equal(updates, 0); assert.equal(infos[3].get_info().available, false); }); test('async resource sampling does not overlap or publish after disposal', async t => { t.mock.timers.enable({apis: ['setInterval']}); let release; let calls = 0; class Sample extends ResourceInfo { name = 'sample'; i18n = {lang: 'en', name: 'Sample', description: ''}; constructor() { super(0); this.start(1000, async () => { calls++; await new Promise(resolve => { release = resolve; }); this.publish(1); }, true); } } const sample = new Sample(); t.mock.timers.tick(30000); assert.equal(calls, 1); sample.dispose(); release(); await Promise.resolve(); await Promise.resolve(); assert.equal(sample.get_info(), 0); t.mock.timers.tick(30000); assert.equal(calls, 1); }); test('InfoManager discovers all resource modules and owns wrappers through end/recreation', async () => { const b = bot(); const original = b.emit; const manager = await create_info_manager(b); for (const name of ['used_cpu', 'used_memory', 'used_network', 'used_storage']) { assert.ok(manager.get_info(name), name); } assert.equal(b.listenerCount('end'), 1); assert.notEqual(b.emit, original); b.emit('end'); assert.equal(b.emit, original); assert.equal(b.listenerCount('end'), 0); assert.equal(manager.get_all_info().length, 0); const next = await create_info_manager(b); next.dispose(); assert.equal(b.emit, original); });