mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-23 17:05:28 +08:00
53 lines
4.1 KiB
JavaScript
53 lines
4.1 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();
|
|
});
|