chore: initial commit
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
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); }};
|
||||
}
|
||||
|
||||
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('bot events are never forwarded while connected, congested or disconnected', 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}};
|
||||
stream.event(event);
|
||||
assert.equal(socket.sent.filter(([name]) => name === 'bot_event').length, 0);
|
||||
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.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(socket.sent.filter(([name]) => name === 'bot_event').length, 0);
|
||||
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(socket.sent.filter(([name]) => name === 'bot_event').length, 0);
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user