import assert from 'node:assert/strict'; import {EventEmitter} from 'node:events'; import {createRequire} from 'node:module'; import test from 'node:test'; import {register} from 'tsx/esm/api'; const loader = register({namespace: 'bot-functions-test'}); const tsImport = loader.import; test.after(() => loader.unregister()); const names = ['chat', 'jump', 'squat_once', 'squat_continue_switch', 'use_direction_key', 'look_at', 'stop_current_function']; const operations = Object.fromEntries(await Promise.all(names.map(async name => { const {init} = await tsImport(`../bot/function/bot/${name}.ts`, import.meta.url); return [name, () => new init()]; }))); const require = createRequire(import.meta.url); const {Vec3} = createRequire(require.resolve('@wp2508/mineflayer'))('vec3'); function fixture() { const bot = new EventEmitter(); bot.entity = {position: new Vec3(1, 2, 3), eyeHeight: 1.62, onGround: true, isValid: true}; bot._client = Object.assign(new EventEmitter(), {state: 'play'}); bot.controlState = {}; bot.calls = []; bot.getControlState = key => bot.controlState[key] === true; bot.setControlState = (key, enabled) => { bot.calls.push([key, enabled]); bot.controlState[key] = enabled; }; bot.chat = message => { bot.message = message; }; bot.lookAt = async target => { bot.target = target; }; return bot; } function clean(bot, client = bot._client) { for (const event of ['end', 'error', 'physicsTick']) assert.equal(bot.listenerCount(event), 0, event); for (const event of ['end', 'error']) assert.equal(client.listenerCount(event), 0, `client ${event}`); } // Track pending timers as well as using the test runner's deterministic clock. function clock(t) { t.mock.timers.enable({apis: ['setTimeout']}); const schedule = globalThis.setTimeout; const clear = globalThis.clearTimeout; const pending = new Set(); t.mock.method(globalThis, 'setTimeout', (callback, delay, ...args) => { const timer = schedule(() => { pending.delete(timer); callback(...args); }, delay); pending.add(timer); return timer; }); t.mock.method(globalThis, 'clearTimeout', timer => { pending.delete(timer); clear(timer); }); return {tick: ms => t.mock.timers.tick(ms), clean: () => assert.equal(pending.size, 0, 'pending timers')}; } test('chat preserves exact content and validates its single argument without server acknowledgement', async () => { const bot = fixture(); for (const message of [' hello ', '/help', 'x'.repeat(256), '你好']) { assert.equal(await operations.chat().run(bot, [message]), true); assert.equal(bot.message, message); clean(bot); } for (const args of [null, [], ['a', 'b'], [1], [''], [' \t '], ['x'.repeat(257)], ['a\rb'], ['a\nb'], ['a\0b']]) { delete bot.message; assert.equal(await operations.chat().run(bot, args), false); assert.equal(bot.message, undefined); } bot.chat = () => { throw Error('send failed'); }; assert.equal(await operations.chat().run(bot, ['hello']), false); clean(bot); }); test('pulses hold their own key for one physicsTick and listen before enabling', async t => { const timer = clock(t); for (const [name, key] of [['jump', 'jump'], ['squat_once', 'sneak']]) { const bot = fixture(); bot.controlState.forward = true; const pending = operations[name]().run(bot, []); assert.equal(bot.controlState[key], true); timer.tick(999); assert.equal(bot.controlState[key], true); bot.emit('physicsTick'); assert.equal(await pending, true); assert.deepEqual(bot.calls, [[key, true], [key, false]]); assert.equal(bot.controlState.forward, true); clean(bot); timer.clean(); const synchronous = fixture(); const set = synchronous.setControlState; synchronous.setControlState = (control, enabled) => { set(control, enabled); if (enabled) synchronous.emit('physicsTick'); }; assert.equal(await operations[name]().run(synchronous, []), true); assert.equal(synchronous.controlState[key], false); clean(synchronous); timer.clean(); } }); test('pulses time out after one second and reject held keys, airborne jump and extra args', async t => { const timer = clock(t); for (const [name, key] of [['jump', 'jump'], ['squat_once', 'sneak']]) { const bot = fixture(); const pending = operations[name]().run(bot, []); timer.tick(1000); assert.equal(await pending, false); assert.equal(bot.controlState[key], false); clean(bot); timer.clean(); bot.controlState[key] = true; bot.calls = []; assert.equal(await operations[name]().run(bot, []), false); for (const args of [null, [false], [1]]) assert.equal(await operations[name]().run(bot, args), false); assert.deepEqual(bot.calls, []); clean(bot); } const bot = fixture(); bot.entity.onGround = false; assert.equal(await operations.jump().run(bot, []), false); assert.deepEqual(bot.calls, []); timer.clean(); }); test('persistent sneak is explicit and idempotent and survives a concurrent squat pulse', async t => { const timer = clock(t); const bot = fixture(); const fn = operations.squat_continue_switch(); for (const enabled of [true, true, false, false]) { assert.equal(await fn.run(bot, [enabled]), true); assert.equal(bot.controlState.sneak, enabled); clean(bot); } for (const args of [null, [], [0], ['true'], [undefined], [true, false]]) assert.equal(await fn.run(bot, args), false); const pending = operations.squat_once().run(bot, []); assert.equal(await fn.run(bot, [true]), true); bot.emit('physicsTick'); assert.equal(await pending, true); assert.equal(bot.controlState.sneak, true); assert.equal(await operations.squat_once().run(bot, []), false); assert.equal(bot.controlState.sneak, true); assert.equal(await fn.run(bot, [false]), true); clean(bot); timer.clean(); }); test('directions use exact millisecond durations, reject held keys and leave unrelated keys alone', async t => { const timer = clock(t); for (const direction of ['forward', 'back', 'left', 'right']) { for (const duration of [1, 5000]) { const bot = fixture(); bot.controlState.sneak = true; const pending = operations.use_direction_key().run(bot, [direction, duration]); assert.equal(bot.controlState[direction], true); bot.emit('physicsTick'); timer.tick(duration - 1); assert.equal(bot.controlState[direction], true); assert.equal(await operations.use_direction_key().run(bot, [direction, 1]), false); timer.tick(1); assert.equal(await pending, true); assert.deepEqual(bot.calls, [[direction, true], [direction, false]]); assert.equal(bot.controlState.sneak, true); clean(bot); timer.clean(); } } const bot = fixture(); for (const args of [null, [], ['forward'], ['forward', 1, 2], ...['jump', 'sneak', 'Forward', '', 1].map(key => [key, 1]), ...[0, -1, 5001, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, '5', null].map(ms => ['forward', ms])]) assert.equal(await operations.use_direction_key().run(bot, args), false); assert.deepEqual(bot.calls, []); clean(bot); timer.clean(); }); test('owned controls release on end, error, identity change, disconnect and synchronous failure', async t => { const timer = clock(t); for (const [name, key, args] of [['jump', 'jump', []], ['squat_once', 'sneak', []], ['use_direction_key', 'left', ['left', 20]]]) { for (const reason of ['end', 'error', 'client-end', 'client-error', 'self', 'client', 'disconnect', 'throw', 'tick-throw']) { const bot = fixture(); const client = bot._client; if (reason === 'throw' || reason === 'tick-throw') { const set = bot.setControlState; bot.setControlState = (control, enabled) => { set(control, enabled); if (enabled) { if (reason === 'tick-throw') bot.emit('physicsTick'); throw Error('failed'); } }; } const pending = operations[name]().run(bot, args); if (reason === 'end' || reason === 'error') bot.emit(reason, Error('closed')); if (reason.startsWith('client-')) client.emit(reason.slice(7), Error('closed')); if (reason === 'self') bot.entity = {...bot.entity}; if (reason === 'client') bot._client = Object.assign(new EventEmitter(), {state: 'play'}); if (reason === 'disconnect') client.state = 'disconnected'; if (['self', 'client', 'disconnect'].includes(reason)) { bot.emit('physicsTick'); timer.tick(20); } assert.equal(await pending, false, `${name}: ${reason}`); assert.equal(bot.controlState[key], false); clean(bot, client); timer.clean(); } } const bot = fixture(); bot.setControlState = () => { throw Error('API failure'); }; assert.equal(await operations.squat_continue_switch().run(bot, [true]), false); clean(bot); timer.clean(); }); test('look_at constructs a vector from position, accepts decimals and rejects invalid or eye-coincident targets', async t => { const timer = clock(t); const bot = fixture(); assert.equal(await operations.look_at().run(bot, [-1.5, 4.25, 8.75]), true); assert.ok(bot.target instanceof Vec3); assert.deepEqual(bot.target, new Vec3(-1.5, 4.25, 8.75)); assert.deepEqual(bot.entity.position, new Vec3(1, 2, 3)); for (const args of [null, [], [1, 2], [1, 2, 3, 4], ['1', 2, 3], [Infinity, 2, 3], [1, NaN, 3], [1, 2, undefined], [1, 2 + bot.entity.eyeHeight, 3]]) { delete bot.target; assert.equal(await operations.look_at().run(bot, args), false); assert.equal(bot.target, undefined); clean(bot); timer.clean(); } }); test('look_at bounds waits and contains late rejections, synchronous errors and session changes', async t => { const timer = clock(t); for (const reason of ['timeout', 'end', 'client-end', 'error', 'self', 'client', 'disconnect', 'invalid', 'throw', 'reject']) { const bot = fixture(); const client = bot._client; let resolveLook, rejectLook; bot.lookAt = () => new Promise((resolve, reject) => { resolveLook = resolve; rejectLook = reject; }); if (reason === 'throw') bot.lookAt = () => { throw Error('look failed'); }; if (reason === 'reject') bot.lookAt = () => Promise.reject(Error('look failed')); const pending = operations.look_at().run(bot, [1.5, 2.5, 3.5]); if (reason === 'timeout') timer.tick(5000); if (reason === 'end' || reason === 'error') bot.emit(reason, Error('closed')); if (reason === 'client-end') client.emit('end'); if (reason === 'self') bot.entity = {...bot.entity}; if (reason === 'client') bot._client = Object.assign(new EventEmitter(), {state: 'play'}); if (reason === 'disconnect') client.state = 'disconnected'; if (reason === 'invalid') bot.entity.isValid = false; if (['self', 'client', 'disconnect', 'invalid'].includes(reason)) resolveLook(); assert.equal(await pending, false, reason); clean(bot, client); timer.clean(); rejectLook?.(Error('late failure')); await Promise.resolve(); } }); test('all operations reject stale sessions before side effects', async () => { for (const [name, args] of [['chat', ['hello']], ['jump', []], ['squat_once', []], ['squat_continue_switch', [true]], ['use_direction_key', ['forward', 1]], ['look_at', [1, 2, 3]]]) { for (const change of [bot => { bot.entity = null; }, bot => { bot.entity.isValid = false; }, bot => { bot._client.state = 'disconnected'; }]) { const bot = fixture(); change(bot); assert.equal(await operations[name]().run(bot, args), false, name); assert.deepEqual(bot.calls, []); assert.equal(bot.message, undefined); assert.equal(bot.target, undefined); clean(bot); } } }); test('dynamic discovery publishes all bot operations and their exact positional metadata', async () => { const {get_function_list} = await tsImport('../bot/function/init.ts', import.meta.url); const category = (await get_function_list()).find(item => item.path === 'bot'); assert.deepEqual(category.function_list.map(item => item.name).sort(), [...names].sort()); const expected = {stop_current_function: [], chat: [['message', 'string']], jump: [], squat_once: [], squat_continue_switch: [['enabled', 'boolean']], use_direction_key: [['direction', 'string'], ['duration_ms', 'number']], look_at: [['x', 'number'], ['y', 'number'], ['z', 'number']]}; for (const fn of category.function_list) { assert.equal(fn.i18n.lang, 'zh-CN'); assert.ok(fn.i18n.name && fn.i18n.description); assert.deepEqual(fn.args.map(arg => [arg.name, arg.type]), expected[fn.name]); for (const arg of fn.args) { assert.equal(arg.i18n.lang, 'zh-CN'); assert.ok(arg.i18n.name && arg.i18n.description); assert.equal(Object.hasOwn(arg, 'default'), false); } } });