chore: initial commit

This commit is contained in:
2026-10-22 17:07:08 +08:00
commit 0acef64f48
200 changed files with 35546 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
import assert from 'node:assert/strict';
import {EventEmitter, getEventListeners} from 'node:events';
import {createRequire} from 'node:module';
import test from 'node:test';
import {register} from 'tsx/esm/api';
const loader = register({namespace: 'function-cancellation-test'});
const tsImport = loader.import;
test.after(() => loader.unregister());
const {create_function_manager} = await tsImport('../bot/function/init.ts', import.meta.url);
const {cancel_functions, cancellation_generation, create_function_cancellation, on_function_cancel} =
await tsImport('../bot/function/cancellation.ts', import.meta.url);
const require = createRequire(import.meta.url);
const {Vec3} = createRequire(require.resolve('@wp2508/mineflayer'))('vec3');
const turn = () => new Promise(resolve => setImmediate(resolve));
const deferred = () => {
let resolve, reject;
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
return {promise, resolve, reject};
};
const listeners = signal => getEventListeners(signal, 'abort').length;
async function fixture(t) {
const bot = new EventEmitter();
bot.entity = {id: 1, position: new Vec3(0, 0, 0), height: 1.8, eyeHeight: 1.62, onGround: true};
bot._client = Object.assign(new EventEmitter(), {state: 'play'});
bot.controlState = {};
bot.getControlState = key => bot.controlState[key] === true;
bot.setControlState = (key, enabled) => { bot.controlState[key] = enabled; };
bot.stopDigging = () => {};
bot.deactivateItem = () => {};
bot.clearControlStates = () => { bot.controlState = {}; };
const messages = [];
bot.chat = message => messages.push(message);
bot.inventory = {slots: [{name: 'stone'}, null], selectedItem: null};
const manager = await create_function_manager({bot_instance: bot, bot_status: 'online'});
t.after(() => manager.dispose());
return {bot, manager, messages};
}
test('pre-aborted calls preserve their reason and never execute callbacks or the stop command', async t => {
const {bot, manager} = await fixture(t);
const click = deferred();
bot.clickWindow = () => click.promise;
const running = manager.run('inventory', 'slot_to_slot', [0, 1]);
await turn();
const queued = manager.run('bot', 'chat', ['queued']);
let before = 0;
for (const reason of [undefined, Error('cancelled'), 'custom reason', null]) {
const controller = new AbortController();
controller.abort(reason);
for (const [name, args] of [['chat', ['ignored']], ['stop_current_function', []]]) {
await assert.rejects(manager.run('bot', name, args, () => before++, {signal: controller.signal}),
error => error === controller.signal.reason);
}
assert.equal(listeners(controller.signal), 0);
}
assert.equal(before, 0);
click.resolve();
assert.equal(await running, true);
assert.equal(await queued, true);
});
test('aborting queued calls frees capacity without cancelling the current call or other queued calls', async t => {
const {bot, manager, messages} = await fixture(t);
const click = deferred();
bot.clickWindow = () => click.promise;
const running = manager.run('inventory', 'slot_to_slot', [0, 1]);
await turn();
const controller = new AbortController();
let before = 0;
const cancelled = manager.run('bot', 'chat', ['cancelled'], () => before++, {signal: controller.signal});
const rejected = assert.rejects(cancelled, {name: 'AbortError'});
const queued = Array.from({length: 63}, (_, index) => manager.run('bot', 'chat', [`queued ${index}`]));
await assert.rejects(manager.run('bot', 'chat', ['full']), /队列已满/);
controller.abort();
await rejected;
assert.equal(listeners(controller.signal), 0);
const replacement = manager.run('bot', 'chat', ['replacement']);
assert.deepEqual(messages, []);
click.resolve();
assert.equal(await running, true);
assert.deepEqual(await Promise.all(queued), Array(63).fill(true));
assert.equal(await replacement, true);
assert.equal(before, 0);
assert.equal(messages.length, 64);
assert.equal(messages.at(-1), 'replacement');
});
test('executing abort notifies existing control helpers and preserves unrelated queued calls and listeners', async t => {
const {bot, manager, messages} = await fixture(t);
let globalCancels = 0;
t.after(on_function_cancel(bot, () => globalCancels++));
const controller = new AbortController();
let before = 0;
const running = manager.run('bot', 'use_direction_key', ['forward', 5000], () => before++, {signal: controller.signal});
const rejected = assert.rejects(running, {name: 'AbortError'});
await turn();
assert.equal(bot.controlState.forward, true);
const queued = manager.run('bot', 'chat', ['survived']);
controller.abort();
await rejected;
assert.equal(await queued, true);
assert.equal(bot.controlState.forward, false);
assert.equal(before, 1);
assert.equal(globalCancels, 0);
assert.deepEqual(messages, ['survived']);
assert.deepEqual(bot.eventNames(), []);
assert.deepEqual(bot._client.eventNames(), []);
assert.equal(listeners(controller.signal), 0);
});
test('executing abort drains inventory execution before rejecting and starting the next call', async t => {
for (const fails of [false, true]) {
await t.test(fails ? 'execution rejects' : 'execution resolves', async t => {
const {bot, manager, messages} = await fixture(t);
const click = deferred();
const clicks = [];
let cancelled = 0;
bot.clickWindow = slot => {
clicks.push(slot);
const unwatch = on_function_cancel(bot, () => cancelled++);
return click.promise.finally(unwatch);
};
const controller = new AbortController();
const reason = Error('workflow aborted');
let settled = false;
const running = manager.run('inventory', 'slot_to_slot', [0, 1], undefined, {signal: controller.signal});
const rejected = assert.rejects(running, error => { settled = true; return error === reason; });
await turn();
const queued = manager.run('bot', 'chat', ['after drain']);
controller.abort(reason);
await turn();
assert.equal(cancelled, 1);
assert.equal(settled, false);
assert.deepEqual(messages, []);
if (fails) click.reject(Error('late failure'));
else click.resolve();
await rejected;
assert.equal(await queued, true);
assert.deepEqual(clicks, [0]);
assert.deepEqual(messages, ['after drain']);
assert.equal(listeners(controller.signal), 0);
});
}
});
test('global stop keeps its early false result while the queue waits for actual execution settlement', async t => {
const {bot, manager, messages} = await fixture(t);
const click = deferred();
const clicks = [];
bot.clickWindow = slot => { clicks.push(slot); return click.promise; };
const running = manager.run('inventory', 'slot_to_slot', [0, 1]);
await turn();
const controller = new AbortController();
const discarded = manager.run('bot', 'chat', ['discarded'], undefined, {signal: controller.signal});
let before = 0;
assert.equal(await manager.run('bot', 'stop_current_function', [], () => before++), true);
assert.equal(await running, false);
assert.equal(await discarded, false);
assert.equal(before, 1);
assert.equal(listeners(controller.signal), 0);
const fresh = manager.run('bot', 'chat', ['fresh']);
await turn();
assert.deepEqual(messages, []);
click.resolve();
assert.equal(await fresh, true);
assert.deepEqual(clicks, [0]);
assert.deepEqual(messages, ['fresh']);
});
test('dispose rejects queued calls immediately and notifies and drains the current owned call', async t => {
const {bot, manager, messages} = await fixture(t);
const click = deferred();
const clicks = [];
let cancelled = 0;
bot.clickWindow = slot => {
clicks.push(slot);
const unwatch = on_function_cancel(bot, () => cancelled++);
return click.promise.finally(unwatch);
};
const controller = new AbortController();
let settled = false;
const running = manager.run('inventory', 'slot_to_slot', [0, 1], undefined, {signal: controller.signal});
const rejected = assert.rejects(running, error => { settled = true; return /已销毁/.test(error.message); });
await turn();
const queuedController = new AbortController();
const queued = manager.run('bot', 'chat', ['discarded'], undefined, {signal: queuedController.signal});
const queuedRejected = assert.rejects(queued, /已销毁/);
manager.dispose();
manager.dispose();
await queuedRejected;
assert.equal(listeners(queuedController.signal), 0);
await turn();
assert.equal(cancelled, 1);
assert.equal(settled, false);
click.resolve();
await rejected;
assert.deepEqual(clicks, [0]);
assert.deepEqual(messages, []);
assert.equal(listeners(controller.signal), 0);
await assert.rejects(manager.run('bot', 'chat', ['after dispose']), /已销毁/);
});
test('abort or disposal inside before_run prevents execution and preserves callback failures', async t => {
const {manager, messages} = await fixture(t);
const controller = new AbortController();
await assert.rejects(manager.run('bot', 'chat', ['aborted'], () => controller.abort(), {signal: controller.signal}),
{name: 'AbortError'});
assert.equal(listeners(controller.signal), 0);
const failure = Error('callback failed');
await assert.rejects(manager.run('bot', 'chat', ['failed'], () => { throw failure; }), error => error === failure);
assert.equal(await manager.run('bot', 'chat', ['next']), true);
await assert.rejects(manager.run('bot', 'chat', ['disposed'], () => manager.dispose()), /已销毁/);
assert.deepEqual(messages, ['next']);
});
test('successful and failed calls retain booleans and remove signal listeners before later aborts', async t => {
const {bot, manager, messages} = await fixture(t);
const controller = new AbortController();
let before = 0;
assert.equal(await manager.run('bot', 'chat', ['success'], () => before++, {signal: controller.signal}), true);
assert.equal(before, 1);
assert.equal(listeners(controller.signal), 0);
bot.inventory = {slots: [{name: 'stone'}, null], selectedItem: null};
bot.clickWindow = () => Promise.reject(Error('click failed'));
assert.equal(await manager.run('inventory', 'slot_to_slot', [0, 1], undefined, {signal: controller.signal}), false);
assert.equal(listeners(controller.signal), 0);
const next = manager.run('bot', 'jump', []);
await turn();
controller.abort();
assert.equal(bot.controlState.jump, true);
bot.emit('physicsTick');
assert.equal(await next, true);
assert.deepEqual(messages, ['success']);
});
test('per-call abort and disposal cannot cancel another manager running on the same bot', async t => {
const {bot, manager} = await fixture(t);
const other = await create_function_manager({bot_instance: bot, bot_status: 'online'});
t.after(() => other.dispose());
const controller = new AbortController();
const first = manager.run('bot', 'use_direction_key', ['forward', 5000], undefined, {signal: controller.signal});
const rejected = assert.rejects(first, {name: 'AbortError'});
const second = other.run('bot', 'jump', []);
await turn();
controller.abort();
await rejected;
assert.equal(bot.controlState.forward, false);
assert.equal(bot.controlState.jump, true);
const third = manager.run('bot', 'use_direction_key', ['right', 5000]);
const disposed = assert.rejects(third, /已销毁/);
await turn();
manager.dispose();
await disposed;
assert.equal(bot.controlState.right, false);
assert.equal(bot.controlState.jump, true);
bot.emit('physicsTick');
assert.equal(await second, true);
assert.deepEqual(bot.eventNames(), []);
assert.deepEqual(bot._client.eventNames(), []);
});
test('scoped cancellation isolates listeners and generation checks across awaits while global cancel reaches all', async () => {
const bot = new EventEmitter();
const first = create_function_cancellation(bot);
const second = create_function_cancellation(bot);
const otherBot = new EventEmitter();
const counts = {global: 0, first: 0, second: 0, other: 0};
const unwatch = on_function_cancel(bot, () => counts.global++);
const unwatchOther = on_function_cancel(otherBot, () => counts.other++);
const globalGeneration = cancellation_generation(bot);
const gate = deferred();
const watch = (scope, name) => scope.run(async () => {
const generation = cancellation_generation(bot);
await turn();
const off = on_function_cancel(bot, () => counts[name]++);
try {
await gate.promise;
return cancellation_generation(bot) - generation;
} finally { off(); }
});
try {
const firstWatch = watch(first, 'first');
const secondWatch = watch(second, 'second');
await turn();
first.cancel();
assert.deepEqual(counts, {global: 0, first: 1, second: 0, other: 0});
assert.equal(cancellation_generation(bot), globalGeneration);
first.run(() => cancel_functions(bot));
assert.deepEqual(counts, {global: 1, first: 2, second: 1, other: 0});
gate.resolve();
assert.deepEqual(await Promise.all([firstWatch, secondWatch]), [2, 1]);
first.dispose();
second.dispose();
cancel_functions(bot);
assert.deepEqual(counts, {global: 2, first: 2, second: 1, other: 0});
} finally {
gate.resolve();
first.dispose();
second.dispose();
unwatch();
unwatchOther();
}
});