@webex/internal-plugin-mercury 3.11.0 → 3.12.0-auth-prejoin-fetch.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.
- package/README.md +54 -0
- package/dist/mercury.js +467 -202
- package/dist/mercury.js.map +1 -1
- package/dist/socket/constants.js +16 -0
- package/dist/socket/constants.js.map +1 -0
- package/dist/socket/socket-base.js +36 -3
- package/dist/socket/socket-base.js.map +1 -1
- package/package.json +17 -17
- package/src/mercury.js +483 -176
- package/src/socket/constants.js +6 -0
- package/src/socket/socket-base.js +40 -3
- package/test/unit/spec/mercury-events.js +35 -3
- package/test/unit/spec/mercury.js +597 -153
- package/test/unit/spec/socket.js +61 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
358
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
654
|
+
it('calls disconnectAll and logs', () => {
|
|
588
655
|
sinon.stub(mercury.logger, 'info');
|
|
589
|
-
sinon.stub(mercury, '
|
|
656
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
590
657
|
mercury.logout();
|
|
591
|
-
assert.called(mercury.
|
|
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, '
|
|
670
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
604
671
|
mercury.config.beforeLogoutOptionsCloseReason = 'done (permanent)';
|
|
605
672
|
mercury.logout();
|
|
606
|
-
assert.calledWith(mercury.
|
|
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, '
|
|
677
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
611
678
|
mercury.config.beforeLogoutOptionsCloseReason = 'test';
|
|
612
679
|
mercury.logout();
|
|
613
|
-
assert.calledWith(mercury.
|
|
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, '
|
|
684
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
618
685
|
mercury.config.beforeLogoutOptionsCloseReason = 'done (forced)';
|
|
619
686
|
mercury.logout();
|
|
620
|
-
assert.calledWith(mercury.
|
|
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(
|
|
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(
|
|
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.
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1041
|
-
assert.
|
|
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
|
-
|
|
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(
|
|
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,251 @@ 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
|
+
|
|
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
|
+
|
|
1111
1460
|
describe('#_onclose() with code 4001 (shutdown replacement)', () => {
|
|
1112
1461
|
let mockSocket, anotherSocket;
|
|
1113
1462
|
|
|
@@ -1121,6 +1470,7 @@ describe('plugin-mercury', () => {
|
|
|
1121
1470
|
removeAllListeners: sinon.stub(),
|
|
1122
1471
|
};
|
|
1123
1472
|
mercury.socket = mockSocket;
|
|
1473
|
+
mercury.sockets.set(mercury.defaultSessionId, mockSocket);
|
|
1124
1474
|
mercury.connected = true;
|
|
1125
1475
|
sinon.stub(mercury, '_emit');
|
|
1126
1476
|
sinon.stub(mercury, '_reconnect');
|
|
@@ -1139,9 +1489,14 @@ describe('plugin-mercury', () => {
|
|
|
1139
1489
|
reason: 'replaced during shutdown',
|
|
1140
1490
|
};
|
|
1141
1491
|
|
|
1142
|
-
mercury._onclose(closeEvent, mockSocket);
|
|
1492
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1143
1493
|
|
|
1144
|
-
assert.calledWith(
|
|
1494
|
+
assert.calledWith(
|
|
1495
|
+
mercury._emit,
|
|
1496
|
+
mercury.defaultSessionId,
|
|
1497
|
+
'offline.permanent',
|
|
1498
|
+
closeEvent
|
|
1499
|
+
);
|
|
1145
1500
|
assert.notCalled(mercury._reconnect); // No reconnect for 4001 on active socket
|
|
1146
1501
|
assert.isFalse(mercury.connected);
|
|
1147
1502
|
});
|
|
@@ -1152,9 +1507,14 @@ describe('plugin-mercury', () => {
|
|
|
1152
1507
|
reason: 'replaced during shutdown',
|
|
1153
1508
|
};
|
|
1154
1509
|
|
|
1155
|
-
mercury._onclose(closeEvent, anotherSocket);
|
|
1510
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1156
1511
|
|
|
1157
|
-
assert.calledWith(
|
|
1512
|
+
assert.calledWith(
|
|
1513
|
+
mercury._emit,
|
|
1514
|
+
mercury.defaultSessionId,
|
|
1515
|
+
'offline.replaced',
|
|
1516
|
+
closeEvent
|
|
1517
|
+
);
|
|
1158
1518
|
assert.notCalled(mercury._reconnect);
|
|
1159
1519
|
assert.isTrue(mercury.connected); // Should remain connected
|
|
1160
1520
|
assert.notCalled(mercury.unset);
|
|
@@ -1167,15 +1527,26 @@ describe('plugin-mercury', () => {
|
|
|
1167
1527
|
};
|
|
1168
1528
|
|
|
1169
1529
|
// Test non-active socket
|
|
1170
|
-
mercury._onclose(closeEvent, anotherSocket);
|
|
1171
|
-
assert.calledWith(
|
|
1530
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1531
|
+
assert.calledWith(
|
|
1532
|
+
mercury._emit,
|
|
1533
|
+
mercury.defaultSessionId,
|
|
1534
|
+
'offline.replaced',
|
|
1535
|
+
closeEvent
|
|
1536
|
+
);
|
|
1172
1537
|
|
|
1173
1538
|
// Reset the spy call history
|
|
1174
1539
|
mercury._emit.resetHistory();
|
|
1175
1540
|
|
|
1176
1541
|
// Test active socket
|
|
1177
|
-
mercury.
|
|
1178
|
-
|
|
1542
|
+
mercury.sockets.set(mercury.defaultSessionId, mockSocket);
|
|
1543
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1544
|
+
assert.calledWith(
|
|
1545
|
+
mercury._emit,
|
|
1546
|
+
mercury.defaultSessionId,
|
|
1547
|
+
'offline.permanent',
|
|
1548
|
+
closeEvent
|
|
1549
|
+
);
|
|
1179
1550
|
});
|
|
1180
1551
|
|
|
1181
1552
|
it('should handle missing sourceSocket parameter (treats as non-active)', () => {
|
|
@@ -1184,10 +1555,15 @@ describe('plugin-mercury', () => {
|
|
|
1184
1555
|
reason: 'replaced during shutdown',
|
|
1185
1556
|
};
|
|
1186
1557
|
|
|
1187
|
-
mercury._onclose(closeEvent); // No sourceSocket parameter
|
|
1558
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent); // No sourceSocket parameter
|
|
1188
1559
|
|
|
1189
1560
|
// With simplified logic, undefined !== this.socket, so isActiveSocket = false
|
|
1190
|
-
assert.calledWith(
|
|
1561
|
+
assert.calledWith(
|
|
1562
|
+
mercury._emit,
|
|
1563
|
+
mercury.defaultSessionId,
|
|
1564
|
+
'offline.replaced',
|
|
1565
|
+
closeEvent
|
|
1566
|
+
);
|
|
1191
1567
|
assert.notCalled(mercury._reconnect);
|
|
1192
1568
|
});
|
|
1193
1569
|
|
|
@@ -1198,7 +1574,7 @@ describe('plugin-mercury', () => {
|
|
|
1198
1574
|
};
|
|
1199
1575
|
|
|
1200
1576
|
// Close non-active socket (not the active one)
|
|
1201
|
-
mercury._onclose(closeEvent, anotherSocket);
|
|
1577
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1202
1578
|
|
|
1203
1579
|
// Verify listeners were removed from the old socket
|
|
1204
1580
|
// The _onclose method checks if sourceSocket !== this.socket (non-active)
|
|
@@ -1213,22 +1589,94 @@ describe('plugin-mercury', () => {
|
|
|
1213
1589
|
};
|
|
1214
1590
|
|
|
1215
1591
|
// Close active socket
|
|
1216
|
-
mercury._onclose(closeEvent, mockSocket);
|
|
1592
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1217
1593
|
|
|
1218
1594
|
// Verify listeners were removed from active socket
|
|
1219
1595
|
assert.calledOnce(mockSocket.removeAllListeners);
|
|
1220
1596
|
});
|
|
1221
1597
|
});
|
|
1222
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
|
+
|
|
1223
1669
|
describe('shutdown switchover with retry logic', () => {
|
|
1224
1670
|
let connectWithBackoffStub;
|
|
1671
|
+
const sessionId = 'mercury-default-session';
|
|
1225
1672
|
|
|
1226
1673
|
beforeEach(() => {
|
|
1227
1674
|
mercury.connected = true;
|
|
1228
|
-
mercury.
|
|
1675
|
+
mercury.sockets.set(sessionId, {
|
|
1229
1676
|
url: 'ws://old-socket.com',
|
|
1230
1677
|
removeAllListeners: sinon.stub(),
|
|
1231
|
-
};
|
|
1678
|
+
});
|
|
1679
|
+
mercury.socket = mercury.sockets.get(sessionId);
|
|
1232
1680
|
connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
|
|
1233
1681
|
sinon.stub(mercury, '_emit');
|
|
1234
1682
|
});
|
|
@@ -1236,39 +1684,46 @@ describe('plugin-mercury', () => {
|
|
|
1236
1684
|
afterEach(() => {
|
|
1237
1685
|
connectWithBackoffStub.restore();
|
|
1238
1686
|
mercury._emit.restore();
|
|
1687
|
+
mercury.sockets.clear();
|
|
1239
1688
|
});
|
|
1240
1689
|
|
|
1241
1690
|
it('should call _connectWithBackoff with shutdown switchover context', (done) => {
|
|
1242
1691
|
connectWithBackoffStub.returns(Promise.resolve());
|
|
1243
1692
|
|
|
1244
|
-
mercury._handleImminentShutdown();
|
|
1693
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1245
1694
|
|
|
1246
|
-
// Give it a tick for the async call to happen
|
|
1247
1695
|
process.nextTick(() => {
|
|
1248
1696
|
assert.calledOnce(connectWithBackoffStub);
|
|
1249
1697
|
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1250
1698
|
|
|
1251
|
-
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1252
|
-
assert.
|
|
1253
|
-
assert.
|
|
1254
|
-
assert.
|
|
1255
|
-
assert.
|
|
1699
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1700
|
+
assert.equal(callArgs[1], sessionId);
|
|
1701
|
+
assert.isObject(callArgs[2]);
|
|
1702
|
+
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1703
|
+
assert.isObject(callArgs[2].attemptOptions);
|
|
1704
|
+
assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
|
|
1256
1705
|
done();
|
|
1257
1706
|
});
|
|
1258
1707
|
});
|
|
1259
1708
|
|
|
1260
1709
|
it('should set _shutdownSwitchoverInProgress flag during switchover', () => {
|
|
1261
|
-
|
|
1710
|
+
// With the new behavior, "in progress" is represented by the presence
|
|
1711
|
+
// of an entry in _shutdownSwitchoverBackoffCalls.
|
|
1712
|
+
// Since _connectWithBackoff is stubbed in this suite, simulate its side-effect
|
|
1713
|
+
// of seeding the backoff-call map entry.
|
|
1714
|
+
connectWithBackoffStub.callsFake(() => {
|
|
1715
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
|
|
1716
|
+
return new Promise(() => {}); // Never resolves
|
|
1717
|
+
});
|
|
1262
1718
|
|
|
1263
|
-
mercury._handleImminentShutdown();
|
|
1719
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1264
1720
|
|
|
1265
|
-
|
|
1721
|
+
const switchoverBackoffCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
|
|
1722
|
+
assert.isOk(switchoverBackoffCall);
|
|
1266
1723
|
});
|
|
1267
1724
|
|
|
1268
1725
|
it('should emit success event when switchover completes', async () => {
|
|
1269
|
-
|
|
1270
|
-
connectWithBackoffStub.callsFake((url, context) => {
|
|
1271
|
-
// Simulate successful connection by calling onSuccess
|
|
1726
|
+
connectWithBackoffStub.callsFake((url, sid, context) => {
|
|
1272
1727
|
if (context && context.attemptOptions && context.attemptOptions.onSuccess) {
|
|
1273
1728
|
const mockSocket = {url: 'ws://new-socket.com'};
|
|
1274
1729
|
context.attemptOptions.onSuccess(mockSocket, 'ws://new-socket.com');
|
|
@@ -1276,14 +1731,15 @@ describe('plugin-mercury', () => {
|
|
|
1276
1731
|
return Promise.resolve();
|
|
1277
1732
|
});
|
|
1278
1733
|
|
|
1279
|
-
mercury._handleImminentShutdown();
|
|
1734
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1280
1735
|
|
|
1281
|
-
// Wait for async operations
|
|
1282
1736
|
await promiseTick(50);
|
|
1283
1737
|
|
|
1284
1738
|
const emitCalls = mercury._emit.getCalls();
|
|
1285
1739
|
const hasCompleteEvent = emitCalls.some(
|
|
1286
|
-
(call) =>
|
|
1740
|
+
(call) =>
|
|
1741
|
+
call.args[0] === sessionId &&
|
|
1742
|
+
call.args[1] === 'event:mercury_shutdown_switchover_complete'
|
|
1287
1743
|
);
|
|
1288
1744
|
|
|
1289
1745
|
assert.isTrue(hasCompleteEvent, 'Should emit switchover complete event');
|
|
@@ -1294,16 +1750,16 @@ describe('plugin-mercury', () => {
|
|
|
1294
1750
|
|
|
1295
1751
|
connectWithBackoffStub.returns(Promise.reject(testError));
|
|
1296
1752
|
|
|
1297
|
-
mercury._handleImminentShutdown();
|
|
1753
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1298
1754
|
await promiseTick(50);
|
|
1299
1755
|
|
|
1300
|
-
// Check if failure event was emitted
|
|
1301
1756
|
const emitCalls = mercury._emit.getCalls();
|
|
1302
1757
|
const hasFailureEvent = emitCalls.some(
|
|
1303
1758
|
(call) =>
|
|
1304
|
-
call.args[0] ===
|
|
1305
|
-
call.args[1] &&
|
|
1306
|
-
call.args[
|
|
1759
|
+
call.args[0] === sessionId &&
|
|
1760
|
+
call.args[1] === 'event:mercury_shutdown_switchover_failed' &&
|
|
1761
|
+
call.args[2] &&
|
|
1762
|
+
call.args[2].reason === testError
|
|
1307
1763
|
);
|
|
1308
1764
|
|
|
1309
1765
|
assert.isTrue(hasFailureEvent, 'Should emit switchover failed event');
|
|
@@ -1312,10 +1768,9 @@ describe('plugin-mercury', () => {
|
|
|
1312
1768
|
it('should allow old socket to be closed by server after switchover failure', async () => {
|
|
1313
1769
|
connectWithBackoffStub.returns(Promise.reject(new Error('Failed')));
|
|
1314
1770
|
|
|
1315
|
-
mercury._handleImminentShutdown();
|
|
1771
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1316
1772
|
await promiseTick(50);
|
|
1317
1773
|
|
|
1318
|
-
// Old socket should not be closed immediately - server will close it
|
|
1319
1774
|
assert.equal(mercury.socket.removeAllListeners.callCount, 0);
|
|
1320
1775
|
});
|
|
1321
1776
|
});
|
|
@@ -1399,6 +1854,16 @@ describe('plugin-mercury', () => {
|
|
|
1399
1854
|
assert.equal(result, 'ws://example.com');
|
|
1400
1855
|
});
|
|
1401
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
|
+
|
|
1402
1867
|
it('should handle errors during socket open', async () => {
|
|
1403
1868
|
mockSocket.open.returns(Promise.reject(new Error('Open failed')));
|
|
1404
1869
|
|
|
@@ -1407,23 +1872,22 @@ describe('plugin-mercury', () => {
|
|
|
1407
1872
|
assert.fail('Should have thrown an error');
|
|
1408
1873
|
} catch (err) {
|
|
1409
1874
|
assert.equal(err.message, 'Open failed');
|
|
1875
|
+
assert.equal(err.webSocketUrl, 'ws://example.com');
|
|
1410
1876
|
}
|
|
1411
1877
|
});
|
|
1412
1878
|
});
|
|
1413
1879
|
|
|
1414
1880
|
describe('#_attemptConnection() with shutdown switchover', () => {
|
|
1415
|
-
let
|
|
1881
|
+
let prepareAndOpenSocketStub, callback;
|
|
1882
|
+
const sessionId = 'mercury-default-session';
|
|
1416
1883
|
|
|
1417
1884
|
beforeEach(() => {
|
|
1418
|
-
mockSocket = {
|
|
1419
|
-
url: 'ws://test.com',
|
|
1420
|
-
};
|
|
1421
1885
|
prepareAndOpenSocketStub = sinon
|
|
1422
1886
|
.stub(mercury, '_prepareAndOpenSocket')
|
|
1423
1887
|
.returns(Promise.resolve('ws://new-socket.com'));
|
|
1424
1888
|
callback = sinon.stub();
|
|
1425
|
-
mercury.
|
|
1426
|
-
mercury.socket =
|
|
1889
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: sinon.stub()});
|
|
1890
|
+
mercury.socket = {url: 'ws://test.com'};
|
|
1427
1891
|
mercury.connected = true;
|
|
1428
1892
|
sinon.stub(mercury, '_emit');
|
|
1429
1893
|
sinon.stub(mercury, '_attachSocketEventListeners');
|
|
@@ -1433,28 +1897,26 @@ describe('plugin-mercury', () => {
|
|
|
1433
1897
|
prepareAndOpenSocketStub.restore();
|
|
1434
1898
|
mercury._emit.restore();
|
|
1435
1899
|
mercury._attachSocketEventListeners.restore();
|
|
1900
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1436
1901
|
});
|
|
1437
1902
|
|
|
1438
1903
|
it('should not set socket reference before opening for shutdown switchover', async () => {
|
|
1439
1904
|
const originalSocket = mercury.socket;
|
|
1440
1905
|
|
|
1441
|
-
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1906
|
+
await mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1442
1907
|
isShutdownSwitchover: true,
|
|
1443
1908
|
onSuccess: (newSocket, url) => {
|
|
1444
|
-
// During onSuccess, verify original socket is still set
|
|
1445
|
-
// (socket swap happens inside onSuccess callback in _handleImminentShutdown)
|
|
1446
1909
|
assert.equal(mercury.socket, originalSocket);
|
|
1447
1910
|
},
|
|
1448
1911
|
});
|
|
1449
1912
|
|
|
1450
|
-
// After onSuccess, socket should still be original since we only swap in _handleImminentShutdown
|
|
1451
1913
|
assert.equal(mercury.socket, originalSocket);
|
|
1452
1914
|
});
|
|
1453
1915
|
|
|
1454
1916
|
it('should call onSuccess callback with new socket and URL for shutdown', async () => {
|
|
1455
1917
|
const onSuccessStub = sinon.stub();
|
|
1456
1918
|
|
|
1457
|
-
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1919
|
+
await mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1458
1920
|
isShutdownSwitchover: true,
|
|
1459
1921
|
onSuccess: onSuccessStub,
|
|
1460
1922
|
});
|
|
@@ -1464,20 +1926,18 @@ describe('plugin-mercury', () => {
|
|
|
1464
1926
|
});
|
|
1465
1927
|
|
|
1466
1928
|
it('should emit shutdown switchover complete event', async () => {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1929
|
+
await mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1470
1930
|
isShutdownSwitchover: true,
|
|
1471
1931
|
onSuccess: (newSocket, url) => {
|
|
1472
|
-
// Simulate the onSuccess callback behavior
|
|
1473
1932
|
mercury.socket = newSocket;
|
|
1474
1933
|
mercury.connected = true;
|
|
1475
|
-
mercury._emit('event:mercury_shutdown_switchover_complete', {url});
|
|
1934
|
+
mercury._emit(sessionId, 'event:mercury_shutdown_switchover_complete', {url});
|
|
1476
1935
|
},
|
|
1477
1936
|
});
|
|
1478
1937
|
|
|
1479
1938
|
assert.calledWith(
|
|
1480
1939
|
mercury._emit,
|
|
1940
|
+
sessionId,
|
|
1481
1941
|
'event:mercury_shutdown_switchover_complete',
|
|
1482
1942
|
sinon.match.has('url', 'ws://new-socket.com')
|
|
1483
1943
|
);
|
|
@@ -1486,23 +1946,20 @@ describe('plugin-mercury', () => {
|
|
|
1486
1946
|
it('should use simpler error handling for shutdown switchover failures', async () => {
|
|
1487
1947
|
prepareAndOpenSocketStub.returns(Promise.reject(new Error('Connection failed')));
|
|
1488
1948
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1949
|
+
await mercury
|
|
1950
|
+
._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1491
1951
|
isShutdownSwitchover: true,
|
|
1492
|
-
})
|
|
1493
|
-
|
|
1494
|
-
// Error should be caught and passed to callback
|
|
1495
|
-
}
|
|
1952
|
+
})
|
|
1953
|
+
.catch(() => {});
|
|
1496
1954
|
|
|
1497
|
-
// Should call callback with error for retry
|
|
1498
1955
|
assert.calledOnce(callback);
|
|
1499
1956
|
assert.instanceOf(callback.firstCall.args[0], Error);
|
|
1500
1957
|
});
|
|
1501
1958
|
|
|
1502
1959
|
it('should check _shutdownSwitchoverBackoffCall for shutdown connections', () => {
|
|
1503
|
-
mercury.
|
|
1960
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1504
1961
|
|
|
1505
|
-
const result = mercury._attemptConnection('ws://test.com', callback, {
|
|
1962
|
+
const result = mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1506
1963
|
isShutdownSwitchover: true,
|
|
1507
1964
|
});
|
|
1508
1965
|
|
|
@@ -1514,35 +1971,28 @@ describe('plugin-mercury', () => {
|
|
|
1514
1971
|
});
|
|
1515
1972
|
|
|
1516
1973
|
describe('#_connectWithBackoff() with shutdown switchover', () => {
|
|
1517
|
-
|
|
1518
|
-
// to avoid test hangs. The backoff mechanism itself is tested in other test suites.
|
|
1974
|
+
const sessionId = 'mercury-default-session';
|
|
1519
1975
|
|
|
1520
1976
|
it('should use shutdown-specific parameters when called', () => {
|
|
1521
|
-
// Stub _connectWithBackoff to prevent real execution
|
|
1522
1977
|
const connectWithBackoffStub = sinon
|
|
1523
1978
|
.stub(mercury, '_connectWithBackoff')
|
|
1524
1979
|
.returns(Promise.resolve());
|
|
1525
1980
|
|
|
1526
|
-
mercury._handleImminentShutdown();
|
|
1981
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1527
1982
|
|
|
1528
|
-
// Verify it was called with shutdown context
|
|
1529
1983
|
assert.calledOnce(connectWithBackoffStub);
|
|
1530
1984
|
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1531
|
-
assert.
|
|
1532
|
-
assert.
|
|
1985
|
+
assert.equal(callArgs[1], sessionId);
|
|
1986
|
+
assert.isObject(callArgs[2]);
|
|
1987
|
+
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1533
1988
|
|
|
1534
1989
|
connectWithBackoffStub.restore();
|
|
1535
1990
|
});
|
|
1536
1991
|
|
|
1537
1992
|
it('should pass shutdown switchover options to _attemptConnection', () => {
|
|
1538
|
-
// Stub _attemptConnection to verify it receives correct options
|
|
1539
1993
|
const attemptStub = sinon.stub(mercury, '_attemptConnection');
|
|
1540
|
-
attemptStub.callsFake((url,
|
|
1541
|
-
// Immediately succeed
|
|
1542
|
-
callback();
|
|
1543
|
-
});
|
|
1994
|
+
attemptStub.callsFake((url, sid, cb) => cb());
|
|
1544
1995
|
|
|
1545
|
-
// Call _connectWithBackoff with shutdown context
|
|
1546
1996
|
const context = {
|
|
1547
1997
|
isShutdownSwitchover: true,
|
|
1548
1998
|
attemptOptions: {
|
|
@@ -1551,34 +2001,30 @@ describe('plugin-mercury', () => {
|
|
|
1551
2001
|
},
|
|
1552
2002
|
};
|
|
1553
2003
|
|
|
1554
|
-
|
|
1555
|
-
const promise = mercury._connectWithBackoff(undefined, context);
|
|
2004
|
+
const promise = mercury._connectWithBackoff(undefined, sessionId, context);
|
|
1556
2005
|
|
|
1557
|
-
// Check that _attemptConnection was called with shutdown options
|
|
1558
2006
|
return promise.then(() => {
|
|
1559
2007
|
assert.calledOnce(attemptStub);
|
|
1560
2008
|
const callArgs = attemptStub.firstCall.args;
|
|
1561
|
-
assert.
|
|
1562
|
-
assert.
|
|
1563
|
-
|
|
2009
|
+
assert.equal(callArgs[1], sessionId);
|
|
2010
|
+
assert.isObject(callArgs[3]);
|
|
2011
|
+
assert.isTrue(callArgs[3].isShutdownSwitchover);
|
|
1564
2012
|
attemptStub.restore();
|
|
1565
2013
|
});
|
|
1566
2014
|
});
|
|
1567
2015
|
|
|
1568
2016
|
it('should set and clear state flags appropriately', () => {
|
|
1569
|
-
|
|
1570
|
-
sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
|
|
2017
|
+
sinon.stub(mercury, '_attemptConnection').callsFake((url, sid, cb) => cb());
|
|
1571
2018
|
|
|
1572
|
-
mercury.
|
|
2019
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
|
|
1573
2020
|
|
|
1574
|
-
const promise = mercury._connectWithBackoff(undefined, {
|
|
2021
|
+
const promise = mercury._connectWithBackoff(undefined, sessionId, {
|
|
1575
2022
|
isShutdownSwitchover: true,
|
|
1576
2023
|
attemptOptions: {isShutdownSwitchover: true, onSuccess: () => {}},
|
|
1577
2024
|
});
|
|
1578
2025
|
|
|
1579
2026
|
return promise.then(() => {
|
|
1580
|
-
|
|
1581
|
-
assert.isFalse(mercury._shutdownSwitchoverInProgress);
|
|
2027
|
+
assert.isUndefined(mercury._shutdownSwitchoverBackoffCalls.get(sessionId));
|
|
1582
2028
|
mercury._attemptConnection.restore();
|
|
1583
2029
|
});
|
|
1584
2030
|
});
|
|
@@ -1586,32 +2032,30 @@ describe('plugin-mercury', () => {
|
|
|
1586
2032
|
|
|
1587
2033
|
describe('#disconnect() with shutdown switchover in progress', () => {
|
|
1588
2034
|
let abortStub;
|
|
2035
|
+
const sessionId = 'mercury-default-session';
|
|
1589
2036
|
|
|
1590
2037
|
beforeEach(() => {
|
|
1591
|
-
mercury.
|
|
2038
|
+
mercury.sockets.clear();
|
|
2039
|
+
mercury.sockets.set(sessionId, {
|
|
1592
2040
|
close: sinon.stub().returns(Promise.resolve()),
|
|
1593
2041
|
removeAllListeners: sinon.stub(),
|
|
1594
|
-
};
|
|
2042
|
+
});
|
|
1595
2043
|
abortStub = sinon.stub();
|
|
1596
|
-
mercury.
|
|
1597
|
-
abort: abortStub,
|
|
1598
|
-
};
|
|
2044
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: abortStub});
|
|
1599
2045
|
});
|
|
1600
2046
|
|
|
1601
2047
|
it('should abort shutdown switchover backoff call on disconnect', async () => {
|
|
1602
|
-
await mercury.disconnect();
|
|
2048
|
+
await mercury.disconnect(undefined, sessionId);
|
|
1603
2049
|
|
|
1604
2050
|
assert.calledOnce(abortStub);
|
|
1605
2051
|
});
|
|
1606
2052
|
|
|
1607
2053
|
it('should handle disconnect when no switchover is in progress', async () => {
|
|
1608
|
-
mercury.
|
|
2054
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1609
2055
|
|
|
1610
|
-
|
|
1611
|
-
await mercury.disconnect();
|
|
2056
|
+
await mercury.disconnect(undefined, sessionId);
|
|
1612
2057
|
|
|
1613
|
-
|
|
1614
|
-
assert.calledOnce(mercury.socket.close);
|
|
2058
|
+
assert.calledOnce(mercury.sockets.get(sessionId).close);
|
|
1615
2059
|
});
|
|
1616
2060
|
});
|
|
1617
2061
|
});
|