Files
LangBot/web/tests/unit/websocket-client.test.mjs
T

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();
});