@webex/internal-plugin-mercury 3.12.0-next.5 → 3.12.0-next.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mercury.js +78 -10
- package/dist/mercury.js.map +1 -1
- package/package.json +17 -17
- package/src/mercury.js +90 -10
- package/test/unit/spec/mercury-events.js +15 -1
- package/test/unit/spec/mercury.js +315 -39
|
@@ -135,7 +135,7 @@ describe('plugin-mercury', () => {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// Small delay to ensure all async operations complete
|
|
138
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
describe('#listen()', () => {
|
|
@@ -294,18 +294,20 @@ describe('plugin-mercury', () => {
|
|
|
294
294
|
.then(() => {
|
|
295
295
|
assert.calledOnce(Socket.prototype.open);
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
// Wait for error handling from 1st failure to schedule 2nd timer
|
|
298
|
+
return promiseTick(10);
|
|
298
299
|
})
|
|
299
300
|
.then(() => {
|
|
300
301
|
clock.tick(mercury.config.backoffTimeReset);
|
|
301
302
|
|
|
302
|
-
|
|
303
|
+
// Wait for 2nd socket.open call AND error handling to schedule 3rd timer
|
|
304
|
+
return promiseTick(10);
|
|
303
305
|
})
|
|
304
306
|
.then(() => {
|
|
305
307
|
assert.calledTwice(Socket.prototype.open);
|
|
306
308
|
clock.tick(2 * mercury.config.backoffTimeReset);
|
|
307
309
|
|
|
308
|
-
return promiseTick(
|
|
310
|
+
return promiseTick(10);
|
|
309
311
|
})
|
|
310
312
|
.then(() => {
|
|
311
313
|
assert.calledThrice(Socket.prototype.open);
|
|
@@ -380,20 +382,20 @@ describe('plugin-mercury', () => {
|
|
|
380
382
|
.then(() => {
|
|
381
383
|
assert.calledOnce(Socket.prototype.open);
|
|
382
384
|
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
return promiseTick(5);
|
|
385
|
+
// Wait for error handling from 1st failure to schedule 2nd timer
|
|
386
|
+
return promiseTick(10);
|
|
386
387
|
})
|
|
387
388
|
.then(() => {
|
|
388
389
|
clock.tick(mercury.config.backoffTimeReset);
|
|
389
390
|
|
|
390
|
-
|
|
391
|
+
// Wait for 2nd socket.open call AND error handling to schedule 3rd timer
|
|
392
|
+
return promiseTick(10);
|
|
391
393
|
})
|
|
392
394
|
.then(() => {
|
|
393
395
|
assert.calledTwice(Socket.prototype.open);
|
|
394
396
|
clock.tick(2 * mercury.config.backoffTimeReset);
|
|
395
397
|
|
|
396
|
-
return promiseTick(
|
|
398
|
+
return promiseTick(10);
|
|
397
399
|
})
|
|
398
400
|
.then(() => {
|
|
399
401
|
assert.calledThrice(Socket.prototype.open);
|
|
@@ -493,15 +495,49 @@ describe('plugin-mercury', () => {
|
|
|
493
495
|
// });
|
|
494
496
|
|
|
495
497
|
describe('when web-high-availability feature is enabled', () => {
|
|
496
|
-
it('marks
|
|
498
|
+
it('marks the URL as failed when socket.open() fails with a ConnectionError', () => {
|
|
499
|
+
// When socket.open() fails, _prepareAndOpenSocket attaches webSocketUrl to the error.
|
|
500
|
+
// The error handler then uses wsUrl = newWSUrl || reason.webSocketUrl, so markFailedUrl
|
|
501
|
+
// is called with the URL obtained before the socket open attempt.
|
|
497
502
|
webex.internal.feature.getFeature.returns(Promise.resolve(true));
|
|
498
503
|
socketOpenStub.restore();
|
|
499
504
|
socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
|
|
500
505
|
socketOpenStub.onCall(0).returns(Promise.reject(new ConnectionError({code: 4001})));
|
|
501
506
|
const promise = mercury.connect();
|
|
502
507
|
|
|
503
|
-
return promiseTick(
|
|
508
|
+
return promiseTick(15).then(() => {
|
|
504
509
|
assert.calledOnce(webex.internal.services.markFailedUrl);
|
|
510
|
+
assert.calledWith(
|
|
511
|
+
webex.internal.services.markFailedUrl,
|
|
512
|
+
sinon.match(/example-2\.com/)
|
|
513
|
+
);
|
|
514
|
+
clock.tick(1000);
|
|
515
|
+
|
|
516
|
+
return promise;
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('does not mark a URL as failed when no wsUrl is available in ConnectionError', () => {
|
|
521
|
+
// If the error has no webSocketUrl and newWSUrl is also undefined,
|
|
522
|
+
// markFailedUrl should not be called.
|
|
523
|
+
webex.internal.feature.getFeature.returns(Promise.resolve(true));
|
|
524
|
+
|
|
525
|
+
const errWithNoUrl = new ConnectionError({code: 4001});
|
|
526
|
+
// Explicitly ensure no webSocketUrl on the error
|
|
527
|
+
delete errWithNoUrl.webSocketUrl;
|
|
528
|
+
|
|
529
|
+
// Stub _prepareAndOpenSocket so it rejects without attaching webSocketUrl
|
|
530
|
+
// (simulating a failure before socket.open is reached, e.g. _prepareUrl failure)
|
|
531
|
+
const prepareAndOpenStub = sinon.stub(mercury, '_prepareAndOpenSocket');
|
|
532
|
+
|
|
533
|
+
prepareAndOpenStub.onCall(0).returns(Promise.reject(errWithNoUrl));
|
|
534
|
+
prepareAndOpenStub.returns(Promise.resolve('ws://example-2.com'));
|
|
535
|
+
|
|
536
|
+
const promise = mercury.connect();
|
|
537
|
+
|
|
538
|
+
return promiseTick(15).then(() => {
|
|
539
|
+
assert.notCalled(webex.internal.services.markFailedUrl);
|
|
540
|
+
prepareAndOpenStub.restore();
|
|
505
541
|
clock.tick(1000);
|
|
506
542
|
|
|
507
543
|
return promise;
|
|
@@ -532,13 +568,13 @@ describe('plugin-mercury', () => {
|
|
|
532
568
|
|
|
533
569
|
return promise.then(() =>
|
|
534
570
|
promiseTick(2)
|
|
535
|
-
|
|
536
|
-
|
|
571
|
+
.then(() => {
|
|
572
|
+
clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
|
|
537
573
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
574
|
+
return promiseTick(2);
|
|
575
|
+
})
|
|
576
|
+
.then(() => {
|
|
577
|
+
assert.calledOnce(Socket.prototype.open);
|
|
542
578
|
})
|
|
543
579
|
);
|
|
544
580
|
});
|
|
@@ -754,12 +790,18 @@ describe('plugin-mercury', () => {
|
|
|
754
790
|
return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
|
|
755
791
|
// By this time backoffCall and mercury socket should be defined by the
|
|
756
792
|
// 'connect' call
|
|
757
|
-
assert.isDefined(
|
|
793
|
+
assert.isDefined(
|
|
794
|
+
mercury.backoffCalls.get('mercury-default-session'),
|
|
795
|
+
'Mercury backoffCall is not defined'
|
|
796
|
+
);
|
|
758
797
|
assert.isDefined(mercury.socket, 'Mercury socket is not defined');
|
|
759
798
|
// Calling disconnect will abort the backoffCall, close the socket, and
|
|
760
799
|
// reject the connect
|
|
761
800
|
mercury.disconnect();
|
|
762
|
-
assert.isUndefined(
|
|
801
|
+
assert.isUndefined(
|
|
802
|
+
mercury.backoffCalls.get('mercury-default-session'),
|
|
803
|
+
'Mercury backoffCall is still defined'
|
|
804
|
+
);
|
|
763
805
|
// The socket will never be unset (which seems bad)
|
|
764
806
|
assert.isDefined(mercury.socket, 'Mercury socket is not defined');
|
|
765
807
|
|
|
@@ -815,7 +857,10 @@ describe('plugin-mercury', () => {
|
|
|
815
857
|
return assert.isRejected(promise).then((error) => {
|
|
816
858
|
const lastError = mercury.getLastError();
|
|
817
859
|
|
|
818
|
-
assert.equal(
|
|
860
|
+
assert.equal(
|
|
861
|
+
error.message,
|
|
862
|
+
`Mercury Connection Aborted for ${mercury.defaultSessionId}`
|
|
863
|
+
);
|
|
819
864
|
assert.isDefined(lastError);
|
|
820
865
|
assert.equal(lastError, realError);
|
|
821
866
|
});
|
|
@@ -1113,7 +1158,6 @@ describe('plugin-mercury', () => {
|
|
|
1113
1158
|
});
|
|
1114
1159
|
});
|
|
1115
1160
|
|
|
1116
|
-
|
|
1117
1161
|
describe('#_onmessage() with shutdown message', () => {
|
|
1118
1162
|
beforeEach(() => {
|
|
1119
1163
|
sinon.stub(mercury, '_handleImminentShutdown');
|
|
@@ -1249,8 +1293,18 @@ describe('plugin-mercury', () => {
|
|
|
1249
1293
|
|
|
1250
1294
|
// Normal flow emits namespace-specific events after processing handlers.
|
|
1251
1295
|
// The early-return guard only emits 'event', so asserting these proves the normal path was taken.
|
|
1252
|
-
assert.calledWith(
|
|
1253
|
-
|
|
1296
|
+
assert.calledWith(
|
|
1297
|
+
mercury._emit,
|
|
1298
|
+
mercury.defaultSessionId,
|
|
1299
|
+
'event:conversation',
|
|
1300
|
+
event.data
|
|
1301
|
+
);
|
|
1302
|
+
assert.calledWith(
|
|
1303
|
+
mercury._emit,
|
|
1304
|
+
mercury.defaultSessionId,
|
|
1305
|
+
'event:conversation.activity',
|
|
1306
|
+
event.data
|
|
1307
|
+
);
|
|
1254
1308
|
});
|
|
1255
1309
|
});
|
|
1256
1310
|
|
|
@@ -1280,6 +1334,129 @@ describe('plugin-mercury', () => {
|
|
|
1280
1334
|
});
|
|
1281
1335
|
});
|
|
1282
1336
|
|
|
1337
|
+
describe('#_onclose() with code 4000', () => {
|
|
1338
|
+
let mockSocket, anotherSocket;
|
|
1339
|
+
|
|
1340
|
+
beforeEach(() => {
|
|
1341
|
+
mockSocket = {
|
|
1342
|
+
url: 'ws://active-socket.com',
|
|
1343
|
+
removeAllListeners: sinon.stub(),
|
|
1344
|
+
};
|
|
1345
|
+
anotherSocket = {
|
|
1346
|
+
url: 'ws://old-socket.com',
|
|
1347
|
+
removeAllListeners: sinon.stub(),
|
|
1348
|
+
};
|
|
1349
|
+
mercury.socket = mockSocket;
|
|
1350
|
+
mercury.sockets.set(mercury.defaultSessionId, mockSocket);
|
|
1351
|
+
mercury.sessionWebSocketUrls.set(mercury.defaultSessionId, mockSocket.url);
|
|
1352
|
+
mercury.connected = true;
|
|
1353
|
+
sinon.stub(mercury, '_emit');
|
|
1354
|
+
sinon.stub(mercury, '_reconnect');
|
|
1355
|
+
sinon.stub(mercury, 'unset');
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
afterEach(() => {
|
|
1359
|
+
mercury._emit.restore();
|
|
1360
|
+
mercury._reconnect.restore();
|
|
1361
|
+
mercury.unset.restore();
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
it('should not reconnect when active socket is replaced', () => {
|
|
1365
|
+
const closeEvent = {
|
|
1366
|
+
code: 4000,
|
|
1367
|
+
reason: 'Replaced',
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1371
|
+
|
|
1372
|
+
assert.calledOnceWithExactly(
|
|
1373
|
+
webex.internal.metrics.submitClientMetrics,
|
|
1374
|
+
'JS_SDK_MERCURY_CLOSE_4000',
|
|
1375
|
+
{
|
|
1376
|
+
fields: {
|
|
1377
|
+
action: 'no_action',
|
|
1378
|
+
close_code: 4000,
|
|
1379
|
+
close_reason: 'Replaced',
|
|
1380
|
+
is_active_socket: true,
|
|
1381
|
+
message_type: 'replaced',
|
|
1382
|
+
session_id: mercury.defaultSessionId,
|
|
1383
|
+
},
|
|
1384
|
+
tags: {
|
|
1385
|
+
action: 'no_action',
|
|
1386
|
+
message_type: 'replaced',
|
|
1387
|
+
},
|
|
1388
|
+
}
|
|
1389
|
+
);
|
|
1390
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
|
|
1391
|
+
assert.notCalled(mercury._reconnect);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('should reconnect active socket when 4000 has an unknown reason', () => {
|
|
1395
|
+
const closeEvent = {
|
|
1396
|
+
code: 4000,
|
|
1397
|
+
reason: 'Unexpected close',
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1401
|
+
|
|
1402
|
+
assert.calledOnceWithExactly(
|
|
1403
|
+
webex.internal.metrics.submitClientMetrics,
|
|
1404
|
+
'JS_SDK_MERCURY_CLOSE_4000',
|
|
1405
|
+
{
|
|
1406
|
+
fields: {
|
|
1407
|
+
action: 'reconnect',
|
|
1408
|
+
close_code: 4000,
|
|
1409
|
+
close_reason: 'Unexpected close',
|
|
1410
|
+
is_active_socket: true,
|
|
1411
|
+
message_type: 'other',
|
|
1412
|
+
session_id: mercury.defaultSessionId,
|
|
1413
|
+
},
|
|
1414
|
+
tags: {
|
|
1415
|
+
action: 'reconnect',
|
|
1416
|
+
message_type: 'other',
|
|
1417
|
+
},
|
|
1418
|
+
}
|
|
1419
|
+
);
|
|
1420
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.transient', closeEvent);
|
|
1421
|
+
assert.calledWith(
|
|
1422
|
+
mercury._reconnect,
|
|
1423
|
+
mockSocket.url,
|
|
1424
|
+
mercury.defaultSessionId
|
|
1425
|
+
);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
it('should not reconnect a non-active socket with an unknown 4000 reason', () => {
|
|
1429
|
+
const closeEvent = {
|
|
1430
|
+
code: 4000,
|
|
1431
|
+
reason: 'Unexpected close',
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1435
|
+
|
|
1436
|
+
assert.calledOnceWithExactly(
|
|
1437
|
+
webex.internal.metrics.submitClientMetrics,
|
|
1438
|
+
'JS_SDK_MERCURY_CLOSE_4000',
|
|
1439
|
+
{
|
|
1440
|
+
fields: {
|
|
1441
|
+
action: 'ignore_non_active',
|
|
1442
|
+
close_code: 4000,
|
|
1443
|
+
close_reason: 'Unexpected close',
|
|
1444
|
+
is_active_socket: false,
|
|
1445
|
+
message_type: 'other',
|
|
1446
|
+
session_id: mercury.defaultSessionId,
|
|
1447
|
+
},
|
|
1448
|
+
tags: {
|
|
1449
|
+
action: 'ignore_non_active',
|
|
1450
|
+
message_type: 'other',
|
|
1451
|
+
},
|
|
1452
|
+
}
|
|
1453
|
+
);
|
|
1454
|
+
assert.notCalled(mercury._emit);
|
|
1455
|
+
assert.notCalled(mercury._reconnect);
|
|
1456
|
+
assert.isTrue(mercury.connected);
|
|
1457
|
+
});
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1283
1460
|
describe('#_onclose() with code 4001 (shutdown replacement)', () => {
|
|
1284
1461
|
let mockSocket, anotherSocket;
|
|
1285
1462
|
|
|
@@ -1314,7 +1491,12 @@ describe('plugin-mercury', () => {
|
|
|
1314
1491
|
|
|
1315
1492
|
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1316
1493
|
|
|
1317
|
-
assert.calledWith(
|
|
1494
|
+
assert.calledWith(
|
|
1495
|
+
mercury._emit,
|
|
1496
|
+
mercury.defaultSessionId,
|
|
1497
|
+
'offline.permanent',
|
|
1498
|
+
closeEvent
|
|
1499
|
+
);
|
|
1318
1500
|
assert.notCalled(mercury._reconnect); // No reconnect for 4001 on active socket
|
|
1319
1501
|
assert.isFalse(mercury.connected);
|
|
1320
1502
|
});
|
|
@@ -1327,7 +1509,12 @@ describe('plugin-mercury', () => {
|
|
|
1327
1509
|
|
|
1328
1510
|
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1329
1511
|
|
|
1330
|
-
assert.calledWith(
|
|
1512
|
+
assert.calledWith(
|
|
1513
|
+
mercury._emit,
|
|
1514
|
+
mercury.defaultSessionId,
|
|
1515
|
+
'offline.replaced',
|
|
1516
|
+
closeEvent
|
|
1517
|
+
);
|
|
1331
1518
|
assert.notCalled(mercury._reconnect);
|
|
1332
1519
|
assert.isTrue(mercury.connected); // Should remain connected
|
|
1333
1520
|
assert.notCalled(mercury.unset);
|
|
@@ -1341,7 +1528,12 @@ describe('plugin-mercury', () => {
|
|
|
1341
1528
|
|
|
1342
1529
|
// Test non-active socket
|
|
1343
1530
|
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1344
|
-
assert.calledWith(
|
|
1531
|
+
assert.calledWith(
|
|
1532
|
+
mercury._emit,
|
|
1533
|
+
mercury.defaultSessionId,
|
|
1534
|
+
'offline.replaced',
|
|
1535
|
+
closeEvent
|
|
1536
|
+
);
|
|
1345
1537
|
|
|
1346
1538
|
// Reset the spy call history
|
|
1347
1539
|
mercury._emit.resetHistory();
|
|
@@ -1349,7 +1541,12 @@ describe('plugin-mercury', () => {
|
|
|
1349
1541
|
// Test active socket
|
|
1350
1542
|
mercury.sockets.set(mercury.defaultSessionId, mockSocket);
|
|
1351
1543
|
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1352
|
-
assert.calledWith(
|
|
1544
|
+
assert.calledWith(
|
|
1545
|
+
mercury._emit,
|
|
1546
|
+
mercury.defaultSessionId,
|
|
1547
|
+
'offline.permanent',
|
|
1548
|
+
closeEvent
|
|
1549
|
+
);
|
|
1353
1550
|
});
|
|
1354
1551
|
|
|
1355
1552
|
it('should handle missing sourceSocket parameter (treats as non-active)', () => {
|
|
@@ -1361,7 +1558,12 @@ describe('plugin-mercury', () => {
|
|
|
1361
1558
|
mercury._onclose(mercury.defaultSessionId, closeEvent); // No sourceSocket parameter
|
|
1362
1559
|
|
|
1363
1560
|
// With simplified logic, undefined !== this.socket, so isActiveSocket = false
|
|
1364
|
-
assert.calledWith(
|
|
1561
|
+
assert.calledWith(
|
|
1562
|
+
mercury._emit,
|
|
1563
|
+
mercury.defaultSessionId,
|
|
1564
|
+
'offline.replaced',
|
|
1565
|
+
closeEvent
|
|
1566
|
+
);
|
|
1365
1567
|
assert.notCalled(mercury._reconnect);
|
|
1366
1568
|
});
|
|
1367
1569
|
|
|
@@ -1394,6 +1596,76 @@ describe('plugin-mercury', () => {
|
|
|
1394
1596
|
});
|
|
1395
1597
|
});
|
|
1396
1598
|
|
|
1599
|
+
describe('#_onclose() reconnect URL derivation', () => {
|
|
1600
|
+
const sessionId = 'mercury-default-session';
|
|
1601
|
+
// The URL resolved and stored in _prepareAndOpenSocket (catalog-valid host).
|
|
1602
|
+
const resolvedUrl = 'wss://mercury-connection.wbx2.com/v1/apps/wx2/registrations';
|
|
1603
|
+
// The URL the native socket was actually opened with, after a lower
|
|
1604
|
+
// layer (e.g. the same-site websocket proxy) rewrote it into a
|
|
1605
|
+
// non-catalog host. Reusing this would fail host-catalog validation.
|
|
1606
|
+
const proxiedUrl = 'wss://web.webex.com/webproxy/mercury-connection.wbx2.com/v1/apps/wx2';
|
|
1607
|
+
let sourceSocket;
|
|
1608
|
+
|
|
1609
|
+
beforeEach(() => {
|
|
1610
|
+
sourceSocket = {
|
|
1611
|
+
url: proxiedUrl,
|
|
1612
|
+
removeAllListeners: sinon.stub(),
|
|
1613
|
+
};
|
|
1614
|
+
mercury.socket = sourceSocket;
|
|
1615
|
+
mercury.sockets.set(sessionId, sourceSocket);
|
|
1616
|
+
mercury.sessionWebSocketUrls.set(sessionId, resolvedUrl);
|
|
1617
|
+
mercury.connected = true;
|
|
1618
|
+
sinon.stub(mercury, '_emit');
|
|
1619
|
+
sinon.stub(mercury, '_reconnect');
|
|
1620
|
+
sinon.stub(mercury, 'unset');
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
afterEach(() => {
|
|
1624
|
+
mercury._emit.restore();
|
|
1625
|
+
mercury._reconnect.restore();
|
|
1626
|
+
mercury.unset.restore();
|
|
1627
|
+
mercury.sockets.clear();
|
|
1628
|
+
mercury.sessionWebSocketUrls.clear();
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
[1001, 1005, 1006, 1011].forEach((code) => {
|
|
1632
|
+
it(`reconnects an active socket using the stored resolved URL (not the proxied socket.url) for code ${code}`, () => {
|
|
1633
|
+
mercury._onclose(sessionId, {code, reason: 'transient'}, sourceSocket);
|
|
1634
|
+
|
|
1635
|
+
assert.calledOnceWithExactly(mercury._reconnect, resolvedUrl, sessionId);
|
|
1636
|
+
assert.neverCalledWith(mercury._reconnect, proxiedUrl, sessionId);
|
|
1637
|
+
});
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it('reconnects using the stored resolved URL for a normal close (1000) with a reconnect reason', () => {
|
|
1641
|
+
mercury._onclose(sessionId, {code: 1000, reason: 'done (forced)'}, sourceSocket);
|
|
1642
|
+
|
|
1643
|
+
assert.calledOnceWithExactly(mercury._reconnect, resolvedUrl, sessionId);
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
it('does not reconnect when a non-active (old) socket closes', () => {
|
|
1647
|
+
const oldSocket = {
|
|
1648
|
+
url: proxiedUrl,
|
|
1649
|
+
removeAllListeners: sinon.stub(),
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
mercury._onclose(sessionId, {code: 1006, reason: 'transient'}, oldSocket);
|
|
1653
|
+
|
|
1654
|
+
assert.notCalled(mercury._reconnect);
|
|
1655
|
+
});
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
describe('#disconnectAll()', () => {
|
|
1659
|
+
it('clears sessionWebSocketUrls along with the other session state', async () => {
|
|
1660
|
+
mercury.sessionWebSocketUrls.set('mercury-default-session', 'ws://example.com');
|
|
1661
|
+
mercury.sessionWebSocketUrls.set('llm-session', 'ws://example-2.com');
|
|
1662
|
+
|
|
1663
|
+
await mercury.disconnectAll();
|
|
1664
|
+
|
|
1665
|
+
assert.equal(mercury.sessionWebSocketUrls.size, 0);
|
|
1666
|
+
});
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1397
1669
|
describe('shutdown switchover with retry logic', () => {
|
|
1398
1670
|
let connectWithBackoffStub;
|
|
1399
1671
|
const sessionId = 'mercury-default-session';
|
|
@@ -1582,6 +1854,16 @@ describe('plugin-mercury', () => {
|
|
|
1582
1854
|
assert.equal(result, 'ws://example.com');
|
|
1583
1855
|
});
|
|
1584
1856
|
|
|
1857
|
+
it('stores the resolved (pre-proxy) URL under the sessionId in sessionWebSocketUrls', async () => {
|
|
1858
|
+
const sessionId = 'llm-session';
|
|
1859
|
+
|
|
1860
|
+
await mercury._prepareAndOpenSocket(mockSocket, 'ws://original.com', sessionId);
|
|
1861
|
+
|
|
1862
|
+
// The stored value is the URL resolved by _prepareUrl, not the raw
|
|
1863
|
+
// input, so _onclose can later reconnect from a catalog-valid host.
|
|
1864
|
+
assert.equal(mercury.sessionWebSocketUrls.get(sessionId), 'ws://example.com');
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1585
1867
|
it('should handle errors during socket open', async () => {
|
|
1586
1868
|
mockSocket.open.returns(Promise.reject(new Error('Open failed')));
|
|
1587
1869
|
|
|
@@ -1590,6 +1872,7 @@ describe('plugin-mercury', () => {
|
|
|
1590
1872
|
assert.fail('Should have thrown an error');
|
|
1591
1873
|
} catch (err) {
|
|
1592
1874
|
assert.equal(err.message, 'Open failed');
|
|
1875
|
+
assert.equal(err.webSocketUrl, 'ws://example.com');
|
|
1593
1876
|
}
|
|
1594
1877
|
});
|
|
1595
1878
|
});
|
|
@@ -1648,11 +1931,7 @@ describe('plugin-mercury', () => {
|
|
|
1648
1931
|
onSuccess: (newSocket, url) => {
|
|
1649
1932
|
mercury.socket = newSocket;
|
|
1650
1933
|
mercury.connected = true;
|
|
1651
|
-
mercury._emit(
|
|
1652
|
-
sessionId,
|
|
1653
|
-
'event:mercury_shutdown_switchover_complete',
|
|
1654
|
-
{url}
|
|
1655
|
-
);
|
|
1934
|
+
mercury._emit(sessionId, 'event:mercury_shutdown_switchover_complete', {url});
|
|
1656
1935
|
},
|
|
1657
1936
|
});
|
|
1658
1937
|
|
|
@@ -1680,12 +1959,9 @@ describe('plugin-mercury', () => {
|
|
|
1680
1959
|
it('should check _shutdownSwitchoverBackoffCall for shutdown connections', () => {
|
|
1681
1960
|
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1682
1961
|
|
|
1683
|
-
const result = mercury._attemptConnection(
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
callback,
|
|
1687
|
-
{isShutdownSwitchover: true}
|
|
1688
|
-
);
|
|
1962
|
+
const result = mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1963
|
+
isShutdownSwitchover: true,
|
|
1964
|
+
});
|
|
1689
1965
|
|
|
1690
1966
|
return result.catch((err) => {
|
|
1691
1967
|
assert.instanceOf(err, Error);
|