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
+713
View File
@@ -0,0 +1,713 @@
import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import {EventEmitter} from 'node:events';
import {createRequire} from 'node:module';
import {fileURLToPath} from 'node:url';
import vm from 'node:vm';
import test from 'node:test';
import {build} from 'esbuild';
const require = createRequire(import.meta.url);
const root = fileURLToPath(new URL('../', import.meta.url));
const sources = {
'./bot_instance_init': 'export const BotContext = globalThis.mocks.BotContext;',
'../workflow/init': 'export const init_workflow = async () => {};',
'../../service/server/update/bot_status.ts': 'export const update_one_bot_status = async () => {};',
};
const compiled = await build({
stdin: {
contents: `export * from './bot/forward/stream.ts'; export * from './bot/instance/index.ts'; export * from './utils/command_completion.ts'; export {init as InventoryInfo} from './bot/info/bot/inventory.ts';`,
resolveDir: root, loader: 'ts',
},
bundle: true, platform: 'node', format: 'cjs', packages: 'external', write: false,
plugins: [{
name: 'mock-stream-lifecycle',
setup(build) {
build.onResolve({filter: /.*/}, args => Object.hasOwn(sources, args.path)
? {path: args.path, namespace: 'mock'} : undefined);
build.onLoad({filter: /.*/, namespace: 'mock'}, args => ({contents: sources[args.path], loader: 'js'}));
},
}],
});
function setup() {
const contexts = [];
const mocks = {
cleanup_error: false,
BotContext: class extends EventEmitter {
constructor(config) {
super();
this.bot_config = {...config, secret: 'SECRET'};
this.bot_status = 'offline';
this.plugin_manager = {async init() {}, async dispose() {}};
this.bot_instance = null;
this.logger = {get_recent_log() { throw new Error('Disk access forbidden'); }};
contexts.push(this);
}
validate_config() { return 0; }
async connect() {
this.bot_status = 'online';
this.sent = [];
this.bot_instance = {chat: message => this.sent.push(message)};
this.emit('status_update', 'online');
return 0;
}
async clear_this_context() {
this.bot_status = 'offline';
this.bot_instance = null;
this.emit('status_update', 'offline');
if (mocks.cleanup_error) throw new Error('SECRET disposal failure');
this.removeAllListeners();
}
},
};
const module = {exports: {}};
vm.runInNewContext(compiled.outputFiles[0].text, {
module, exports: module.exports, require, mocks, console, Buffer, structuredClone, setTimeout, clearTimeout,
});
const api = module.exports;
return {
...api, contexts, mocks,
start: () => api.start_bot_context({}, {
instance_config: {bot_id: 1, user_id: 1}, plugins: [], workflow: [],
}, 1),
};
}
function source() {
let current;
const observers = new Set();
return {
get: () => current,
observe(id, callback) {
observers.add(callback);
callback(current);
return () => observers.delete(callback);
},
set(context) {
current = context;
for (const callback of observers) callback(context);
},
get count() { return observers.size; },
};
}
function context() {
return Object.assign(new EventEmitter(), {
bot_status: 'online', bot_instance: {chat() {}}, secret: 'SECRET',
logger: {get_recent_log() { throw new Error('Disk access forbidden'); }},
});
}
const segment = message => ({message, font_color: '#abcdef', font_style: ['normal']});
const plain = value => JSON.parse(JSON.stringify(value));
const fails = (action, code) => assert.throws(action, error => error.code === code);
test('context snapshot accepts the complete inventory translation tree alongside other information', () => {
const {BotStream, InventoryInfo} = setup();
const instance = Object.assign(new EventEmitter(), {_client: new EventEmitter(), inventory: null, currentWindow: null});
const inventory = new InventoryInfo(instance);
const context_source = source();
const bot = context();
const records = [
{name: 'health', i18n: {lang: 'zh-CN', name: '生命值', description: ''}, value: 20},
{name: inventory.name, i18n: inventory.i18n, value: inventory.get_info()},
];
bot.info_manager = {get_all_info: () => records};
context_source.set(bot);
const stream = new BotStream(1, {source: context_source, now: () => 100});
try {
assert.deepEqual(plain(stream.get_context_info({}, 200)), plain(records));
} finally { stream.dispose(); inventory.dispose(); }
});
test('context info is online-only, cloned and rechecks state after reading', () => {
const {BotStream} = setup();
const context_source = source();
const stream = new BotStream(1, {source: context_source, now: () => 100});
const read = () => stream.get_context_info({}, 200);
fails(read, 'NO_CONTEXT');
const bot = context();
context_source.set(bot);
for (const status of ['offline', 'ready', 'connecting', 'disconnecting', 'reconnecting']) {
bot.bot_status = status;
fails(read, 'BOT_NOT_ONLINE');
}
bot.bot_status = 'online';
fails(read, 'INTERNAL_ERROR');
const records = [{name: 'health', i18n: {lang: 'zh-CN', name: 'Health', description: ''}, value: {health: 20}}];
bot.info_manager = {get_all_info: () => records};
assert.deepEqual(plain(read()), records);
const result = read(); result[0].value.health = 1;
assert.equal(records[0].value.health, 20);
fails(() => stream.get_context_info({extra: true}, 200), 'INVALID_REQUEST');
fails(() => stream.get_context_info({}, 100), 'TIMEOUT');
bot.info_manager.get_all_info = () => {bot.bot_instance = null; return records;};
fails(read, 'CANCELLED');
fails(read, 'BOT_NOT_ONLINE');
stream.dispose();
fails(read, 'CANCELLED');
});
test('context info rejects malformed, cyclic, oversized and non-JSON module results without invoking getters', () => {
const {BotStream} = setup();
const context_source = source(); const bot = context(); context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
const record = {name: 'health', i18n: {lang: 'en', name: 'Health', description: ''}, value: 20};
const cyclic = {}; cyclic.self = cyclic;
let getter_calls = 0;
const getter = Object.defineProperty({}, 'value', {enumerable: true, get() {getter_calls++; return 1;}});
const deep = {}; let leaf = deep;
for (let i = 0; i < 70; i++) leaf = leaf.next = {};
const sparse = []; sparse.length = 2;
for (const value of [undefined, NaN, Infinity, 1n, () => {}, cyclic, getter, new Date(), sparse,
{toJSON() {throw new Error('must not call');}}, 'x'.repeat(4 * 1024 * 1024 + 1), deep]) {
bot.info_manager = {get_all_info: () => [{...record, value}]};
fails(() => stream.get_context_info({}, Date.now() + 1000), 'INTERNAL_ERROR');
}
for (const records of [[{...record, name: 'bad.name'}], [record, record],
[{...record, i18n: {...record.i18n, fields: {x: {lang: 'en', name: 'X'}}}}],
[{...record, i18n: {...record.i18n, values: []}}],
[{...record, i18n: {...record.i18n, extra: true}}], [{...record, secret: true}],
[{...record, i18n: {...record.i18n, name: 'x'.repeat(257)}}], Array(257).fill(record)]) {
bot.info_manager = {get_all_info: () => records};
fails(() => stream.get_context_info({}, Date.now() + 1000), 'INTERNAL_ERROR');
}
assert.equal(getter_calls, 0);
stream.dispose();
});
test('info updates share log ordering, clone values and detach stale contexts', () => {
const {BotStream} = setup();
const context_source = source(); const bot = context(); context_source.set(bot);
bot.info_manager = {get_all_info: () => []};
const stream = new BotStream(1, {source: context_source});
const events = []; stream.subscribe(event => events.push(event));
bot.emit('log', [segment('before')]);
const input = {name: 'health', value: {health: 19}};
bot.emit('info_update', input); input.value.health = 1;
bot.emit('log', [segment('after')]);
assert.deepEqual(events.map(event => event.name), ['log', 'info_update', 'log']);
assert.equal(events[1].payload.value.health, 19);
assert.ok(events[0].seq < events[1].seq && events[1].seq < events[2].seq);
bot.bot_status = 'offline'; bot.emit('info_update', {name: 'health', value: 0});
assert.equal(events.length, 3);
bot.bot_status = 'online';
const stale = bot.listeners('info_update')[0];
context_source.set(context());
const count = events.length;
stale({name: 'health', value: 1});
assert.equal(events.length, count);
assert.equal(bot.listenerCount('info_update'), 0);
stream.dispose();
assert.equal(context_source.get().listenerCount('info_update'), 0);
});
test('invalid info pushes are skipped without interrupting later valid updates', t => {
const diagnostics = [];
t.mock.method(console, 'error', message => diagnostics.push(message));
const {BotStream} = setup();
const context_source = source(); const bot = context(); context_source.set(bot);
bot.info_manager = {get_all_info: () => []};
const stream = new BotStream(1, {source: context_source});
const events = []; stream.subscribe(event => events.push(event));
let called = false;
const accessor = Object.defineProperty({name: 'health'}, 'value', {
enumerable: true, get() {called = true; return 10;},
});
for (const update of [accessor, {name: 'health', value: NaN}, {name: 'health', value: 1, extra: true}])
bot.emit('info_update', update);
assert.equal(called, false);
assert.equal(events.length, 0);
assert.equal(diagnostics.length, 3);
bot.emit('info_update', {name: 'health', value: 12});
assert.equal(events.length, 1);
assert.equal(events[0].payload.value, 12);
stream.dispose();
});
test('deep and large inventory values work for both queries and pushes with bounded diagnostics', t => {
const diagnostics = [];
t.mock.method(console, 'error', (...args) => diagnostics.push(args));
const {BotStream} = setup();
const context_source = source(); const bot = context(); context_source.set(bot);
let deep = {text: 'book page'};
for (let i = 0; i < 24; i++) deep = {value: deep};
const value = {window: {slots: Array.from({length: 90}, () => ({
type: 1, nbt: deep, components: [{type: 'custom_data', data: 'x'.repeat(4096)}],
}))}};
const records = [{name: 'inventory', i18n: {lang: 'en', name: 'Inventory', description: ''}, value}];
bot.info_manager = {get_all_info: () => records};
const stream = new BotStream(1, {source: context_source});
try {
assert.ok(Buffer.byteLength(JSON.stringify(value)) > 196_608);
assert.deepEqual(plain(stream.get_context_info({}, Date.now() + 1000)), records);
const events = []; stream.subscribe(event => events.push(event));
bot.emit('info_update', {name: 'inventory', value});
assert.equal(events.length, 1);
assert.deepEqual(plain(events[0].payload.value), value);
let too_deep = {secret: 'DO_NOT_LOG'};
for (let i = 0; i < 70; i++) too_deep = {next: too_deep};
bot.emit('info_update', {name: 'inventory', value: too_deep});
assert.equal(events.length, 1);
assert.equal(diagnostics[0][1].module, 'inventory');
assert.equal(diagnostics[0][1].reason, 'max_depth');
assert.ok(diagnostics[0][1].path.startsWith('$.value.'));
assert.equal(JSON.stringify(diagnostics).includes('DO_NOT_LOG'), false);
bot.emit('info_update', {name: 'inventory', value: {window: null}});
assert.equal(events.length, 2);
} finally {stream.dispose();}
});
test('observers follow absent, insertion, replacement and removal; unsubscribe releases observer', async () => {
const app = setup();
const seen = [];
const unsubscribe = app.observe_bot_context(1, value => seen.push(value));
assert.deepEqual(seen, [undefined]);
assert.equal(app.get_bot_context(1), undefined);
await app.start();
const first = app.contexts[0];
assert.equal(app.get_bot_context(1), first);
first.bot_status = 'offline';
await app.start();
const second = app.contexts[1];
assert.deepEqual(seen, [undefined, first, undefined, second]);
await first.clear_this_context();
assert.equal(app.get_bot_context(1), second);
assert.equal(seen.length, 4);
await app.stop_bot_context(1);
assert.equal(seen.at(-1), undefined);
unsubscribe();
unsubscribe();
await app.start();
assert.equal(seen.length, 5);
});
test('cleanup failure still removes context and notifies observers; failed observer cannot interrupt startup', async () => {
const app = setup();
const seen = [];
const unsubscribe = app.observe_bot_context(1, value => seen.push(value));
const failing = app.observe_bot_context(1, () => { throw new Error('consumer failure'); });
assert.equal(await app.start(), 464);
app.mocks.cleanup_error = true;
assert.equal(await app.stop_bot_context(1), 466);
assert.equal(app.get_bot_context(1), undefined);
assert.equal(seen.at(-1), undefined);
unsubscribe(); failing();
});
test('default source follows start/stop/restart with stable stream ID and UUID generation per context lifetime', async () => {
const app = setup();
const stream = new app.BotStream(1);
const events = [];
stream.subscribe(event => events.push(event));
const absent = stream.snapshot({});
assert.equal(absent.context_exists, false);
assert.equal(absent.context_generation, null);
assert.equal(absent.bot_status, 'offline');
assert.equal(absent.seq, 0);
await app.start();
const first = stream.snapshot();
assert.match(first.context_generation, /^[0-9a-f-]{36}$/);
app.contexts[0].emit('log', [segment('first')]);
const twin = new app.BotStream(1);
assert.equal(twin.snapshot().context_generation, first.context_generation);
assert.notEqual(twin.stream_id, stream.stream_id);
twin.dispose();
await app.stop_bot_context(1);
const stopped = stream.snapshot();
assert.equal(stopped.context_exists, false);
assert.equal(stopped.context_generation, null);
assert.equal(stopped.bot_status, 'offline');
assert.deepEqual(plain(events.at(-1).payload), {status: 'offline', context_exists: false});
await app.start();
const restarted = stream.snapshot();
assert.notEqual(restarted.context_generation, first.context_generation);
assert.equal(restarted.stream_id, absent.stream_id);
assert.ok(restarted.seq > first.seq);
assert.equal(stream.logs({limit: 200}).records[0].context_generation, first.context_generation);
for (let i = 1; i < events.length; i++) assert.ok(events[i].seq > events[i - 1].seq);
stream.dispose();
await app.stop_bot_context(1);
});
test('bounded replay has shared sequence watermarks, gap detection, limit paging and stream reset detection', () => {
const {BotStream} = setup();
const context_source = source();
const bot = context();
context_source.set(bot);
const stream = new BotStream(1, {source: context_source, capacity: 3, now: () => 123});
const start = stream.snapshot();
const events = [];
stream.subscribe(event => events.push(event));
for (let i = 0; i < 5; i++) bot.emit('log', [segment(String(i))]);
bot.emit('message', 'hello', 'chat');
const snapshot = stream.snapshot();
assert.equal(snapshot.seq, events.at(-1).seq);
const latest = stream.logs({limit: 2});
assert.equal(latest.seq, snapshot.seq);
assert.deepEqual(latest.records.map(record => record.segments[0].message), ['3', '4']);
assert.equal(latest.truncated, true);
const missing = stream.logs({limit: 2, stream_id: stream.stream_id, after_seq: start.seq});
assert.deepEqual(missing.records.map(record => record.segments[0].message), ['2', '3']);
assert.equal(missing.truncated, true);
const last = stream.logs({limit: 2, stream_id: stream.stream_id, after_seq: missing.records.at(-1).seq});
assert.deepEqual(last.records.map(record => record.segments[0].message), ['4']);
assert.equal(last.truncated, false);
assert.equal(stream.logs({limit: 2, stream_id: randomUUID(), after_seq: 0}).truncated, true);
fails(() => stream.logs({limit: 1, stream_id: stream.stream_id, after_seq: snapshot.seq + 1}), 'RESYNC_REQUIRED');
stream.dispose();
});
test('resync emits a fresh status watermark for absent and current contexts without resetting stream', () => {
const {BotStream} = setup();
const context_source = source();
const stream = new BotStream(1, {source: context_source});
const events = [];
stream.subscribe(event => events.push(event));
stream.resync();
assert.deepEqual(plain(events[0].payload), {status: 'offline', context_exists: false});
const bot = context();
context_source.set(bot);
const current = stream.snapshot();
stream.resync();
assert.equal(events.at(-1).stream_id, current.stream_id);
assert.equal(events.at(-1).context_generation, current.context_generation);
assert.ok(events.at(-1).seq > current.seq);
assert.equal(events.at(-1).seq, stream.snapshot().seq);
stream.dispose();
fails(() => stream.resync(), 'CANCELLED');
});
test('safe projection never serializes raw objects, message JSON, accessors, error details or extra fields', () => {
const {BotStream} = setup();
const context_source = source();
const bot = context();
context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
const events = [];
stream.subscribe(event => events.push(event));
let conversions = 0;
const secret = {secret: 'SECRET', toString() { conversions++; throw new Error('Do not stringify'); }};
const accessor = {get message() { conversions++; throw new Error('Do not invoke'); }};
bot.emit('log', [segment(secret), accessor]);
assert.equal(events.length, 0);
bot.emit('log', [{...segment('safe'), secret, font_color: 'url(SECRET)', font_style: ['bold', 'unsafe']}]);
bot.emit('message', {json: {text: 'hello', extra: [{text: ' world'}], secret}, toString: secret.toString}, 'chat');
const error = new Error('SECRET stack/password');
error.code = 'SECRET';
bot.emit('error', error);
assert.equal(conversions, 0);
assert.deepEqual(events.map(event => event.name), ['log', 'message', 'bot_error']);
assert.deepEqual(plain(events[0].payload.records[0].segments), [{
message: 'safe', font_color: '#ffffff', font_style: ['bold'],
}]);
assert.deepEqual(plain(events[1].payload), {text: 'hello world', position: 'chat'});
assert.deepEqual(plain(events[2].payload), {message: 'Bot connection error'});
assert.equal(JSON.stringify({events, snapshot: stream.snapshot(), logs: stream.logs({limit: 200})}).includes('SECRET'), false);
assert.deepEqual(Object.keys(events[0]).sort(), ['bot_id', 'context_generation', 'emitted_at', 'name', 'payload', 'seq', 'stream_id']);
stream.dispose();
});
test('projection and buffer sizes stay bounded even for huge Unicode messages and log arrays', () => {
const {BotStream} = setup();
const context_source = source();
const bot = context();
context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
const input = Array.from({length: 500}, () => ({
...segment('😀'.repeat(10_000)), font_style: ['normal', 'italic', 'bold', 'light', 'underline', 'url'],
}));
for (let i = 0; i < 250; i++) bot.emit('log', input);
const logs = stream.logs({limit: 200});
assert.ok(logs.records.length > 0 && logs.records.length < 200);
assert.equal(logs.truncated, true);
assert.ok(Buffer.byteLength(JSON.stringify(logs)) < 262_144);
assert.ok(logs.records.every(record => record.segments.every(item => Buffer.byteLength(item.message) <= 16_000)));
const next = stream.logs({limit: 200, stream_id: stream.snapshot().stream_id, after_seq: 0});
const page = stream.logs({limit: 200, stream_id: next.stream_id, after_seq: next.records.at(-1).seq});
assert.ok(page.records[0].seq > next.records.at(-1).seq);
stream.dispose();
});
test('forwarded logs preserve every colored character and the string prefix', () => {
const {BotStream} = setup();
const context_source = source();
const bot = context();
context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
const input = [{...segment('chat: '), hoverEvent: {action: 'show_text', value: 'tooltip', segments: [{message: 'tooltip', font_color: '#55ffff', font_style: ['bold']}]},
clickEvent: {action: 'suggest_command', value: '/help'}, insertion: 'chat'}, ...Array.from('完整的多色聊天消息 ABC 123 😀', (message, index) => ({
...segment(message), font_color: index % 2 ? '#ff0000' : '#00ff00',
}))];
let live;
stream.subscribe(event => { if (event.name === 'log') live = event; });
bot.emit('log', input);
const expected = input.map(item => item.message).join('');
const segments = live.payload.records[0].segments;
assert.equal(segments.length, input.length);
assert.equal(segments.map(item => item.message).join(''), expected);
assert.deepEqual(plain(segments[0]), input[0]);
assert.deepEqual(plain(stream.logs({limit: 1}).records[0].segments), plain(segments));
stream.dispose();
});
test('disk history works without a context and does not change live stream cursors', async () => {
const {BotStream} = setup();
const context_source = source();
const entries = Array.from({length: 25}, (_, index) => ({id: randomUUID(), timestamp: index,
log: [{...segment(`disk ${index}`), hoverEvent: {action: 'show_text', value: 'tooltip'}}]}));
let requested;
const stream = new BotStream(1, {source: context_source, history_reader: async count => {
requested = count;
return entries.slice(-count);
}});
const before = stream.snapshot();
const result = await stream.get_log_history();
assert.equal(requested, 20);
assert.equal(result.records.length, 20);
assert.deepEqual(plain(result.records.map(record => record.id)), entries.slice(-20).map(entry => entry.id));
assert.equal(result.records[0].segments[0].hoverEvent.value, 'tooltip');
assert.notEqual(result.stream_id, before.stream_id);
assert.equal(stream.snapshot().seq, before.seq);
assert.equal(stream.logs({limit: 20}).records.length, 0);
stream.dispose();
});
test('subscriber mutations and exceptions cannot corrupt other consumers or buffered records', () => {
const {BotStream} = setup();
const context_source = source();
const bot = context();
context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
stream.subscribe(event => {
if (event.name === 'log') event.payload.records[0].segments[0].message = 'changed';
throw new Error('consumer failure');
});
let received;
stream.subscribe(event => { received = event; });
const input = [segment('original')];
bot.emit('log', input);
input[0].message = 'changed';
assert.equal(received.payload.records[0].segments[0].message, 'original');
const result = stream.logs({limit: 1});
result.records[0].segments[0].message = 'changed';
assert.equal(stream.logs({limit: 1}).records[0].segments[0].message, 'original');
stream.dispose();
});
test('reentrant subscribers preserve event sequence order for every consumer', () => {
const {BotStream} = setup();
const context_source = source();
const bot = context();
context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
const seen = [];
stream.subscribe(event => {
if (event.name === 'log') stream.resync();
});
stream.subscribe(event => seen.push(event.seq));
bot.emit('log', [segment('nested')]);
assert.equal(seen.length, 2);
assert.ok(seen[1] > seen[0]);
stream.dispose();
});
test('replacement detaches listeners, rejects stale callbacks; disposal releases source and context subscriptions', () => {
const {BotStream} = setup();
const context_source = source();
const old = context();
context_source.set(old);
const stream = new BotStream(1, {source: context_source});
const events = [];
const unsubscribe = stream.subscribe(event => events.push(event));
const old_log = old.listeners('log')[0];
const replacement = context();
context_source.set(replacement);
assert.equal(old.listenerCount('log'), 0);
const count = events.length;
old_log([segment('stale')]);
assert.equal(events.length, count);
replacement.emit('log', [segment('current')]);
assert.equal(events.length, count + 1);
unsubscribe();
replacement.emit('log', [segment('unsubscribed')]);
assert.equal(events.length, count + 1);
assert.equal(context_source.count, 1);
stream.dispose(); stream.dispose();
assert.equal(context_source.count, 0);
for (const event of ['status_update', 'log', 'message', 'error']) assert.equal(replacement.listenerCount(event), 0);
fails(() => stream.snapshot(), 'CANCELLED');
fails(() => stream.subscribe(() => {}), 'CANCELLED');
});
test('tab completion normalizes mixed matches, deduplicates and cancels stale results', async () => {
const {BotStream} = setup();
const context_source = source();
const stream = new BotStream(1, {source: context_source});
const reject = (promise, code) => assert.rejects(promise, error => error.code === code);
await reject(stream.tab_complete({text: '/he'}, Date.now() + 1000), 'NO_CONTEXT');
const bot = context(); bot.bot_status = 'online';
let release;
bot.bot_instance.tabComplete = () => new Promise(resolve => { release = resolve; });
context_source.set(bot);
const pending = stream.tab_complete({text: '/he'}, Date.now() + 1000);
await reject(stream.tab_complete({text: '/he'}, Date.now() + 1000), 'RATE_LIMITED');
release(['help', {match: 'help'}, {match: 'hello'}, 'hello', '', {match: 42}]);
assert.deepEqual(plain(await pending), {matches: ['help', 'hello'], start: 1, length: 2});
bot.bot_instance.tabComplete = async () => ['Alex', 'Alex'];
assert.deepEqual(plain(await stream.tab_complete({text: '/tell Al'}, Date.now() + 1000)),
{matches: ['Alex'], start: 6, length: 2});
assert.deepEqual(plain(await stream.tab_complete({text: '/tell '}, Date.now() + 1000)),
{matches: ['Alex'], start: 6, length: 0});
bot.bot_instance.tabComplete = async () => ['/help', 'help'];
assert.deepEqual(plain(await stream.tab_complete({text: '/he'}, Date.now() + 1000)),
{matches: ['help'], start: 1, length: 2});
bot.bot_instance.tabComplete = () => new Promise(resolve => { release = resolve; });
for (const payload of [{}, {text: 2}, {text: 'a\n'}, {text: 'x'.repeat(257)}])
await reject(stream.tab_complete(payload, Date.now() + 1000), 'INVALID_REQUEST');
const stale = stream.tab_complete({text: ''}, Date.now() + 1000);
context_source.set(undefined); release(['stale']);
await reject(stale, 'CANCELLED');
stream.dispose();
});
test('modern completion matches transaction IDs and cleans listeners on timeout', async () => {
const {BotStream} = setup();
const context_source = source();
const bot = context(); bot.bot_status = 'online';
const client = new EventEmitter(); client.protocolVersion = 393;
let sent;
client.write = (name, packet) => { sent = packet; assert.equal(name, 'tab_complete'); };
bot.bot_instance._client = client;
bot.bot_instance.tabComplete = () => { throw new Error('Legacy API must not be used'); };
context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
const pending = stream.tab_complete({text: '/he'}, Date.now() + 1000);
client.emit('tab_complete', {transactionId: sent.transactionId + 1, matches: ['wrong']});
client.emit('tab_complete', {transactionId: sent.transactionId, start: 1, length: 2, matches: [{match: 'help'}, {match: 'help'}]});
assert.deepEqual(plain(await pending), {matches: ['help'], start: 1, length: 2});
const parameter = stream.tab_complete({text: '/tell Alex he'}, Date.now() + 1000);
client.emit('tab_complete', {transactionId: sent.transactionId, start: 11, length: 2, matches: [{match: 'hello'}]});
assert.deepEqual(plain(await parameter), {matches: ['hello'], start: 11, length: 2});
const malformed = stream.tab_complete({text: '/tell '}, Date.now() + 1000);
client.emit('tab_complete', {transactionId: sent.transactionId, start: 6, length: 10, matches: []});
await assert.rejects(malformed, error => error.code === 'INTERNAL_ERROR');
assert.equal(client.listenerCount('tab_complete'), 0);
await assert.rejects(stream.tab_complete({text: '/he'}, Date.now() + 15), error => error.code === 'TIMEOUT');
assert.equal(client.listenerCount('tab_complete'), 0);
assert.equal(client.listenerCount('end'), 0);
stream.dispose();
});
test('declared commands supply missing literals, subcommands and player arguments', async () => {
const {BotStream, remember_command_tree} = setup();
const context_source = source();
const bot = context(); bot.bot_status = 'online';
const client = new EventEmitter(); client.protocolVersion = 393;
client.write = (_name, packet) => queueMicrotask(() => client.emit('tab_complete', {
transactionId: packet.transactionId, start: 0, length: 0, matches: [],
}));
bot.bot_instance._client = client;
bot.bot_instance.tabComplete = async () => [];
bot.bot_instance.players = {Alex: {}, Alice: {}, Bob: {}};
const literal = (name, children = []) => ({flags: {command_node_type: 1}, extraNodeData: {name}, children});
remember_command_tree(bot.bot_instance, {rootIndex: 0, nodes: [
{children: [1, 3]}, literal('tell', [2]),
{flags: {command_node_type: 2}, extraNodeData: {name: 'player', parser: 'minecraft:entity'}, children: []},
literal('team', [4]), literal('join', [5]), literal('red'),
]});
context_source.set(bot);
const stream = new BotStream(1, {source: context_source});
assert.deepEqual(plain(await stream.tab_complete({text: '/te'}, Date.now() + 1000)),
{matches: ['tell', 'team'], start: 1, length: 2});
assert.deepEqual(plain(await stream.tab_complete({text: '/tell Al'}, Date.now() + 1000)),
{matches: ['Alex', 'Alice'], start: 6, length: 2});
assert.deepEqual(plain(await stream.tab_complete({text: '/team join r'}, Date.now() + 1000)),
{matches: ['red'], start: 11, length: 1});
client.write = () => {};
assert.deepEqual(plain(await stream.tab_complete({text: '/tell Al'}, Date.now() + 1000)),
{matches: ['Alex', 'Alice'], start: 6, length: 2});
stream.dispose();
});
test('26.3 codec preserves actual command suggestion request and response fields', () => {
const {createSerializer, createDeserializer} = require('@wp2508/minecraft-protocol/src/transforms/serializer');
for (const isServer of [false, true]) {
const packet = {name: 'tab_complete', params: isServer
? {transactionId: 7, start: 6, length: 2, matches: [{match: 'Alex'}]}
: {transactionId: 7, text: '/tell Al'}};
const serializer = createSerializer({state: 'play', isServer, version: '26.3'});
const decoder = createDeserializer({state: 'play', isServer: !isServer, version: '26.3'});
assert.deepEqual(JSON.parse(JSON.stringify(decoder.parsePacketBuffer(serializer.createPacketBuffer(packet)).data)), packet);
}
});
test('function RPC returns booleans and prevents expired queued work', async () => {
const {BotStream} = setup();
const context_source = source();
let now = 100;
const stream = new BotStream(1, {source: context_source, now: () => now});
const bot = context();
context_source.set(bot);
const payload = {function: 'inventory', name: 'left_click_slot', args: [9]};
for (const result of [true, false, undefined]) {
bot.function_manager = {async run(path, name, args, before_run) {
assert.equal(path, payload.function);
assert.equal(name, payload.name);
assert.deepEqual(args, payload.args);
before_run();
return result;
}};
assert.equal(await stream.execute_function(payload, 200), result === true);
}
bot.function_manager = {async run() { throw new Error('SECRET'); }};
assert.equal(await stream.execute_function(payload, 200), false);
bot.bot_status = 'offline';
assert.equal(await stream.execute_function(payload, 200), false);
bot.bot_status = 'online';
bot.function_manager = {async run(path, name, args, before_run) { now = 200; before_run(); }};
await assert.rejects(stream.execute_function(payload, 200), error => error.code === 'TIMEOUT');
now = 100;
bot.function_manager = {async run(path, name, args, before_run) {
context_source.set(context());
before_run();
}};
await assert.rejects(stream.execute_function(payload, 200), error => error.code === 'CANCELLED');
stream.dispose();
});
test('strict RPC inputs and chat eligibility/deadlines prevent side effects and sanitize failures', () => {
const {BotStream} = setup();
const context_source = source();
let now = 100;
const stream = new BotStream(1, {source: context_source, now: () => now});
fails(() => stream.chat({message: 'hello'}, 200), 'NO_CONTEXT');
const bot = context();
const sent = [];
bot.bot_instance.chat = message => sent.push(message);
context_source.set(bot);
for (const payload of [null, [], {}, {message: ''}, {message: ' '}, {message: 'x'.repeat(257)},
{message: 'hello', extra: 'SECRET'}, {get message() { throw new Error('getter'); }}]) {
fails(() => stream.chat(payload, 200), 'INVALID_REQUEST');
}
for (const deadline of [100, 99, NaN, Infinity]) fails(() => stream.chat({message: 'hello'}, deadline), 'TIMEOUT');
for (const status of ['ready', 'connecting', 'reconnecting', 'disconnecting', 'offline']) {
bot.bot_status = status;
fails(() => stream.chat({message: 'hello'}, 200), 'BOT_NOT_ONLINE');
}
assert.deepEqual(sent, []);
bot.bot_status = 'online';
assert.deepEqual(plain(stream.chat({message: 'hello'}, 200)), {accepted: true});
assert.deepEqual(sent, ['hello']);
bot.bot_instance.chat = () => { throw new Error('SECRET chat failed'); };
assert.throws(() => stream.chat({message: 'hello'}, 200), error => error.code === 'INTERNAL_ERROR' && !error.message.includes('SECRET'));
for (const payload of [null, [], {extra: true}]) fails(() => stream.snapshot(payload), 'INVALID_REQUEST');
for (const payload of [null, [], {}, {limit: 0}, {limit: 201}, {limit: 1.5}, {limit: 1, after_seq: 0},
{limit: 1, stream_id: 'bad'}, {limit: 1, stream_id: stream.stream_id, after_seq: -1}, {limit: 1, extra: true}]) {
fails(() => stream.logs(payload), 'INVALID_REQUEST');
}
now = 300;
fails(() => stream.chat({message: 'hello'}, 200), 'TIMEOUT');
stream.dispose();
});