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(); });