@webex/internal-plugin-mercury 3.11.0 → 3.12.0-metrics-init-fix-2.1

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.
@@ -100,9 +100,32 @@ describe('plugin-mercury', () => {
100
100
  });
101
101
 
102
102
  mercury = webex.internal.mercury;
103
+ mercury.defaultSessionId = 'mercury-default-session';
103
104
  });
104
105
 
105
- afterEach(() => {
106
+ afterEach(async () => {
107
+ // Clean up Mercury connections and internal state
108
+ if (mercury) {
109
+ try {
110
+ await mercury.disconnectAll();
111
+ } catch (e) {
112
+ // Ignore cleanup errors
113
+ }
114
+ // Clear any remaining connection promises
115
+ if (mercury._connectPromises) {
116
+ mercury._connectPromises.clear();
117
+ }
118
+ }
119
+
120
+ // Ensure mock socket is properly closed
121
+ if (mockWebSocket && typeof mockWebSocket.close === 'function') {
122
+ try {
123
+ mockWebSocket.close();
124
+ } catch (e) {
125
+ // Ignore cleanup errors
126
+ }
127
+ }
128
+
106
129
  if (socketOpenStub) {
107
130
  socketOpenStub.restore();
108
131
  }
@@ -110,6 +133,9 @@ describe('plugin-mercury', () => {
110
133
  if (Socket.getWebSocketConstructor.restore) {
111
134
  Socket.getWebSocketConstructor.restore();
112
135
  }
136
+
137
+ // Small delay to ensure all async operations complete
138
+ await new Promise((resolve) => setTimeout(resolve, 10));
113
139
  });
114
140
 
115
141
  describe('#listen()', () => {
@@ -268,18 +294,20 @@ describe('plugin-mercury', () => {
268
294
  .then(() => {
269
295
  assert.calledOnce(Socket.prototype.open);
270
296
 
271
- return promiseTick(5);
297
+ // Wait for error handling from 1st failure to schedule 2nd timer
298
+ return promiseTick(10);
272
299
  })
273
300
  .then(() => {
274
301
  clock.tick(mercury.config.backoffTimeReset);
275
302
 
276
- return promiseTick(5);
303
+ // Wait for 2nd socket.open call AND error handling to schedule 3rd timer
304
+ return promiseTick(10);
277
305
  })
278
306
  .then(() => {
279
307
  assert.calledTwice(Socket.prototype.open);
280
308
  clock.tick(2 * mercury.config.backoffTimeReset);
281
309
 
282
- return promiseTick(5);
310
+ return promiseTick(10);
283
311
  })
284
312
  .then(() => {
285
313
  assert.calledThrice(Socket.prototype.open);
@@ -354,20 +382,20 @@ describe('plugin-mercury', () => {
354
382
  .then(() => {
355
383
  assert.calledOnce(Socket.prototype.open);
356
384
 
357
- // I'm not sure why, but it's important the clock doesn't advance
358
- // until a tick happens
359
- return promiseTick(5);
385
+ // Wait for error handling from 1st failure to schedule 2nd timer
386
+ return promiseTick(10);
360
387
  })
361
388
  .then(() => {
362
389
  clock.tick(mercury.config.backoffTimeReset);
363
390
 
364
- return promiseTick(5);
391
+ // Wait for 2nd socket.open call AND error handling to schedule 3rd timer
392
+ return promiseTick(10);
365
393
  })
366
394
  .then(() => {
367
395
  assert.calledTwice(Socket.prototype.open);
368
396
  clock.tick(2 * mercury.config.backoffTimeReset);
369
397
 
370
- return promiseTick(5);
398
+ return promiseTick(10);
371
399
  })
372
400
  .then(() => {
373
401
  assert.calledThrice(Socket.prototype.open);
@@ -467,15 +495,49 @@ describe('plugin-mercury', () => {
467
495
  // });
468
496
 
469
497
  describe('when web-high-availability feature is enabled', () => {
470
- it('marks current socket url as failed and get new one on Connection Error', () => {
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.
471
502
  webex.internal.feature.getFeature.returns(Promise.resolve(true));
472
503
  socketOpenStub.restore();
473
504
  socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
474
505
  socketOpenStub.onCall(0).returns(Promise.reject(new ConnectionError({code: 4001})));
475
506
  const promise = mercury.connect();
476
507
 
477
- return promiseTick(7).then(() => {
508
+ return promiseTick(15).then(() => {
478
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();
479
541
  clock.tick(1000);
480
542
 
481
543
  return promise;
@@ -499,17 +561,22 @@ describe('plugin-mercury', () => {
499
561
 
500
562
  // skipping due to apparent bug with lolex in all browsers but Chrome.
501
563
  skipInBrowser(it)('does not continue attempting to connect', () => {
502
- mercury.connect();
564
+ const promise = mercury.connect();
503
565
 
504
- return promiseTick(2)
505
- .then(() => {
506
- clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
566
+ // Wait for the connection to be established before proceeding
567
+ mockWebSocket.open();
507
568
 
508
- return promiseTick(2);
509
- })
510
- .then(() => {
511
- assert.calledOnce(Socket.prototype.open);
512
- });
569
+ return promise.then(() =>
570
+ promiseTick(2)
571
+ .then(() => {
572
+ clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
573
+
574
+ return promiseTick(2);
575
+ })
576
+ .then(() => {
577
+ assert.calledOnce(Socket.prototype.open);
578
+ })
579
+ );
513
580
  });
514
581
  });
515
582
 
@@ -584,11 +651,11 @@ describe('plugin-mercury', () => {
584
651
  });
585
652
 
586
653
  describe('#logout()', () => {
587
- it('calls disconnect and logs', () => {
654
+ it('calls disconnectAll and logs', () => {
588
655
  sinon.stub(mercury.logger, 'info');
589
- sinon.stub(mercury, 'disconnect');
656
+ sinon.stub(mercury, 'disconnectAll');
590
657
  mercury.logout();
591
- assert.called(mercury.disconnect);
658
+ assert.called(mercury.disconnectAll);
592
659
  assert.calledTwice(mercury.logger.info);
593
660
 
594
661
  assert.calledWith(mercury.logger.info.getCall(0), 'Mercury: logout() called');
@@ -600,24 +667,24 @@ describe('plugin-mercury', () => {
600
667
  });
601
668
 
602
669
  it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout', () => {
603
- sinon.stub(mercury, 'disconnect');
670
+ sinon.stub(mercury, 'disconnectAll');
604
671
  mercury.config.beforeLogoutOptionsCloseReason = 'done (permanent)';
605
672
  mercury.logout();
606
- assert.calledWith(mercury.disconnect, {code: 3050, reason: 'done (permanent)'});
673
+ assert.calledWith(mercury.disconnectAll, {code: 3050, reason: 'done (permanent)'});
607
674
  });
608
675
 
609
676
  it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout if the reason is different than standard', () => {
610
- sinon.stub(mercury, 'disconnect');
677
+ sinon.stub(mercury, 'disconnectAll');
611
678
  mercury.config.beforeLogoutOptionsCloseReason = 'test';
612
679
  mercury.logout();
613
- assert.calledWith(mercury.disconnect, {code: 3050, reason: 'test'});
680
+ assert.calledWith(mercury.disconnectAll, {code: 3050, reason: 'test'});
614
681
  });
615
682
 
616
683
  it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send undefined for logout if the reason is same as standard', () => {
617
- sinon.stub(mercury, 'disconnect');
684
+ sinon.stub(mercury, 'disconnectAll');
618
685
  mercury.config.beforeLogoutOptionsCloseReason = 'done (forced)';
619
686
  mercury.logout();
620
- assert.calledWith(mercury.disconnect, undefined);
687
+ assert.calledWith(mercury.disconnectAll, undefined);
621
688
  });
622
689
  });
623
690
 
@@ -723,12 +790,18 @@ describe('plugin-mercury', () => {
723
790
  return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
724
791
  // By this time backoffCall and mercury socket should be defined by the
725
792
  // 'connect' call
726
- assert.isDefined(mercury.backoffCall, 'Mercury backoffCall is not defined');
793
+ assert.isDefined(
794
+ mercury.backoffCalls.get('mercury-default-session'),
795
+ 'Mercury backoffCall is not defined'
796
+ );
727
797
  assert.isDefined(mercury.socket, 'Mercury socket is not defined');
728
798
  // Calling disconnect will abort the backoffCall, close the socket, and
729
799
  // reject the connect
730
800
  mercury.disconnect();
731
- assert.isUndefined(mercury.backoffCall, 'Mercury backoffCall is still defined');
801
+ assert.isUndefined(
802
+ mercury.backoffCalls.get('mercury-default-session'),
803
+ 'Mercury backoffCall is still defined'
804
+ );
732
805
  // The socket will never be unset (which seems bad)
733
806
  assert.isDefined(mercury.socket, 'Mercury socket is not defined');
734
807
 
@@ -746,16 +819,24 @@ describe('plugin-mercury', () => {
746
819
 
747
820
  let reason;
748
821
 
749
- mercury.backoffCall = undefined;
750
- mercury._attemptConnection('ws://example.com', (_reason) => {
751
- reason = _reason;
752
- });
822
+ mercury.backoffCalls.clear();
823
+
824
+ const promise = mercury._attemptConnection(
825
+ 'ws://example.com',
826
+ 'mercury-default-session',
827
+ (_reason) => {
828
+ reason = _reason;
829
+ }
830
+ );
753
831
 
754
832
  return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
755
833
  assert.equal(
756
834
  reason.message,
757
- 'Mercury: prevent socket open when backoffCall no longer defined'
835
+ `Mercury: prevent socket open when backoffCall no longer defined for ${mercury.defaultSessionId}`
758
836
  );
837
+
838
+ // Ensure the promise was actually rejected (short-circuited)
839
+ return assert.isRejected(promise);
759
840
  });
760
841
  });
761
842
 
@@ -776,7 +857,10 @@ describe('plugin-mercury', () => {
776
857
  return assert.isRejected(promise).then((error) => {
777
858
  const lastError = mercury.getLastError();
778
859
 
779
- assert.equal(error.message, 'Mercury Connection Aborted');
860
+ assert.equal(
861
+ error.message,
862
+ `Mercury Connection Aborted for ${mercury.defaultSessionId}`
863
+ );
780
864
  assert.isDefined(lastError);
781
865
  assert.equal(lastError, realError);
782
866
  });
@@ -870,7 +954,7 @@ describe('plugin-mercury', () => {
870
954
  },
871
955
  };
872
956
  assert.isUndefined(mercury.mercuryTimeOffset);
873
- mercury._setTimeOffset(event);
957
+ mercury._setTimeOffset('mercury-default-session', event);
874
958
  assert.isDefined(mercury.mercuryTimeOffset);
875
959
  assert.isTrue(mercury.mercuryTimeOffset > 0);
876
960
  });
@@ -880,7 +964,7 @@ describe('plugin-mercury', () => {
880
964
  wsWriteTimestamp: Date.now() + 60000,
881
965
  },
882
966
  };
883
- mercury._setTimeOffset(event);
967
+ mercury._setTimeOffset('mercury-default-session', event);
884
968
  assert.isTrue(mercury.mercuryTimeOffset < 0);
885
969
  });
886
970
  it('handles invalid wsWriteTimestamp', () => {
@@ -891,7 +975,7 @@ describe('plugin-mercury', () => {
891
975
  wsWriteTimestamp: invalidTimestamp,
892
976
  },
893
977
  };
894
- mercury._setTimeOffset(event);
978
+ mercury._setTimeOffset('mercury-default-session', event);
895
979
  assert.isUndefined(mercury.mercuryTimeOffset);
896
980
  });
897
981
  });
@@ -998,13 +1082,15 @@ describe('plugin-mercury', () => {
998
1082
  describe('shutdown protocol', () => {
999
1083
  describe('#_handleImminentShutdown()', () => {
1000
1084
  let connectWithBackoffStub;
1085
+ const sessionId = 'mercury-default-session';
1001
1086
 
1002
1087
  beforeEach(() => {
1003
1088
  mercury.connected = true;
1004
- mercury.socket = {
1089
+ mercury.sockets.set(sessionId, {
1005
1090
  url: 'ws://old-socket.com',
1006
1091
  removeAllListeners: sinon.stub(),
1007
- };
1092
+ });
1093
+ mercury.socket = mercury.sockets.get(sessionId);
1008
1094
  connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
1009
1095
  connectWithBackoffStub.returns(Promise.resolve());
1010
1096
  sinon.stub(mercury, '_emit');
@@ -1013,32 +1099,47 @@ describe('plugin-mercury', () => {
1013
1099
  afterEach(() => {
1014
1100
  connectWithBackoffStub.restore();
1015
1101
  mercury._emit.restore();
1102
+ mercury.sockets.clear();
1016
1103
  });
1017
1104
 
1018
1105
  it('should be idempotent - no-op if already in progress', () => {
1019
- mercury._shutdownSwitchoverInProgress = true;
1106
+ // Simulate an existing switchover in progress by seeding the backoff map
1107
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1020
1108
 
1021
- mercury._handleImminentShutdown();
1109
+ mercury._handleImminentShutdown(sessionId);
1022
1110
 
1023
1111
  assert.notCalled(connectWithBackoffStub);
1024
1112
  });
1025
1113
 
1026
1114
  it('should set switchover flags when called', () => {
1027
- mercury._handleImminentShutdown();
1115
+ mercury._handleImminentShutdown(sessionId);
1028
1116
 
1029
- assert.isTrue(mercury._shutdownSwitchoverInProgress);
1117
+ // With _connectWithBackoff stubbed, the backoff map entry may not be created here.
1118
+ // Assert that switchover initiation state was set and a shutdown switchover connect was requested.
1030
1119
  assert.isDefined(mercury._shutdownSwitchoverId);
1120
+
1121
+ assert.calledOnce(connectWithBackoffStub);
1122
+ const callArgs = connectWithBackoffStub.firstCall.args;
1123
+ assert.isUndefined(callArgs[0]); // webSocketUrl
1124
+ assert.equal(callArgs[1], sessionId); // sessionId
1125
+ assert.isObject(callArgs[2]); // context
1126
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1127
+ assert.isObject(callArgs[2].attemptOptions);
1128
+ assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
1031
1129
  });
1032
1130
 
1033
1131
  it('should call _connectWithBackoff with correct parameters', (done) => {
1034
- mercury._handleImminentShutdown();
1132
+ mercury._handleImminentShutdown(sessionId);
1035
1133
 
1036
1134
  process.nextTick(() => {
1037
1135
  assert.calledOnce(connectWithBackoffStub);
1038
1136
  const callArgs = connectWithBackoffStub.firstCall.args;
1039
1137
  assert.isUndefined(callArgs[0]); // webSocketUrl
1040
- assert.isObject(callArgs[1]); // context
1041
- assert.isTrue(callArgs[1].isShutdownSwitchover);
1138
+ assert.equal(callArgs[1], sessionId); // sessionId
1139
+ assert.isObject(callArgs[2]); // context
1140
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1141
+ assert.isObject(callArgs[2].attemptOptions);
1142
+ assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
1042
1143
  done();
1043
1144
  });
1044
1145
  });
@@ -1047,9 +1148,13 @@ describe('plugin-mercury', () => {
1047
1148
  connectWithBackoffStub.restore();
1048
1149
  sinon.stub(mercury, '_connectWithBackoff').throws(new Error('Connection failed'));
1049
1150
 
1050
- mercury._handleImminentShutdown();
1151
+ mercury._handleImminentShutdown(sessionId);
1051
1152
 
1052
- assert.isFalse(mercury._shutdownSwitchoverInProgress);
1153
+ // When an exception happens synchronously, the placeholder entry
1154
+ // should be removed from the map.
1155
+ const switchoverCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
1156
+ assert.isUndefined(switchoverCall);
1157
+ mercury._connectWithBackoff.restore();
1053
1158
  });
1054
1159
  });
1055
1160
 
@@ -1073,10 +1178,15 @@ describe('plugin-mercury', () => {
1073
1178
  },
1074
1179
  };
1075
1180
 
1076
- const result = mercury._onmessage(shutdownEvent);
1181
+ const result = mercury._onmessage(mercury.defaultSessionId, shutdownEvent);
1077
1182
 
1078
1183
  assert.calledOnce(mercury._handleImminentShutdown);
1079
- assert.calledWith(mercury._emit, 'event:mercury_shutdown_imminent', shutdownEvent.data);
1184
+ assert.calledWith(
1185
+ mercury._emit,
1186
+ mercury.defaultSessionId,
1187
+ 'event:mercury_shutdown_imminent',
1188
+ shutdownEvent.data
1189
+ );
1080
1190
  assert.instanceOf(result, Promise);
1081
1191
  });
1082
1192
 
@@ -1087,7 +1197,7 @@ describe('plugin-mercury', () => {
1087
1197
  },
1088
1198
  };
1089
1199
 
1090
- mercury._onmessage(shutdownEvent);
1200
+ mercury._onmessage(mercury.defaultSessionId, shutdownEvent);
1091
1201
 
1092
1202
  assert.calledOnce(mercury._handleImminentShutdown);
1093
1203
  });
@@ -1102,12 +1212,128 @@ describe('plugin-mercury', () => {
1102
1212
  },
1103
1213
  };
1104
1214
 
1105
- mercury._onmessage(regularEvent);
1215
+ mercury._onmessage(mercury.defaultSessionId, regularEvent);
1106
1216
 
1107
1217
  assert.notCalled(mercury._handleImminentShutdown);
1108
1218
  });
1109
1219
  });
1110
1220
 
1221
+ describe('#_onmessage() with missing data or eventType', () => {
1222
+ beforeEach(() => {
1223
+ sinon.stub(mercury, '_emit');
1224
+ sinon.stub(mercury, '_setTimeOffset');
1225
+ sinon.stub(mercury, '_applyOverrides');
1226
+ });
1227
+
1228
+ afterEach(() => {
1229
+ mercury._emit.restore();
1230
+ mercury._setTimeOffset.restore();
1231
+ mercury._applyOverrides.restore();
1232
+ });
1233
+
1234
+ it('should not throw when envelope.data is undefined', () => {
1235
+ const event = {
1236
+ data: {
1237
+ type: 'someType',
1238
+ // no nested data property
1239
+ },
1240
+ };
1241
+
1242
+ const result = mercury._onmessage(mercury.defaultSessionId, event);
1243
+
1244
+ assert.instanceOf(result, Promise);
1245
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
1246
+ });
1247
+
1248
+ it('should not throw when data.eventType is undefined', () => {
1249
+ const event = {
1250
+ data: {
1251
+ type: 'someType',
1252
+ data: {
1253
+ // no eventType property
1254
+ someField: 'value',
1255
+ },
1256
+ },
1257
+ };
1258
+
1259
+ const result = mercury._onmessage(mercury.defaultSessionId, event);
1260
+
1261
+ assert.instanceOf(result, Promise);
1262
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
1263
+ });
1264
+
1265
+ it('should emit generic event for messages without eventType (e.g. subscription responses)', () => {
1266
+ const event = {
1267
+ data: {
1268
+ id: 'msg-123',
1269
+ sequenceNumber: 5,
1270
+ data: {
1271
+ statusCode: 200,
1272
+ },
1273
+ },
1274
+ };
1275
+
1276
+ const result = mercury._onmessage(mercury.defaultSessionId, event);
1277
+
1278
+ assert.instanceOf(result, Promise);
1279
+ assert.calledOnce(mercury._emit);
1280
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
1281
+ });
1282
+
1283
+ it('should still process messages with a valid eventType', async () => {
1284
+ const event = {
1285
+ data: {
1286
+ data: {
1287
+ eventType: 'conversation.activity',
1288
+ },
1289
+ },
1290
+ };
1291
+
1292
+ await mercury._onmessage(mercury.defaultSessionId, event);
1293
+
1294
+ // Normal flow emits namespace-specific events after processing handlers.
1295
+ // The early-return guard only emits 'event', so asserting these proves the normal path was taken.
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
+ );
1308
+ });
1309
+ });
1310
+
1311
+ describe('#_getEventHandlers()', () => {
1312
+ it('should return an empty array when eventType is undefined', () => {
1313
+ const result = mercury._getEventHandlers(undefined);
1314
+
1315
+ assert.deepEqual(result, []);
1316
+ });
1317
+
1318
+ it('should return an empty array when eventType is null', () => {
1319
+ const result = mercury._getEventHandlers(null);
1320
+
1321
+ assert.deepEqual(result, []);
1322
+ });
1323
+
1324
+ it('should return an empty array when eventType is an empty string', () => {
1325
+ const result = mercury._getEventHandlers('');
1326
+
1327
+ assert.deepEqual(result, []);
1328
+ });
1329
+
1330
+ it('should return an empty array when namespace is not registered', () => {
1331
+ const result = mercury._getEventHandlers('unknownNamespace.someEvent');
1332
+
1333
+ assert.deepEqual(result, []);
1334
+ });
1335
+ });
1336
+
1111
1337
  describe('#_onclose() with code 4001 (shutdown replacement)', () => {
1112
1338
  let mockSocket, anotherSocket;
1113
1339
 
@@ -1121,6 +1347,7 @@ describe('plugin-mercury', () => {
1121
1347
  removeAllListeners: sinon.stub(),
1122
1348
  };
1123
1349
  mercury.socket = mockSocket;
1350
+ mercury.sockets.set(mercury.defaultSessionId, mockSocket);
1124
1351
  mercury.connected = true;
1125
1352
  sinon.stub(mercury, '_emit');
1126
1353
  sinon.stub(mercury, '_reconnect');
@@ -1139,9 +1366,14 @@ describe('plugin-mercury', () => {
1139
1366
  reason: 'replaced during shutdown',
1140
1367
  };
1141
1368
 
1142
- mercury._onclose(closeEvent, mockSocket);
1369
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1143
1370
 
1144
- assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
1371
+ assert.calledWith(
1372
+ mercury._emit,
1373
+ mercury.defaultSessionId,
1374
+ 'offline.permanent',
1375
+ closeEvent
1376
+ );
1145
1377
  assert.notCalled(mercury._reconnect); // No reconnect for 4001 on active socket
1146
1378
  assert.isFalse(mercury.connected);
1147
1379
  });
@@ -1152,9 +1384,14 @@ describe('plugin-mercury', () => {
1152
1384
  reason: 'replaced during shutdown',
1153
1385
  };
1154
1386
 
1155
- mercury._onclose(closeEvent, anotherSocket);
1387
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1156
1388
 
1157
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1389
+ assert.calledWith(
1390
+ mercury._emit,
1391
+ mercury.defaultSessionId,
1392
+ 'offline.replaced',
1393
+ closeEvent
1394
+ );
1158
1395
  assert.notCalled(mercury._reconnect);
1159
1396
  assert.isTrue(mercury.connected); // Should remain connected
1160
1397
  assert.notCalled(mercury.unset);
@@ -1167,15 +1404,26 @@ describe('plugin-mercury', () => {
1167
1404
  };
1168
1405
 
1169
1406
  // Test non-active socket
1170
- mercury._onclose(closeEvent, anotherSocket);
1171
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1407
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1408
+ assert.calledWith(
1409
+ mercury._emit,
1410
+ mercury.defaultSessionId,
1411
+ 'offline.replaced',
1412
+ closeEvent
1413
+ );
1172
1414
 
1173
1415
  // Reset the spy call history
1174
1416
  mercury._emit.resetHistory();
1175
1417
 
1176
1418
  // Test active socket
1177
- mercury._onclose(closeEvent, mockSocket);
1178
- assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
1419
+ mercury.sockets.set(mercury.defaultSessionId, mockSocket);
1420
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1421
+ assert.calledWith(
1422
+ mercury._emit,
1423
+ mercury.defaultSessionId,
1424
+ 'offline.permanent',
1425
+ closeEvent
1426
+ );
1179
1427
  });
1180
1428
 
1181
1429
  it('should handle missing sourceSocket parameter (treats as non-active)', () => {
@@ -1184,10 +1432,15 @@ describe('plugin-mercury', () => {
1184
1432
  reason: 'replaced during shutdown',
1185
1433
  };
1186
1434
 
1187
- mercury._onclose(closeEvent); // No sourceSocket parameter
1435
+ mercury._onclose(mercury.defaultSessionId, closeEvent); // No sourceSocket parameter
1188
1436
 
1189
1437
  // With simplified logic, undefined !== this.socket, so isActiveSocket = false
1190
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1438
+ assert.calledWith(
1439
+ mercury._emit,
1440
+ mercury.defaultSessionId,
1441
+ 'offline.replaced',
1442
+ closeEvent
1443
+ );
1191
1444
  assert.notCalled(mercury._reconnect);
1192
1445
  });
1193
1446
 
@@ -1198,7 +1451,7 @@ describe('plugin-mercury', () => {
1198
1451
  };
1199
1452
 
1200
1453
  // Close non-active socket (not the active one)
1201
- mercury._onclose(closeEvent, anotherSocket);
1454
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1202
1455
 
1203
1456
  // Verify listeners were removed from the old socket
1204
1457
  // The _onclose method checks if sourceSocket !== this.socket (non-active)
@@ -1213,22 +1466,94 @@ describe('plugin-mercury', () => {
1213
1466
  };
1214
1467
 
1215
1468
  // Close active socket
1216
- mercury._onclose(closeEvent, mockSocket);
1469
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1217
1470
 
1218
1471
  // Verify listeners were removed from active socket
1219
1472
  assert.calledOnce(mockSocket.removeAllListeners);
1220
1473
  });
1221
1474
  });
1222
1475
 
1476
+ describe('#_onclose() reconnect URL derivation', () => {
1477
+ const sessionId = 'mercury-default-session';
1478
+ // The URL resolved and stored in _prepareAndOpenSocket (catalog-valid host).
1479
+ const resolvedUrl = 'wss://mercury-connection.wbx2.com/v1/apps/wx2/registrations';
1480
+ // The URL the native socket was actually opened with, after a lower
1481
+ // layer (e.g. the same-site websocket proxy) rewrote it into a
1482
+ // non-catalog host. Reusing this would fail host-catalog validation.
1483
+ const proxiedUrl = 'wss://web.webex.com/webproxy/mercury-connection.wbx2.com/v1/apps/wx2';
1484
+ let sourceSocket;
1485
+
1486
+ beforeEach(() => {
1487
+ sourceSocket = {
1488
+ url: proxiedUrl,
1489
+ removeAllListeners: sinon.stub(),
1490
+ };
1491
+ mercury.socket = sourceSocket;
1492
+ mercury.sockets.set(sessionId, sourceSocket);
1493
+ mercury.sessionWebSocketUrls.set(sessionId, resolvedUrl);
1494
+ mercury.connected = true;
1495
+ sinon.stub(mercury, '_emit');
1496
+ sinon.stub(mercury, '_reconnect');
1497
+ sinon.stub(mercury, 'unset');
1498
+ });
1499
+
1500
+ afterEach(() => {
1501
+ mercury._emit.restore();
1502
+ mercury._reconnect.restore();
1503
+ mercury.unset.restore();
1504
+ mercury.sockets.clear();
1505
+ mercury.sessionWebSocketUrls.clear();
1506
+ });
1507
+
1508
+ [1001, 1005, 1006, 1011].forEach((code) => {
1509
+ it(`reconnects an active socket using the stored resolved URL (not the proxied socket.url) for code ${code}`, () => {
1510
+ mercury._onclose(sessionId, {code, reason: 'transient'}, sourceSocket);
1511
+
1512
+ assert.calledOnceWithExactly(mercury._reconnect, resolvedUrl, sessionId);
1513
+ assert.neverCalledWith(mercury._reconnect, proxiedUrl, sessionId);
1514
+ });
1515
+ });
1516
+
1517
+ it('reconnects using the stored resolved URL for a normal close (1000) with a reconnect reason', () => {
1518
+ mercury._onclose(sessionId, {code: 1000, reason: 'done (forced)'}, sourceSocket);
1519
+
1520
+ assert.calledOnceWithExactly(mercury._reconnect, resolvedUrl, sessionId);
1521
+ });
1522
+
1523
+ it('does not reconnect when a non-active (old) socket closes', () => {
1524
+ const oldSocket = {
1525
+ url: proxiedUrl,
1526
+ removeAllListeners: sinon.stub(),
1527
+ };
1528
+
1529
+ mercury._onclose(sessionId, {code: 1006, reason: 'transient'}, oldSocket);
1530
+
1531
+ assert.notCalled(mercury._reconnect);
1532
+ });
1533
+ });
1534
+
1535
+ describe('#disconnectAll()', () => {
1536
+ it('clears sessionWebSocketUrls along with the other session state', async () => {
1537
+ mercury.sessionWebSocketUrls.set('mercury-default-session', 'ws://example.com');
1538
+ mercury.sessionWebSocketUrls.set('llm-session', 'ws://example-2.com');
1539
+
1540
+ await mercury.disconnectAll();
1541
+
1542
+ assert.equal(mercury.sessionWebSocketUrls.size, 0);
1543
+ });
1544
+ });
1545
+
1223
1546
  describe('shutdown switchover with retry logic', () => {
1224
1547
  let connectWithBackoffStub;
1548
+ const sessionId = 'mercury-default-session';
1225
1549
 
1226
1550
  beforeEach(() => {
1227
1551
  mercury.connected = true;
1228
- mercury.socket = {
1552
+ mercury.sockets.set(sessionId, {
1229
1553
  url: 'ws://old-socket.com',
1230
1554
  removeAllListeners: sinon.stub(),
1231
- };
1555
+ });
1556
+ mercury.socket = mercury.sockets.get(sessionId);
1232
1557
  connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
1233
1558
  sinon.stub(mercury, '_emit');
1234
1559
  });
@@ -1236,39 +1561,46 @@ describe('plugin-mercury', () => {
1236
1561
  afterEach(() => {
1237
1562
  connectWithBackoffStub.restore();
1238
1563
  mercury._emit.restore();
1564
+ mercury.sockets.clear();
1239
1565
  });
1240
1566
 
1241
1567
  it('should call _connectWithBackoff with shutdown switchover context', (done) => {
1242
1568
  connectWithBackoffStub.returns(Promise.resolve());
1243
1569
 
1244
- mercury._handleImminentShutdown();
1570
+ mercury._handleImminentShutdown(sessionId);
1245
1571
 
1246
- // Give it a tick for the async call to happen
1247
1572
  process.nextTick(() => {
1248
1573
  assert.calledOnce(connectWithBackoffStub);
1249
1574
  const callArgs = connectWithBackoffStub.firstCall.args;
1250
1575
 
1251
- assert.isUndefined(callArgs[0]); // webSocketUrl is undefined
1252
- assert.isObject(callArgs[1]); // context object
1253
- assert.isTrue(callArgs[1].isShutdownSwitchover);
1254
- assert.isObject(callArgs[1].attemptOptions);
1255
- assert.isTrue(callArgs[1].attemptOptions.isShutdownSwitchover);
1576
+ assert.isUndefined(callArgs[0]); // webSocketUrl
1577
+ assert.equal(callArgs[1], sessionId);
1578
+ assert.isObject(callArgs[2]);
1579
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1580
+ assert.isObject(callArgs[2].attemptOptions);
1581
+ assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
1256
1582
  done();
1257
1583
  });
1258
1584
  });
1259
1585
 
1260
1586
  it('should set _shutdownSwitchoverInProgress flag during switchover', () => {
1261
- connectWithBackoffStub.returns(new Promise(() => {})); // Never resolves
1587
+ // With the new behavior, "in progress" is represented by the presence
1588
+ // of an entry in _shutdownSwitchoverBackoffCalls.
1589
+ // Since _connectWithBackoff is stubbed in this suite, simulate its side-effect
1590
+ // of seeding the backoff-call map entry.
1591
+ connectWithBackoffStub.callsFake(() => {
1592
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1593
+ return new Promise(() => {}); // Never resolves
1594
+ });
1262
1595
 
1263
- mercury._handleImminentShutdown();
1596
+ mercury._handleImminentShutdown(sessionId);
1264
1597
 
1265
- assert.isTrue(mercury._shutdownSwitchoverInProgress);
1598
+ const switchoverBackoffCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
1599
+ assert.isOk(switchoverBackoffCall);
1266
1600
  });
1267
1601
 
1268
1602
  it('should emit success event when switchover completes', async () => {
1269
- // We need to actually call the onSuccess callback to trigger the event
1270
- connectWithBackoffStub.callsFake((url, context) => {
1271
- // Simulate successful connection by calling onSuccess
1603
+ connectWithBackoffStub.callsFake((url, sid, context) => {
1272
1604
  if (context && context.attemptOptions && context.attemptOptions.onSuccess) {
1273
1605
  const mockSocket = {url: 'ws://new-socket.com'};
1274
1606
  context.attemptOptions.onSuccess(mockSocket, 'ws://new-socket.com');
@@ -1276,14 +1608,15 @@ describe('plugin-mercury', () => {
1276
1608
  return Promise.resolve();
1277
1609
  });
1278
1610
 
1279
- mercury._handleImminentShutdown();
1611
+ mercury._handleImminentShutdown(sessionId);
1280
1612
 
1281
- // Wait for async operations
1282
1613
  await promiseTick(50);
1283
1614
 
1284
1615
  const emitCalls = mercury._emit.getCalls();
1285
1616
  const hasCompleteEvent = emitCalls.some(
1286
- (call) => call.args[0] === 'event:mercury_shutdown_switchover_complete'
1617
+ (call) =>
1618
+ call.args[0] === sessionId &&
1619
+ call.args[1] === 'event:mercury_shutdown_switchover_complete'
1287
1620
  );
1288
1621
 
1289
1622
  assert.isTrue(hasCompleteEvent, 'Should emit switchover complete event');
@@ -1294,16 +1627,16 @@ describe('plugin-mercury', () => {
1294
1627
 
1295
1628
  connectWithBackoffStub.returns(Promise.reject(testError));
1296
1629
 
1297
- mercury._handleImminentShutdown();
1630
+ mercury._handleImminentShutdown(sessionId);
1298
1631
  await promiseTick(50);
1299
1632
 
1300
- // Check if failure event was emitted
1301
1633
  const emitCalls = mercury._emit.getCalls();
1302
1634
  const hasFailureEvent = emitCalls.some(
1303
1635
  (call) =>
1304
- call.args[0] === 'event:mercury_shutdown_switchover_failed' &&
1305
- call.args[1] &&
1306
- call.args[1].reason === testError
1636
+ call.args[0] === sessionId &&
1637
+ call.args[1] === 'event:mercury_shutdown_switchover_failed' &&
1638
+ call.args[2] &&
1639
+ call.args[2].reason === testError
1307
1640
  );
1308
1641
 
1309
1642
  assert.isTrue(hasFailureEvent, 'Should emit switchover failed event');
@@ -1312,10 +1645,9 @@ describe('plugin-mercury', () => {
1312
1645
  it('should allow old socket to be closed by server after switchover failure', async () => {
1313
1646
  connectWithBackoffStub.returns(Promise.reject(new Error('Failed')));
1314
1647
 
1315
- mercury._handleImminentShutdown();
1648
+ mercury._handleImminentShutdown(sessionId);
1316
1649
  await promiseTick(50);
1317
1650
 
1318
- // Old socket should not be closed immediately - server will close it
1319
1651
  assert.equal(mercury.socket.removeAllListeners.callCount, 0);
1320
1652
  });
1321
1653
  });
@@ -1399,6 +1731,16 @@ describe('plugin-mercury', () => {
1399
1731
  assert.equal(result, 'ws://example.com');
1400
1732
  });
1401
1733
 
1734
+ it('stores the resolved (pre-proxy) URL under the sessionId in sessionWebSocketUrls', async () => {
1735
+ const sessionId = 'llm-session';
1736
+
1737
+ await mercury._prepareAndOpenSocket(mockSocket, 'ws://original.com', sessionId);
1738
+
1739
+ // The stored value is the URL resolved by _prepareUrl, not the raw
1740
+ // input, so _onclose can later reconnect from a catalog-valid host.
1741
+ assert.equal(mercury.sessionWebSocketUrls.get(sessionId), 'ws://example.com');
1742
+ });
1743
+
1402
1744
  it('should handle errors during socket open', async () => {
1403
1745
  mockSocket.open.returns(Promise.reject(new Error('Open failed')));
1404
1746
 
@@ -1407,23 +1749,22 @@ describe('plugin-mercury', () => {
1407
1749
  assert.fail('Should have thrown an error');
1408
1750
  } catch (err) {
1409
1751
  assert.equal(err.message, 'Open failed');
1752
+ assert.equal(err.webSocketUrl, 'ws://example.com');
1410
1753
  }
1411
1754
  });
1412
1755
  });
1413
1756
 
1414
1757
  describe('#_attemptConnection() with shutdown switchover', () => {
1415
- let mockSocket, prepareAndOpenSocketStub, callback;
1758
+ let prepareAndOpenSocketStub, callback;
1759
+ const sessionId = 'mercury-default-session';
1416
1760
 
1417
1761
  beforeEach(() => {
1418
- mockSocket = {
1419
- url: 'ws://test.com',
1420
- };
1421
1762
  prepareAndOpenSocketStub = sinon
1422
1763
  .stub(mercury, '_prepareAndOpenSocket')
1423
1764
  .returns(Promise.resolve('ws://new-socket.com'));
1424
1765
  callback = sinon.stub();
1425
- mercury._shutdownSwitchoverBackoffCall = {}; // Mock backoff call
1426
- mercury.socket = mockSocket;
1766
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: sinon.stub()});
1767
+ mercury.socket = {url: 'ws://test.com'};
1427
1768
  mercury.connected = true;
1428
1769
  sinon.stub(mercury, '_emit');
1429
1770
  sinon.stub(mercury, '_attachSocketEventListeners');
@@ -1433,28 +1774,26 @@ describe('plugin-mercury', () => {
1433
1774
  prepareAndOpenSocketStub.restore();
1434
1775
  mercury._emit.restore();
1435
1776
  mercury._attachSocketEventListeners.restore();
1777
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1436
1778
  });
1437
1779
 
1438
1780
  it('should not set socket reference before opening for shutdown switchover', async () => {
1439
1781
  const originalSocket = mercury.socket;
1440
1782
 
1441
- await mercury._attemptConnection('ws://test.com', callback, {
1783
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1442
1784
  isShutdownSwitchover: true,
1443
1785
  onSuccess: (newSocket, url) => {
1444
- // During onSuccess, verify original socket is still set
1445
- // (socket swap happens inside onSuccess callback in _handleImminentShutdown)
1446
1786
  assert.equal(mercury.socket, originalSocket);
1447
1787
  },
1448
1788
  });
1449
1789
 
1450
- // After onSuccess, socket should still be original since we only swap in _handleImminentShutdown
1451
1790
  assert.equal(mercury.socket, originalSocket);
1452
1791
  });
1453
1792
 
1454
1793
  it('should call onSuccess callback with new socket and URL for shutdown', async () => {
1455
1794
  const onSuccessStub = sinon.stub();
1456
1795
 
1457
- await mercury._attemptConnection('ws://test.com', callback, {
1796
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1458
1797
  isShutdownSwitchover: true,
1459
1798
  onSuccess: onSuccessStub,
1460
1799
  });
@@ -1464,20 +1803,18 @@ describe('plugin-mercury', () => {
1464
1803
  });
1465
1804
 
1466
1805
  it('should emit shutdown switchover complete event', async () => {
1467
- const oldSocket = mercury.socket;
1468
-
1469
- await mercury._attemptConnection('ws://test.com', callback, {
1806
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1470
1807
  isShutdownSwitchover: true,
1471
1808
  onSuccess: (newSocket, url) => {
1472
- // Simulate the onSuccess callback behavior
1473
1809
  mercury.socket = newSocket;
1474
1810
  mercury.connected = true;
1475
- mercury._emit('event:mercury_shutdown_switchover_complete', {url});
1811
+ mercury._emit(sessionId, 'event:mercury_shutdown_switchover_complete', {url});
1476
1812
  },
1477
1813
  });
1478
1814
 
1479
1815
  assert.calledWith(
1480
1816
  mercury._emit,
1817
+ sessionId,
1481
1818
  'event:mercury_shutdown_switchover_complete',
1482
1819
  sinon.match.has('url', 'ws://new-socket.com')
1483
1820
  );
@@ -1486,23 +1823,20 @@ describe('plugin-mercury', () => {
1486
1823
  it('should use simpler error handling for shutdown switchover failures', async () => {
1487
1824
  prepareAndOpenSocketStub.returns(Promise.reject(new Error('Connection failed')));
1488
1825
 
1489
- try {
1490
- await mercury._attemptConnection('ws://test.com', callback, {
1826
+ await mercury
1827
+ ._attemptConnection('ws://test.com', sessionId, callback, {
1491
1828
  isShutdownSwitchover: true,
1492
- });
1493
- } catch (err) {
1494
- // Error should be caught and passed to callback
1495
- }
1829
+ })
1830
+ .catch(() => {});
1496
1831
 
1497
- // Should call callback with error for retry
1498
1832
  assert.calledOnce(callback);
1499
1833
  assert.instanceOf(callback.firstCall.args[0], Error);
1500
1834
  });
1501
1835
 
1502
1836
  it('should check _shutdownSwitchoverBackoffCall for shutdown connections', () => {
1503
- mercury._shutdownSwitchoverBackoffCall = undefined;
1837
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1504
1838
 
1505
- const result = mercury._attemptConnection('ws://test.com', callback, {
1839
+ const result = mercury._attemptConnection('ws://test.com', sessionId, callback, {
1506
1840
  isShutdownSwitchover: true,
1507
1841
  });
1508
1842
 
@@ -1514,35 +1848,28 @@ describe('plugin-mercury', () => {
1514
1848
  });
1515
1849
 
1516
1850
  describe('#_connectWithBackoff() with shutdown switchover', () => {
1517
- // Note: These tests verify the parameterization logic without running real backoff timers
1518
- // to avoid test hangs. The backoff mechanism itself is tested in other test suites.
1851
+ const sessionId = 'mercury-default-session';
1519
1852
 
1520
1853
  it('should use shutdown-specific parameters when called', () => {
1521
- // Stub _connectWithBackoff to prevent real execution
1522
1854
  const connectWithBackoffStub = sinon
1523
1855
  .stub(mercury, '_connectWithBackoff')
1524
1856
  .returns(Promise.resolve());
1525
1857
 
1526
- mercury._handleImminentShutdown();
1858
+ mercury._handleImminentShutdown(sessionId);
1527
1859
 
1528
- // Verify it was called with shutdown context
1529
1860
  assert.calledOnce(connectWithBackoffStub);
1530
1861
  const callArgs = connectWithBackoffStub.firstCall.args;
1531
- assert.isObject(callArgs[1]); // context
1532
- assert.isTrue(callArgs[1].isShutdownSwitchover);
1862
+ assert.equal(callArgs[1], sessionId);
1863
+ assert.isObject(callArgs[2]);
1864
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1533
1865
 
1534
1866
  connectWithBackoffStub.restore();
1535
1867
  });
1536
1868
 
1537
1869
  it('should pass shutdown switchover options to _attemptConnection', () => {
1538
- // Stub _attemptConnection to verify it receives correct options
1539
1870
  const attemptStub = sinon.stub(mercury, '_attemptConnection');
1540
- attemptStub.callsFake((url, callback) => {
1541
- // Immediately succeed
1542
- callback();
1543
- });
1871
+ attemptStub.callsFake((url, sid, cb) => cb());
1544
1872
 
1545
- // Call _connectWithBackoff with shutdown context
1546
1873
  const context = {
1547
1874
  isShutdownSwitchover: true,
1548
1875
  attemptOptions: {
@@ -1551,34 +1878,30 @@ describe('plugin-mercury', () => {
1551
1878
  },
1552
1879
  };
1553
1880
 
1554
- // Start the backoff
1555
- const promise = mercury._connectWithBackoff(undefined, context);
1881
+ const promise = mercury._connectWithBackoff(undefined, sessionId, context);
1556
1882
 
1557
- // Check that _attemptConnection was called with shutdown options
1558
1883
  return promise.then(() => {
1559
1884
  assert.calledOnce(attemptStub);
1560
1885
  const callArgs = attemptStub.firstCall.args;
1561
- assert.isObject(callArgs[2]); // options parameter
1562
- assert.isTrue(callArgs[2].isShutdownSwitchover);
1563
-
1886
+ assert.equal(callArgs[1], sessionId);
1887
+ assert.isObject(callArgs[3]);
1888
+ assert.isTrue(callArgs[3].isShutdownSwitchover);
1564
1889
  attemptStub.restore();
1565
1890
  });
1566
1891
  });
1567
1892
 
1568
1893
  it('should set and clear state flags appropriately', () => {
1569
- // Stub to prevent actual connection
1570
- sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
1894
+ sinon.stub(mercury, '_attemptConnection').callsFake((url, sid, cb) => cb());
1571
1895
 
1572
- mercury._shutdownSwitchoverInProgress = true;
1896
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1573
1897
 
1574
- const promise = mercury._connectWithBackoff(undefined, {
1898
+ const promise = mercury._connectWithBackoff(undefined, sessionId, {
1575
1899
  isShutdownSwitchover: true,
1576
1900
  attemptOptions: {isShutdownSwitchover: true, onSuccess: () => {}},
1577
1901
  });
1578
1902
 
1579
1903
  return promise.then(() => {
1580
- // Should be cleared after completion
1581
- assert.isFalse(mercury._shutdownSwitchoverInProgress);
1904
+ assert.isUndefined(mercury._shutdownSwitchoverBackoffCalls.get(sessionId));
1582
1905
  mercury._attemptConnection.restore();
1583
1906
  });
1584
1907
  });
@@ -1586,32 +1909,30 @@ describe('plugin-mercury', () => {
1586
1909
 
1587
1910
  describe('#disconnect() with shutdown switchover in progress', () => {
1588
1911
  let abortStub;
1912
+ const sessionId = 'mercury-default-session';
1589
1913
 
1590
1914
  beforeEach(() => {
1591
- mercury.socket = {
1915
+ mercury.sockets.clear();
1916
+ mercury.sockets.set(sessionId, {
1592
1917
  close: sinon.stub().returns(Promise.resolve()),
1593
1918
  removeAllListeners: sinon.stub(),
1594
- };
1919
+ });
1595
1920
  abortStub = sinon.stub();
1596
- mercury._shutdownSwitchoverBackoffCall = {
1597
- abort: abortStub,
1598
- };
1921
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: abortStub});
1599
1922
  });
1600
1923
 
1601
1924
  it('should abort shutdown switchover backoff call on disconnect', async () => {
1602
- await mercury.disconnect();
1925
+ await mercury.disconnect(undefined, sessionId);
1603
1926
 
1604
1927
  assert.calledOnce(abortStub);
1605
1928
  });
1606
1929
 
1607
1930
  it('should handle disconnect when no switchover is in progress', async () => {
1608
- mercury._shutdownSwitchoverBackoffCall = undefined;
1931
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1609
1932
 
1610
- // Should not throw
1611
- await mercury.disconnect();
1933
+ await mercury.disconnect(undefined, sessionId);
1612
1934
 
1613
- // Should still close the socket
1614
- assert.calledOnce(mercury.socket.close);
1935
+ assert.calledOnce(mercury.sockets.get(sessionId).close);
1615
1936
  });
1616
1937
  });
1617
1938
  });