mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 16:35:20 +08:00
176 lines
5.0 KiB
JavaScript
176 lines
5.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import fs from 'node:fs';
|
|
import ts from 'typescript';
|
|
function load(file, deps = {}, globals = {}) {
|
|
const source = fs
|
|
.readFileSync(
|
|
new URL('../../src/app/infra/websocket/' + file, import.meta.url),
|
|
'utf8',
|
|
)
|
|
.replaceAll('import.meta.env.VITE_API_BASE_URL', 'undefined');
|
|
const code = ts.transpileModule(source, {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
},
|
|
}).outputText;
|
|
const m = { exports: {} };
|
|
new Function('require', 'module', 'exports', ...Object.keys(globals), code)(
|
|
(id) => deps[id],
|
|
m,
|
|
m.exports,
|
|
...Object.values(globals),
|
|
);
|
|
return m.exports;
|
|
}
|
|
const urls = load('url.ts');
|
|
test('OSS and Cloud URLs follow HTTP backend scheme, port and prefix', () => {
|
|
for (const [base, page, expected] of [
|
|
[undefined, 'http://localhost:3000/home', 'ws://localhost:3000/api/ws'],
|
|
[undefined, 'https://cloud.test/home', 'wss://cloud.test/api/ws'],
|
|
[
|
|
'http://localhost:5300',
|
|
'http://localhost:3000',
|
|
'ws://localhost:5300/api/ws',
|
|
],
|
|
['https://api.test/root/', 'https://ui.test', 'wss://api.test/root/api/ws'],
|
|
['/backend/', 'https://ui.test/home', 'wss://ui.test/backend/api/ws'],
|
|
['https://api.test', 'http://ui.test', 'wss://api.test/api/ws'],
|
|
])
|
|
assert.equal(
|
|
urls.backendWebSocketURL(base, page, '/api/ws').toString(),
|
|
expected,
|
|
);
|
|
});
|
|
function setup() {
|
|
let workspace = 'one',
|
|
next = 0;
|
|
const timers = new Map(),
|
|
win = new EventTarget(),
|
|
doc = new EventTarget();
|
|
win.location = { href: 'https://cloud.test/home' };
|
|
doc.visibilityState = 'visible';
|
|
class WS {
|
|
static CONNECTING = 0;
|
|
static OPEN = 1;
|
|
static instances = [];
|
|
readyState = 0;
|
|
constructor(url) {
|
|
this.url = url;
|
|
WS.instances.push(this);
|
|
}
|
|
send(p) {
|
|
this.sent = JSON.parse(p);
|
|
}
|
|
close() {
|
|
this.readyState = 3;
|
|
}
|
|
closed() {
|
|
this.readyState = 3;
|
|
this.onclose?.();
|
|
}
|
|
open() {
|
|
this.readyState = 1;
|
|
this.onopen?.();
|
|
}
|
|
receive(data) {
|
|
this.onmessage?.({ data: JSON.stringify(data) });
|
|
}
|
|
}
|
|
const { WebSocketClient } = load(
|
|
'WebSocketClient.ts',
|
|
{
|
|
'./url': urls,
|
|
'@/app/infra/http/workspaceContext': {
|
|
getActiveWorkspaceUuid: () => workspace,
|
|
},
|
|
},
|
|
{
|
|
window: win,
|
|
document: doc,
|
|
WebSocket: WS,
|
|
localStorage: { getItem: () => 'token' },
|
|
setTimeout: (fn) => {
|
|
timers.set(++next, fn);
|
|
return next;
|
|
},
|
|
clearTimeout: (id) => timers.delete(id),
|
|
setInterval: () => 1,
|
|
clearInterval: () => {},
|
|
console: { warn() {}, error() {}, debug() {} },
|
|
},
|
|
);
|
|
return {
|
|
client: new WebSocketClient('pipeline'),
|
|
WS,
|
|
win,
|
|
timers,
|
|
setWorkspace: (v) => (workspace = v),
|
|
};
|
|
}
|
|
test('only authenticated sockets are connected; disposal removes recovery', async () => {
|
|
const { client, WS, win } = setup(),
|
|
pending = client.connect(),
|
|
ws = WS.instances[0];
|
|
ws.open();
|
|
assert.equal(client.isConnected(), false);
|
|
assert.equal(ws.sent.workspace_uuid, 'one');
|
|
ws.receive({ type: 'connected', connection_id: 'id' });
|
|
assert.equal(await pending, 'id');
|
|
assert(client.isConnected());
|
|
client.disconnect();
|
|
ws.closed();
|
|
win.dispatchEvent(new Event('focus'));
|
|
assert.equal(WS.instances.length, 1);
|
|
});
|
|
test('closed handshakes settle and transient retries survive more than five failures', async () => {
|
|
const { client, WS, timers } = setup();
|
|
const first = client.connect().catch(() => {});
|
|
WS.instances[0].closed();
|
|
await first;
|
|
for (let i = 0; i < 7; i++) {
|
|
const [id, fn] = [...timers][0];
|
|
timers.delete(id);
|
|
fn();
|
|
WS.instances.at(-1).closed();
|
|
await Promise.resolve();
|
|
}
|
|
assert.equal(WS.instances.length, 8);
|
|
assert.equal(timers.size, 1);
|
|
client.disconnect();
|
|
assert.equal(timers.size, 0);
|
|
});
|
|
test('stale closes do not affect replacement sockets and workspace changes block recovery', async () => {
|
|
const { client, WS, win, setWorkspace } = setup();
|
|
const pending = client.connect().catch(() => {}),
|
|
old = WS.instances[0];
|
|
client.disconnect();
|
|
const current = client.connect(),
|
|
ws = WS.instances[1];
|
|
old.closed();
|
|
await pending;
|
|
ws.open();
|
|
ws.receive({ type: 'connected', connection_id: 'new' });
|
|
await current;
|
|
assert(client.isConnected());
|
|
setWorkspace('two');
|
|
ws.closed();
|
|
win.dispatchEvent(new Event('focus'));
|
|
assert.equal(WS.instances.length, 2);
|
|
client.disconnect();
|
|
});
|
|
test('authentication rejection does not reconnect', async () => {
|
|
const { client, WS, timers, win } = setup();
|
|
const pending = client.connect().catch(() => {}),
|
|
ws = WS.instances[0];
|
|
ws.open();
|
|
ws.receive({ type: 'error', message: 'Unauthorized' });
|
|
ws.closed();
|
|
await pending;
|
|
win.dispatchEvent(new Event('focus'));
|
|
assert.equal(timers.size, 0);
|
|
assert.equal(WS.instances.length, 1);
|
|
client.disconnect();
|
|
});
|