@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.
- package/README.md +54 -0
- package/dist/mercury.js +425 -201
- 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 +439 -175
- package/src/socket/constants.js +6 -0
- package/src/socket/socket-base.js +40 -3
- package/test/unit/spec/mercury-events.js +29 -3
- package/test/unit/spec/mercury.js +474 -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,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(
|
|
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(
|
|
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(
|
|
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.
|
|
1178
|
-
|
|
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(
|
|
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.
|
|
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
|
|
1252
|
-
assert.
|
|
1253
|
-
assert.
|
|
1254
|
-
assert.
|
|
1255
|
-
assert.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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] ===
|
|
1305
|
-
call.args[1] &&
|
|
1306
|
-
call.args[
|
|
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
|
|
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.
|
|
1426
|
-
mercury.socket =
|
|
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
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1826
|
+
await mercury
|
|
1827
|
+
._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1491
1828
|
isShutdownSwitchover: true,
|
|
1492
|
-
})
|
|
1493
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1532
|
-
assert.
|
|
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,
|
|
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
|
-
|
|
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.
|
|
1562
|
-
assert.
|
|
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
|
-
|
|
1570
|
-
sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
|
|
1894
|
+
sinon.stub(mercury, '_attemptConnection').callsFake((url, sid, cb) => cb());
|
|
1571
1895
|
|
|
1572
|
-
mercury.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1931
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1609
1932
|
|
|
1610
|
-
|
|
1611
|
-
await mercury.disconnect();
|
|
1933
|
+
await mercury.disconnect(undefined, sessionId);
|
|
1612
1934
|
|
|
1613
|
-
|
|
1614
|
-
assert.calledOnce(mercury.socket.close);
|
|
1935
|
+
assert.calledOnce(mercury.sockets.get(sessionId).close);
|
|
1615
1936
|
});
|
|
1616
1937
|
});
|
|
1617
1938
|
});
|