mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-15 03:02:17 -05:00
147 lines
5.4 KiB
JavaScript
147 lines
5.4 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { EventEmitter } = require('node:events');
|
|
const { createConnectionManager } = require('./connection');
|
|
|
|
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
|
|
|
function deferred() {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
function setup(t, attempts = []) {
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
const runtimes = [];
|
|
const logs = [];
|
|
const manager = createConnectionManager({
|
|
logger: {
|
|
warn: (...args) => logs.push(args),
|
|
error: (...args) => logs.push(args),
|
|
},
|
|
createRuntime() {
|
|
const attempt = attempts[runtimes.length] || {};
|
|
const client = new EventEmitter();
|
|
const runtime = {
|
|
client,
|
|
stopped: false,
|
|
destroyed: false,
|
|
refresh() {},
|
|
stop() { runtime.stopped = true; },
|
|
};
|
|
client.login = async (token) => {
|
|
runtime.token = token;
|
|
await attempt.login?.();
|
|
};
|
|
client.destroy = async () => {
|
|
await attempt.destroy?.();
|
|
runtime.destroyed = true;
|
|
};
|
|
runtimes.push(runtime);
|
|
return runtime;
|
|
},
|
|
});
|
|
return { manager, runtimes, logs };
|
|
}
|
|
|
|
test('retries a network failure at startup with a fresh client', async (t) => {
|
|
const { manager, runtimes } = setup(t, [{ login: () => { throw new Error('getaddrinfo EAI_AGAIN'); } }]);
|
|
await manager.configure({ enabled: true, token: 'token' });
|
|
await flush();
|
|
assert.equal(runtimes[0].stopped, true);
|
|
assert.equal(runtimes[0].destroyed, true);
|
|
t.mock.timers.tick(4999);
|
|
await flush();
|
|
assert.equal(runtimes.length, 1);
|
|
t.mock.timers.tick(1);
|
|
await flush();
|
|
assert.equal(runtimes.length, 2);
|
|
assert.equal(runtimes[1].token, 'token');
|
|
assert.notEqual(runtimes[0].client, runtimes[1].client);
|
|
});
|
|
|
|
test('backs off repeated failures and cancels retries when disabled', async (t) => {
|
|
const failed = { login: () => { throw new Error('Network unavailable'); } };
|
|
const { manager, runtimes, logs } = setup(t, Array(8).fill(failed));
|
|
await manager.configure({ enabled: true, token: 'token' });
|
|
await flush();
|
|
for (const delay of [5000, 10000, 20000, 40000, 60000]) {
|
|
t.mock.timers.tick(delay);
|
|
await flush();
|
|
}
|
|
assert.deepEqual(logs.filter(([message]) => message === 'Retrying Discord login').map(([, data]) => data.delayMs),
|
|
[5000, 10000, 20000, 40000, 60000, 60000]);
|
|
await manager.configure({ enabled: false, token: 'token' });
|
|
t.mock.timers.tick(120000);
|
|
await flush();
|
|
assert.equal(runtimes.length, 6);
|
|
});
|
|
|
|
test('waits for logout before logging in with a changed token', async (t) => {
|
|
const logout = deferred();
|
|
const { manager, runtimes } = setup(t, [{ destroy: () => logout.promise }]);
|
|
await manager.configure({ enabled: true, token: 'old' });
|
|
const change = manager.configure({ enabled: true, token: 'new' });
|
|
await flush();
|
|
assert.equal(runtimes[0].stopped, true);
|
|
assert.equal(runtimes.length, 1);
|
|
logout.resolve();
|
|
await change;
|
|
assert.equal(runtimes[0].destroyed, true);
|
|
assert.equal(runtimes[1].token, 'new');
|
|
});
|
|
|
|
test('retires a pending login before replacement and ignores its stale failure', async (t) => {
|
|
const login = deferred();
|
|
const { manager, runtimes } = setup(t, [{ login: () => login.promise }]);
|
|
await manager.configure({ enabled: true, token: 'old' });
|
|
const change = manager.configure({ enabled: true, token: 'new' });
|
|
await flush();
|
|
assert.equal(runtimes[0].stopped, true);
|
|
assert.equal(runtimes[0].destroyed, false);
|
|
login.reject(new Error('Old connection failed'));
|
|
await change;
|
|
assert.equal(runtimes[0].destroyed, true);
|
|
assert.equal(runtimes[1].token, 'new');
|
|
t.mock.timers.tick(120000);
|
|
await flush();
|
|
assert.equal(runtimes.length, 2);
|
|
});
|
|
|
|
test('re-enabling creates a fresh client; unrelated config edits do not reconnect', async (t) => {
|
|
const { manager, runtimes } = setup(t);
|
|
await manager.configure({ enabled: true, token: 'token' });
|
|
await manager.configure({ enabled: true, token: 'token', channels: { general: 'new-channel' } });
|
|
assert.equal(runtimes.length, 1);
|
|
await manager.configure({ enabled: false, token: 'token' });
|
|
assert.equal(runtimes[0].destroyed, true);
|
|
await manager.configure({ enabled: true, token: 'token' });
|
|
assert.equal(runtimes.length, 2);
|
|
});
|
|
|
|
test('invalid credentials wait for a token edit instead of retrying', async (t) => {
|
|
const { manager, runtimes } = setup(t, [{ login: () => {
|
|
throw Object.assign(new Error('Invalid token'), { code: 'TokenInvalid' });
|
|
} }]);
|
|
await manager.configure({ enabled: true, token: 'bad' });
|
|
await flush();
|
|
t.mock.timers.tick(120000);
|
|
await flush();
|
|
assert.equal(runtimes.length, 1);
|
|
await manager.configure({ enabled: true, token: 'fixed' });
|
|
assert.equal(runtimes[1].token, 'fixed');
|
|
});
|
|
|
|
test('client and gateway errors are logged without throwing', async (t) => {
|
|
const { manager, runtimes, logs } = setup(t);
|
|
await manager.configure({ enabled: true, token: 'token' });
|
|
runtimes[0].client.emit('error', new Error('Client failure'));
|
|
runtimes[0].client.emit('shardError', new Error('Gateway failure'), 0);
|
|
runtimes[0].client.emit('shardDisconnect', { code: 1006 }, 0);
|
|
assert.deepEqual(logs.map(([message]) => message), [
|
|
'Discord client error', 'Discord connection error', 'Discord disconnected',
|
|
]);
|
|
});
|