Collect chunk voxel data from the bot world and stream snapshot, chunk, and unwatch messages through the forward protocol.
439 lines
23 KiB
JavaScript
439 lines
23 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import {EventEmitter} from 'node:events';
|
|
import {createRequire} from 'node:module';
|
|
import {randomUUID} from 'node:crypto';
|
|
import {fileURLToPath} from 'node:url';
|
|
import {readFile} from 'node:fs/promises';
|
|
import vm from 'node:vm';
|
|
import test from 'node:test';
|
|
import {build, transformSync} from 'esbuild';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
const built = await build({
|
|
stdin: {contents: `export * from './bot/forward/manager.ts';
|
|
export {handler} from './event_handler/bot/forward_bot.ts';`, resolveDir: root, loader: 'ts'},
|
|
bundle: true, platform: 'node', format: 'cjs',
|
|
packages: 'external', write: false,
|
|
plugins: [{name: 'stream-stub', setup(build) {
|
|
build.onResolve({filter: /^\.\/stream\.ts$/}, () => ({path: 'stream', namespace: 'mock'}));
|
|
build.onLoad({filter: /.*/, namespace: 'mock'}, () => ({contents: 'export class BotStream {}', loader: 'js'}));
|
|
}}],
|
|
});
|
|
const module = {exports: {}};
|
|
vm.runInNewContext(built.outputFiles[0].text, {module, exports: module.exports, require,
|
|
Buffer, URL, setTimeout, clearTimeout, console});
|
|
const {NodeForwardManager} = module.exports;
|
|
|
|
function command(overrides = {}) {
|
|
return {forward_id: 0, forward_session_id: randomUUID(), node_session_id: 'node-session', node_id: 1, bot_id: 2,
|
|
url: 'http://127.0.0.1:1234', path: '/api/forward_server', namespace: '/', token: randomUUID(),
|
|
expires_at: Date.now() + 30_000, deadline_at: Date.now() + 500, ...overrides};
|
|
}
|
|
|
|
function setup() {
|
|
const sockets = [];
|
|
const streams = [];
|
|
const files = [];
|
|
const hooks = new Map();
|
|
const initial = command();
|
|
let active = initial;
|
|
const manager = new NodeForwardManager(1, 'node-session', {
|
|
socket_factory(url, options) {
|
|
const socket = new EventEmitter();
|
|
socket.sent = [];
|
|
socket.url = url;
|
|
socket.options = options;
|
|
socket.auth = options.auth;
|
|
socket.connected = false;
|
|
socket.volatile = {emit: (...args) => socket.sent.push(args)};
|
|
socket.io = {engine: {writeBuffer: []}};
|
|
socket.connect = () => {
|
|
socket.connected = true;
|
|
queueMicrotask(() => socket.emit('connect'));
|
|
};
|
|
socket.disconnect = () => {
|
|
socket.connected = false;
|
|
socket.emit('disconnect');
|
|
};
|
|
socket.timeout = () => ({emit(event, payload, ack) {
|
|
socket.sent.push([event, payload]);
|
|
const respond = data => ack(null, {status: 'success', data});
|
|
if (hooks.has(event)) return hooks.get(event)({socket, payload, respond, ack});
|
|
if (event === 'forward_node_ready') return queueMicrotask(() => respond({node_ready: true}));
|
|
const {forward_id, forward_session_id, node_session_id, node_id, bot_id, expires_at} = active;
|
|
queueMicrotask(() => respond({forward_id, forward_session_id, node_session_id, node_id, bot_id,
|
|
role: 'node', node_ready: event === 'reauth', expires_at}));
|
|
}});
|
|
sockets.push(socket);
|
|
return socket;
|
|
},
|
|
stream_factory(bot_id) {
|
|
const listeners = new Set();
|
|
const stream = {bot_id, disposed: false, chats: 0,
|
|
snapshot: () => ({bot_id}), logs: () => ({records: []}),
|
|
get_context_info: () => [{name: 'health', i18n: {lang: 'en', name: 'Health', description: ''}, value: 20}],
|
|
chat: () => { stream.chats++; return {accepted: true}; },
|
|
subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener); },
|
|
resync() {}, dispose() { stream.disposed = true; },
|
|
event: event => { for (const listener of listeners) listener(event); },
|
|
get listeners() { return listeners.size; },
|
|
};
|
|
streams.push(stream);
|
|
return stream;
|
|
},
|
|
file_factory(bot_id) {
|
|
const service = {bot_id, disposed: false,
|
|
list: async () => ({listing_id: randomUUID(), expires_at: Date.now() + 300_000, partial: false, children: []}),
|
|
read: async ({id, offset = 0}) => ({id, name: 'empty', encoding: 'base64', offset,
|
|
next_offset: offset, total_bytes: offset, eof: true, data: ''}),
|
|
dispose() { service.disposed = true; },
|
|
};
|
|
files.push(service);
|
|
return service;
|
|
},
|
|
});
|
|
return {manager, sockets, streams, files, hooks, command: active,
|
|
ensure(value = initial) { active = value; return manager.ensure(value); }};
|
|
}
|
|
|
|
test('lifecycle updates use reliable bot_event even while volatile logs are saturated', async t => {
|
|
const f = setup(); t.after(() => f.manager.stop());
|
|
await f.ensure();
|
|
const socket = f.sockets[0], stream = f.streams[0], received = [];
|
|
socket.on('bot_event', event => received.push(event));
|
|
socket.volatile = {emit() {}};
|
|
const base = {bot_id: 2, context_generation: randomUUID(), stream_id: randomUUID(), emitted_at: Date.now()};
|
|
for (let seq = 1; seq <= 160; seq++) stream.event({...base, seq, name: 'log', payload: {records: []}});
|
|
for (const [index, status] of ['online', 'disconnecting', 'offline'].entries()) {
|
|
stream.event({...base, seq: 161 + index, name: 'status_update', payload: {status, context_exists: true}});
|
|
}
|
|
assert.deepEqual(received.map(event => event.payload.status), ['online', 'disconnecting', 'offline']);
|
|
assert.ok(received.every(event => event.forward_session_id === f.command.forward_session_id));
|
|
stream.event({...base, seq: 165, name: 'message', payload: {text: 'not a lifecycle event'}});
|
|
assert.equal(received.length, 3);
|
|
socket.io.engine.writeBuffer.length = 64;
|
|
stream.event({...base, seq: 166, name: 'status_update', payload: {status: 'offline', context_exists: true}});
|
|
assert.equal(socket.connected, false);
|
|
assert.equal(stream.listeners, 0);
|
|
})
|
|
|
|
const tick = () => new Promise(resolve => setImmediate(resolve));
|
|
const code = response => response.data.code;
|
|
|
|
test('file RPCs work without a bot, clamp ID expiry and invalidate services on transport replacement', async t => {
|
|
const app = setup(); t.after(() => app.manager.stop());
|
|
await app.ensure();
|
|
const rpc = (socket, method, payload = {}, deadline_at = Date.now() + 1000) => new Promise(resolve =>
|
|
socket.emit('relay_request', {request_id: randomUUID(), correlation_id: randomUUID(), method, payload,
|
|
forward_session_id: app.command.forward_session_id, deadline_at}, resolve));
|
|
const first = app.sockets[0];
|
|
const listed = await rpc(first, 'get_data_file_list');
|
|
assert.equal(listed.status, 'success');
|
|
assert.equal(listed.data.expires_at, app.command.expires_at);
|
|
assert.equal(app.files.length, 1);
|
|
assert.equal(app.files[0].bot_id, app.command.bot_id);
|
|
const id = randomUUID();
|
|
assert.equal((await rpc(first, 'get_file_data', {id})).data.id, id);
|
|
assert.equal(code(await rpc(first, 'get_file_data', {id, length: 262145})), 'INVALID_REQUEST');
|
|
assert.equal(code(await rpc(first, 'get_file_data', {id, offset: -1})), 'INVALID_REQUEST');
|
|
let release;
|
|
app.files[0].read = () => new Promise(resolve => {release = resolve;});
|
|
const pending = rpc(first, 'get_file_data', {id});
|
|
first.disconnect();
|
|
assert.equal(app.files[0].disposed, true);
|
|
release({id});
|
|
assert.equal(code(await pending), 'SESSION_STALE');
|
|
assert.equal((await app.ensure({...app.command, token: randomUUID()})).status, 'success');
|
|
assert.equal((await rpc(app.sockets[1], 'get_data_file_list')).status, 'success');
|
|
assert.equal(app.files.length, 2);
|
|
let late;
|
|
app.files[1].list = () => new Promise(resolve => {late = resolve;});
|
|
const timed = rpc(app.sockets[1], 'get_data_file_list', {}, Date.now() + 10);
|
|
await new Promise(resolve => setTimeout(resolve, 15));
|
|
late({listing_id: randomUUID(), expires_at: Date.now() + 1000, partial: false, children: []});
|
|
assert.equal(code(await timed), 'TIMEOUT');
|
|
app.manager.stop();
|
|
assert.equal(app.files[1].disposed, true);
|
|
});
|
|
|
|
test('ready ACK waits for ready confirmation, with one RPC handler and log observer across renewals', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
let release;
|
|
app.hooks.set('forward_node_ready', ({respond}) => { release = respond; });
|
|
let completed = false;
|
|
const result = app.ensure().then(value => { completed = true; return value; });
|
|
await tick();
|
|
assert.equal(completed, false);
|
|
assert.equal(app.sockets[0].listenerCount('relay_request'), 1);
|
|
assert.equal(app.streams[0].listeners, 1);
|
|
release({node_ready: true});
|
|
assert.equal((await result).status, 'success');
|
|
app.hooks.delete('forward_node_ready');
|
|
const next = {...app.command, token: randomUUID()};
|
|
assert.equal((await app.ensure(next)).status, 'success');
|
|
assert.equal(app.sockets.length, 1);
|
|
assert.equal(app.streams[0].listeners, 1);
|
|
assert.equal(app.sockets[0].sent.filter(([event]) => event === 'reauth').length, 1);
|
|
});
|
|
|
|
test('absolute deadline cancels stalled readiness, closes channel and ignores late ready ACK', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
let release;
|
|
app.hooks.set('forward_node_ready', ({respond}) => { release = respond; });
|
|
const request = {...app.command, deadline_at: Date.now() + 35};
|
|
const response = await app.ensure(request);
|
|
assert.equal(code(response), 'TIMEOUT');
|
|
assert.equal(app.sockets[0].connected, false);
|
|
assert.equal(app.sockets[0].listenerCount('relay_request'), 0);
|
|
assert.equal(app.streams[0].disposed, true);
|
|
release({node_ready: true});
|
|
await tick();
|
|
assert.equal(app.sockets[0].connected, false);
|
|
});
|
|
|
|
test('session scope and whitelist validation reject stale node, wrong bot scope, and extra fields', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
assert.equal(code(await app.ensure({...app.command, node_session_id: 'old'})), 'SESSION_STALE');
|
|
assert.equal(code(await app.ensure({...app.command, node_id: 3})), 'FORBIDDEN');
|
|
assert.equal(code(await app.ensure({...app.command, extra: true})), 'INVALID_REQUEST');
|
|
assert.equal((await app.ensure()).status, 'success');
|
|
assert.equal(code(await app.ensure({...app.command, bot_id: 3})), 'SESSION_STALE');
|
|
assert.equal(app.sockets.length, 1);
|
|
});
|
|
|
|
test('serial ensure ignores expired queued work without closing a healthy current operation', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
let release;
|
|
app.hooks.set('forward_node_ready', ({respond}) => { release = respond; });
|
|
const first = app.ensure();
|
|
await tick();
|
|
const second = app.ensure({...app.command, token: randomUUID(), deadline_at: Date.now() + 20});
|
|
assert.equal(code(await second), 'TIMEOUT');
|
|
assert.equal(app.sockets[0].connected, true);
|
|
release({node_ready: true});
|
|
assert.equal((await first).status, 'success');
|
|
await tick();
|
|
assert.equal(app.sockets.length, 1);
|
|
assert.equal(app.sockets[0].connected, true);
|
|
});
|
|
|
|
test('closed session cannot revive, old callbacks cannot execute chat, and manager stop removes observers', async () => {
|
|
const app = setup();
|
|
assert.equal((await app.ensure()).status, 'success');
|
|
const socket = app.sockets[0];
|
|
const stale_rpc = socket.listeners('relay_request')[0];
|
|
socket.emit('forward_session_closed', {forward_session_id: app.command.forward_session_id,
|
|
reason: 'revoked', code: 'CANCELLED'});
|
|
assert.equal(app.streams[0].disposed, true);
|
|
assert.equal(socket.connected, false);
|
|
assert.equal(code(await app.ensure({...app.command, token: randomUUID()})), 'SESSION_STALE');
|
|
let response;
|
|
stale_rpc({request_id: randomUUID(), correlation_id: randomUUID(), method: 'bot.chat', payload: {message: 'hello'},
|
|
forward_session_id: app.command.forward_session_id, deadline_at: Date.now() + 1000}, value => { response = value; });
|
|
assert.equal(code(response), 'SESSION_STALE');
|
|
assert.equal(app.streams[0].chats, 0);
|
|
app.manager.stop();
|
|
assert.equal(code(await app.ensure(command())), 'SESSION_STALE');
|
|
});
|
|
|
|
test('RPC checks deadlines and deduplicates correlation IDs, while allowing grant request ID reuse', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
await app.ensure();
|
|
const base = {request_id: randomUUID(), correlation_id: randomUUID(), method: 'bot.chat', payload: {message: 'hello'},
|
|
forward_session_id: app.command.forward_session_id, deadline_at: Date.now() + 1000};
|
|
const rpc = request => new Promise(resolve => app.sockets[0].emit('relay_request', request, resolve));
|
|
assert.equal((await rpc(base)).status, 'success');
|
|
assert.equal(code(await rpc(base)), 'CANCELLED');
|
|
assert.equal((await rpc({...base, correlation_id: randomUUID()})).status, 'success');
|
|
assert.equal(app.streams[0].chats, 2);
|
|
assert.equal(code(await rpc({...base, correlation_id: randomUUID(), deadline_at: Date.now() - 1})), 'TIMEOUT');
|
|
assert.equal(app.streams[0].chats, 2);
|
|
assert.equal(code(await rpc({...base, method: 'bot.subscribe', payload: {events: []}})), 'INVALID_REQUEST');
|
|
const info = await rpc({...base, correlation_id: randomUUID(), method: 'get_context_info', payload: {}});
|
|
assert.equal(info.status, 'success');
|
|
assert.equal(info.data[0].value, 20);
|
|
});
|
|
|
|
test('transport recovery requires a fresh token; ordinary disconnect never reconnects automatically', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
await app.ensure();
|
|
app.sockets[0].disconnect();
|
|
assert.equal(app.sockets[0].options.reconnection, false);
|
|
assert.equal(code(await app.ensure(app.command)), 'TOKEN_EXPIRED');
|
|
assert.equal(app.sockets.length, 1);
|
|
assert.equal((await app.ensure({...app.command, token: randomUUID()})).status, 'success');
|
|
assert.equal(app.sockets.length, 2);
|
|
assert.equal(app.streams.length, 1);
|
|
assert.equal(app.streams[0].listeners, 1);
|
|
});
|
|
|
|
test('wrong-scope join response closes the channel and sanitized ACK never exposes raw relay details', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
app.hooks.set('join_room', ({respond}) => respond({node_id: 999, token: 'secret', stack: 'private'}));
|
|
const response = await app.ensure();
|
|
assert.equal(code(response), 'SESSION_STALE');
|
|
assert.equal(app.sockets[0].connected, false);
|
|
assert.doesNotMatch(JSON.stringify(response), /secret|private|stack/);
|
|
});
|
|
|
|
test('many historical closed sessions do not consume the active-session budget', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
for (let index = 0; index < 505; index++) {
|
|
const request = command({deadline_at: Date.now() + 2000});
|
|
assert.equal((await app.ensure(request)).status, 'success');
|
|
app.sockets.at(-1).emit('forward_session_closed', {forward_session_id: request.forward_session_id,
|
|
reason: 'revoked', code: 'CANCELLED'});
|
|
}
|
|
assert.equal(app.streams.filter(stream => !stream.disposed).length, 0);
|
|
assert.equal((await app.ensure(command())).status, 'success');
|
|
});
|
|
|
|
test('lease expiry retires data channel and queue capacity stays bounded under stalled work', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
const request = {...app.command, expires_at: Date.now() + 40};
|
|
assert.equal((await app.ensure(request)).status, 'success');
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
assert.equal(app.sockets[0].connected, false);
|
|
assert.equal(app.streams[0].disposed, true);
|
|
assert.equal(code(await app.ensure({...request, expires_at: Date.now() + 1000, token: randomUUID()})), 'SESSION_STALE');
|
|
|
|
const stalled = setup();
|
|
t.after(() => stalled.manager.stop());
|
|
stalled.hooks.set('forward_node_ready', () => {});
|
|
const first = stalled.ensure();
|
|
await tick();
|
|
const queued = Array.from({length: 15}, () => stalled.ensure({...stalled.command, token: randomUUID()}));
|
|
assert.equal(code(await stalled.ensure({...stalled.command, token: randomUUID()})), 'RATE_LIMITED');
|
|
stalled.manager.stop();
|
|
const replies = await Promise.all([first, ...queued]);
|
|
assert.ok(replies.every(reply => reply.status === 'error'));
|
|
assert.equal(stalled.sockets.length, 1);
|
|
});
|
|
|
|
test('lifecycle events are forwarded while ready and congestion closes the stale transport', async t => {
|
|
const app = setup();
|
|
t.after(() => app.manager.stop());
|
|
await app.ensure();
|
|
const socket = app.sockets[0];
|
|
const stream = app.streams[0];
|
|
let resyncs = 0;
|
|
stream.resync = () => { resyncs++; };
|
|
const event = {name: 'status_update', seq: 1, payload: {status: 'offline', context_exists: false}};
|
|
let lifecycle = 0;
|
|
socket.on('bot_event', () => lifecycle++);
|
|
stream.event(event);
|
|
assert.equal(lifecycle, 1);
|
|
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 0);
|
|
stream.event({name: 'log', seq: 2, payload: {records: []}});
|
|
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 1);
|
|
socket.emit('relay_status', {forward_session_id: app.command.forward_session_id, node_ready: true, reason: 'resync_required'});
|
|
assert.equal(resyncs, 1);
|
|
socket.io.engine.writeBuffer.length = 64;
|
|
stream.event({...event, seq: 2});
|
|
stream.event({name: 'log', seq: 3, payload: {records: []}});
|
|
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 1);
|
|
assert.equal(lifecycle, 1);
|
|
assert.equal(socket.connected, false);
|
|
socket.emit('relay_status', {forward_session_id: app.command.forward_session_id, node_ready: true, reason: 'resync_required'});
|
|
assert.equal(resyncs, 1);
|
|
socket.disconnect();
|
|
stream.event({...event, seq: 3});
|
|
stream.event({name: 'log', seq: 4, payload: {records: []}});
|
|
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 1);
|
|
assert.equal(lifecycle, 1);
|
|
assert.equal(stream.listeners, 0);
|
|
});
|
|
|
|
test('info updates use the data envelope and share push congestion and rate controls with logs', async t => {
|
|
const app = setup(); t.after(() => app.manager.stop());
|
|
let ready;
|
|
app.hooks.set('forward_node_ready', ({respond}) => {ready = respond;});
|
|
const pending = app.ensure(); await tick();
|
|
const stream = app.streams[0]; const socket = app.sockets[0];
|
|
const event = {bot_id: 2, context_generation: randomUUID(), stream_id: randomUUID(), emitted_at: Date.now(),
|
|
seq: 1, name: 'info_update', payload: {name: 'health', value: 18}};
|
|
stream.event(event);
|
|
assert.equal(socket.sent.some(([name]) => name === 'info_update'), false);
|
|
ready({node_ready: true}); await pending;
|
|
stream.event(event);
|
|
const packet = socket.sent.find(([name]) => name === 'info_update')[1];
|
|
assert.deepEqual(Object.keys(packet).sort(), ['bot_id', 'context_generation', 'data', 'emitted_at', 'forward_session_id', 'seq', 'stream_id']);
|
|
assert.equal(packet.data.value, 18);
|
|
assert.equal(packet.forward_session_id, app.command.forward_session_id);
|
|
socket.io.engine.writeBuffer.length = 64; stream.event({...event, seq: 2});
|
|
assert.equal(socket.sent.filter(([name]) => name === 'info_update').length, 1);
|
|
socket.io.engine.writeBuffer.length = 0;
|
|
for (let seq = 3; seq < 203; seq++) stream.event({...event, seq, name: seq % 2 ? 'log' : 'info_update'});
|
|
assert.equal(socket.sent.filter(([name]) => ['info_update', 'bot_log'].includes(name)).length, 149);
|
|
socket.disconnect(); stream.event({...event, seq: 204});
|
|
assert.equal(stream.listeners, 0);
|
|
assert.equal(socket.sent.filter(([name]) => ['info_update', 'bot_log'].includes(name)).length, 149);
|
|
});
|
|
|
|
test('plugin lifecycle updates survive a temporarily unwritable transport and retain congestion bounds', async t => {
|
|
const app = setup(); t.after(() => app.manager.stop());
|
|
await app.ensure();
|
|
const socket = app.sockets[0], stream = app.streams[0];
|
|
const received = [];
|
|
socket.on('info_update', update => received.push(update));
|
|
// Match Socket.IO volatile behavior during an outstanding WebSocket write.
|
|
socket.volatile = {emit() {}};
|
|
const event = {bot_id: 2, context_generation: randomUUID(), stream_id: randomUUID(), emitted_at: Date.now(),
|
|
name: 'info_update', payload: {name: 'plugin_states', value: {available: true, plugins: []}}};
|
|
for (const [index, lifecycle] of ['initializing', 'enabled'].entries()) {
|
|
stream.event({...event, seq: index + 1, payload: {...event.payload, value: {available: true,
|
|
plugins: [{name: 'auto_reconnect', path: 'runtime', lifecycle, enabled: lifecycle === 'enabled', states: {attempts: 0}}]}}});
|
|
}
|
|
assert.deepEqual(received.map(update => update.data.value.plugins[0].lifecycle), ['initializing', 'enabled']);
|
|
assert.deepEqual(received.map(update => update.seq), [1, 2]);
|
|
assert.ok(received.every(update => update.forward_session_id === app.command.forward_session_id));
|
|
assert.ok(received.every(update => !('name' in update) && !('payload' in update)));
|
|
stream.event({...event, seq: 3, payload: {name: 'health', value: 20}});
|
|
assert.equal(received.length, 2);
|
|
socket.io.engine.writeBuffer.length = 64;
|
|
stream.event({...event, seq: 4});
|
|
assert.equal(received.length, 2);
|
|
socket.io.engine.writeBuffer.length = 0;
|
|
for (let seq = 5; seq <= 155; seq++) stream.event({...event, seq});
|
|
assert.equal(received.length, 148);
|
|
socket.disconnect(); stream.event({...event, seq: 156});
|
|
assert.equal(received.length, 148);
|
|
assert.equal(stream.listeners, 0);
|
|
});
|
|
|
|
test('forward_bot handler requires an ACK and returns structured authorization errors', async () => {
|
|
const {handler} = module.exports;
|
|
const socket = {connected: true, emit() { assert.fail('Unexpected non-ACK response'); }};
|
|
await handler(socket, command(), undefined);
|
|
const replies = [];
|
|
await handler(socket, command(), reply => replies.push(reply));
|
|
assert.equal(replies.length, 1);
|
|
assert.equal(code(replies[0]), 'UNAUTHORIZED');
|
|
socket.connected = false;
|
|
await handler(socket, command(), reply => replies.push(reply));
|
|
assert.equal(code(replies[1]), 'NODE_OFFLINE');
|
|
});
|
|
|
|
test('dispatcher delivers forward_bot while hosted relay control events remain on their own handlers', async () => {
|
|
const source = await readFile(new URL('../event_handler/init.ts', import.meta.url), 'utf8');
|
|
const dispatch = transformSync(source.slice(source.indexOf('export async function event_handler'))
|
|
.replace('export async function', 'async function'), {loader: 'ts'}).code;
|
|
const calls = [];
|
|
const map = new Map([['forward_bot', (...args) => calls.push(args)]]);
|
|
const context = {handler_map: map, console_print: {log() {}, warning() {}, error() {}}, socket: {}, payload: command()};
|
|
await vm.runInNewContext(`${dispatch}\nevent_handler(socket, 'forward_bot', payload, () => {});`, context);
|
|
assert.equal(calls.length, 1);
|
|
await vm.runInNewContext("event_handler(socket, 'forward_open_session', payload, () => {});", context);
|
|
assert.equal(calls.length, 1);
|
|
});
|