@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.61

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.
Files changed (158) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +6 -3
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +10 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +716 -370
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +22 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/locusRetry.js +23 -8
  27. package/dist/interceptors/locusRetry.js.map +1 -1
  28. package/dist/interpretation/index.js +10 -1
  29. package/dist/interpretation/index.js.map +1 -1
  30. package/dist/interpretation/siLanguage.js +1 -1
  31. package/dist/locus-info/controlsUtils.js +4 -1
  32. package/dist/locus-info/controlsUtils.js.map +1 -1
  33. package/dist/locus-info/index.js +289 -87
  34. package/dist/locus-info/index.js.map +1 -1
  35. package/dist/locus-info/types.js +19 -0
  36. package/dist/locus-info/types.js.map +1 -1
  37. package/dist/media/index.js +3 -1
  38. package/dist/media/index.js.map +1 -1
  39. package/dist/media/properties.js +1 -0
  40. package/dist/media/properties.js.map +1 -1
  41. package/dist/meeting/in-meeting-actions.js +3 -1
  42. package/dist/meeting/in-meeting-actions.js.map +1 -1
  43. package/dist/meeting/index.js +907 -535
  44. package/dist/meeting/index.js.map +1 -1
  45. package/dist/meeting/util.js +19 -2
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meetings/index.js +231 -78
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +6 -1
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/request.js +39 -0
  52. package/dist/meetings/request.js.map +1 -1
  53. package/dist/meetings/util.js +79 -5
  54. package/dist/meetings/util.js.map +1 -1
  55. package/dist/member/index.js +10 -0
  56. package/dist/member/index.js.map +1 -1
  57. package/dist/member/types.js.map +1 -1
  58. package/dist/member/util.js +3 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +4 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/codec/constants.js +63 -0
  63. package/dist/multistream/codec/constants.js.map +1 -0
  64. package/dist/multistream/mediaRequestManager.js +62 -15
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/receiveSlot.js +9 -0
  67. package/dist/multistream/receiveSlot.js.map +1 -1
  68. package/dist/reactions/reactions.type.js.map +1 -1
  69. package/dist/recording-controller/index.js +1 -3
  70. package/dist/recording-controller/index.js.map +1 -1
  71. package/dist/types/config.d.ts +2 -0
  72. package/dist/types/constants.d.ts +2 -0
  73. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  74. package/dist/types/controls-options-manager/index.d.ts +10 -0
  75. package/dist/types/hashTree/constants.d.ts +1 -0
  76. package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
  77. package/dist/types/hashTree/utils.d.ts +11 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  80. package/dist/types/locus-info/index.d.ts +46 -6
  81. package/dist/types/locus-info/types.d.ts +21 -1
  82. package/dist/types/media/properties.d.ts +1 -0
  83. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  84. package/dist/types/meeting/index.d.ts +87 -3
  85. package/dist/types/meeting/util.d.ts +8 -0
  86. package/dist/types/meetings/index.d.ts +30 -2
  87. package/dist/types/meetings/meetings.types.d.ts +15 -0
  88. package/dist/types/meetings/request.d.ts +14 -0
  89. package/dist/types/member/index.d.ts +1 -0
  90. package/dist/types/member/types.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +1 -0
  92. package/dist/types/metrics/constants.d.ts +3 -0
  93. package/dist/types/multistream/codec/constants.d.ts +7 -0
  94. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  95. package/dist/types/reactions/reactions.type.d.ts +3 -0
  96. package/dist/webinar/index.js +361 -235
  97. package/dist/webinar/index.js.map +1 -1
  98. package/package.json +22 -22
  99. package/src/aiEnableRequest/index.ts +16 -0
  100. package/src/breakouts/breakout.ts +3 -1
  101. package/src/breakouts/index.ts +31 -0
  102. package/src/config.ts +2 -0
  103. package/src/constants.ts +5 -1
  104. package/src/controls-options-manager/constants.ts +14 -1
  105. package/src/controls-options-manager/index.ts +47 -24
  106. package/src/controls-options-manager/util.ts +81 -1
  107. package/src/hashTree/constants.ts +9 -0
  108. package/src/hashTree/hashTreeParser.ts +429 -183
  109. package/src/hashTree/utils.ts +17 -0
  110. package/src/index.ts +5 -0
  111. package/src/interceptors/locusRetry.ts +25 -4
  112. package/src/interpretation/index.ts +25 -8
  113. package/src/locus-info/controlsUtils.ts +3 -1
  114. package/src/locus-info/index.ts +291 -97
  115. package/src/locus-info/types.ts +25 -1
  116. package/src/media/index.ts +3 -0
  117. package/src/media/properties.ts +1 -0
  118. package/src/meeting/in-meeting-actions.ts +4 -0
  119. package/src/meeting/index.ts +388 -33
  120. package/src/meeting/util.ts +20 -2
  121. package/src/meetings/index.ts +134 -44
  122. package/src/meetings/meetings.types.ts +19 -0
  123. package/src/meetings/request.ts +43 -0
  124. package/src/meetings/util.ts +97 -1
  125. package/src/member/index.ts +10 -0
  126. package/src/member/types.ts +1 -0
  127. package/src/member/util.ts +3 -0
  128. package/src/metrics/constants.ts +3 -0
  129. package/src/multistream/codec/constants.ts +58 -0
  130. package/src/multistream/mediaRequestManager.ts +119 -28
  131. package/src/multistream/receiveSlot.ts +18 -0
  132. package/src/reactions/reactions.type.ts +3 -0
  133. package/src/recording-controller/index.ts +1 -2
  134. package/src/webinar/index.ts +162 -21
  135. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  136. package/test/unit/spec/breakouts/breakout.ts +9 -3
  137. package/test/unit/spec/breakouts/index.ts +49 -0
  138. package/test/unit/spec/controls-options-manager/index.js +140 -29
  139. package/test/unit/spec/controls-options-manager/util.js +165 -0
  140. package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
  141. package/test/unit/spec/hashTree/utils.ts +88 -1
  142. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  143. package/test/unit/spec/interpretation/index.ts +26 -4
  144. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  145. package/test/unit/spec/locus-info/index.js +475 -81
  146. package/test/unit/spec/media/index.ts +31 -0
  147. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  148. package/test/unit/spec/meeting/index.js +1131 -49
  149. package/test/unit/spec/meeting/muteState.js +3 -0
  150. package/test/unit/spec/meeting/utils.js +33 -0
  151. package/test/unit/spec/meetings/index.js +360 -10
  152. package/test/unit/spec/meetings/request.js +141 -0
  153. package/test/unit/spec/meetings/utils.js +189 -0
  154. package/test/unit/spec/member/index.js +7 -0
  155. package/test/unit/spec/member/util.js +24 -0
  156. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  157. package/test/unit/spec/recording-controller/index.js +9 -8
  158. package/test/unit/spec/webinar/index.ts +141 -16
@@ -13,6 +13,7 @@ import MediaSharesUtils from '@webex/plugin-meetings/src/locus-info//mediaShares
13
13
  import LocusDeltaParser from '@webex/plugin-meetings/src/locus-info/parser';
14
14
  import Metrics from '@webex/plugin-meetings/src/metrics';
15
15
  import * as HashTreeParserModule from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
16
+ import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
16
17
 
17
18
  import {
18
19
  LOCUSINFO,
@@ -220,6 +221,47 @@ describe('plugin-meetings', () => {
220
221
  assert.isTrue(locusInfo.emitChange);
221
222
  });
222
223
 
224
+ it('calls onLocusSynced callback passed as second argument with full locus from join response', async () => {
225
+ const syncedLocus = {url: 'http://locus-url.com', participants: []};
226
+ const onLocusSynced = sinon.stub();
227
+
228
+ await locusInfo.initialSetup(
229
+ {
230
+ trigger: 'join-response',
231
+ locus: syncedLocus,
232
+ },
233
+ onLocusSynced
234
+ );
235
+
236
+ assert.calledOnceWithExactly(onLocusSynced, syncedLocus);
237
+ });
238
+
239
+ it('swallows onLocusSynced callback errors and logs warn', async () => {
240
+ const syncedLocus = {url: 'http://locus-url.com', participants: []};
241
+ const callbackError = new Error('onLocusSynced failed');
242
+ const onLocusSynced = sinon.stub().throws(callbackError);
243
+ const loggerWarnStub = LoggerProxy.logger.warn?.isSinonProxy
244
+ ? LoggerProxy.logger.warn
245
+ : sinon.stub(LoggerProxy.logger, 'warn');
246
+
247
+ loggerWarnStub.resetHistory();
248
+
249
+ await locusInfo.initialSetup(
250
+ {
251
+ trigger: 'join-response',
252
+ locus: syncedLocus,
253
+ },
254
+ onLocusSynced
255
+ );
256
+
257
+ assert.calledOnceWithExactly(onLocusSynced, syncedLocus);
258
+ assert.calledOnce(loggerWarnStub);
259
+ assert.match(
260
+ loggerWarnStub.firstCall.args[0],
261
+ /Locus-info:index#initialSetup --> onLocusSynced callback failed/
262
+ );
263
+ });
264
+
223
265
  it('should initialize the hash tree parser correctly when triggered from a get loci response containing visible datasets', async () => {
224
266
  const visibleDataSets = ['dataset1', 'dataset2'];
225
267
  const locus = createLocusWithVisibleDataSets(visibleDataSets);
@@ -290,6 +332,7 @@ describe('plugin-meetings', () => {
290
332
  describe('should setup correct locusInfoUpdateCallback when creating HashTreeParser', () => {
291
333
  const OBJECTS_UPDATED = HashTreeParserModule.LocusInfoUpdateType.OBJECTS_UPDATED;
292
334
  const MEETING_ENDED = HashTreeParserModule.LocusInfoUpdateType.MEETING_ENDED;
335
+ const LOCUS_NOT_FOUND = HashTreeParserModule.LocusInfoUpdateType.LOCUS_NOT_FOUND;
293
336
 
294
337
  let locusInfoUpdateCallback;
295
338
  let onDeltaLocusStub;
@@ -413,7 +456,7 @@ describe('plugin-meetings', () => {
413
456
  };
414
457
 
415
458
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
416
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
459
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
417
460
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
418
461
  });
419
462
 
@@ -440,7 +483,7 @@ describe('plugin-meetings', () => {
440
483
  locusInfo.info.isWebinar = true;
441
484
 
442
485
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
443
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
486
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
444
487
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
445
488
  });
446
489
 
@@ -473,7 +516,7 @@ describe('plugin-meetings', () => {
473
516
  locusInfo.info.isWebinar = true;
474
517
 
475
518
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
476
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
519
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
477
520
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
478
521
  });
479
522
 
@@ -501,7 +544,7 @@ describe('plugin-meetings', () => {
501
544
  };
502
545
 
503
546
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
504
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
547
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
505
548
  updatedObjects: [{htMeta: {elementId: {type: 'fullState'}}, data: newFullState}],
506
549
  });
507
550
 
@@ -519,7 +562,7 @@ describe('plugin-meetings', () => {
519
562
  };
520
563
 
521
564
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
522
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
565
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
523
566
  updatedObjects: [{htMeta: {elementId: {type: 'info'}}, data: newInfo}],
524
567
  });
525
568
 
@@ -537,7 +580,7 @@ describe('plugin-meetings', () => {
537
580
  };
538
581
 
539
582
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
540
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
583
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
541
584
  updatedObjects: [{htMeta: {elementId: {type: 'links'}}, data: newLinks}],
542
585
  });
543
586
 
@@ -557,7 +600,7 @@ describe('plugin-meetings', () => {
557
600
  };
558
601
 
559
602
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
560
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
603
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
561
604
  updatedObjects: [{htMeta: newLocusHtMeta, data: newLocus}],
562
605
  });
563
606
 
@@ -590,7 +633,7 @@ describe('plugin-meetings', () => {
590
633
  };
591
634
 
592
635
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
593
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
636
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
594
637
  updatedObjects: [
595
638
  {
596
639
  htMeta: newLocusHtMeta,
@@ -637,7 +680,7 @@ describe('plugin-meetings', () => {
637
680
  };
638
681
 
639
682
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
640
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
683
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
641
684
  updatedObjects: [
642
685
  // first, a removal of LOCUS object
643
686
  {htMeta: {elementId: {type: 'locus'}}, data: null},
@@ -671,7 +714,7 @@ describe('plugin-meetings', () => {
671
714
  const newLocusHtMeta = {elementId: {type: 'locus', version: 99}};
672
715
 
673
716
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
674
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
717
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
675
718
  updatedObjects: [
676
719
  // first, an update
677
720
  {htMeta: newLocusHtMeta, data: newLocus},
@@ -700,7 +743,7 @@ describe('plugin-meetings', () => {
700
743
  };
701
744
 
702
745
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
703
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
746
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
704
747
  updatedObjects: [
705
748
  // first, an update
706
749
  {htMeta: {elementId: {type: 'locus'}}, data: newLocus1},
@@ -730,7 +773,7 @@ describe('plugin-meetings', () => {
730
773
  };
731
774
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
732
775
  // with 1 participant added, 1 updated, and 1 removed
733
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
776
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
734
777
  updatedObjects: [
735
778
  {htMeta: {elementId: {type: 'participant', id: 'fake-ht-participant-1'}}, data: null},
736
779
  {
@@ -774,7 +817,7 @@ describe('plugin-meetings', () => {
774
817
  };
775
818
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
776
819
  // with 1 participant added, 1 updated, and 1 removed
777
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
820
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
778
821
  updatedObjects: [
779
822
  {htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-1'}}, data: null},
780
823
  {
@@ -807,7 +850,7 @@ describe('plugin-meetings', () => {
807
850
  };
808
851
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
809
852
  // with 1 embedded app added, 1 updated, and 1 removed
810
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
853
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
811
854
  updatedObjects: [
812
855
  {htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1'}}, data: null},
813
856
  {
@@ -844,7 +887,7 @@ describe('plugin-meetings', () => {
844
887
  };
845
888
 
846
889
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
847
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
890
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
848
891
  updatedObjects: [
849
892
  {
850
893
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2'}},
@@ -883,7 +926,7 @@ describe('plugin-meetings', () => {
883
926
  };
884
927
 
885
928
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
886
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
929
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
887
930
  updatedObjects: [
888
931
  {
889
932
  htMeta: {elementId: {type: 'controlentry', id: 'control-1'}},
@@ -911,7 +954,7 @@ describe('plugin-meetings', () => {
911
954
 
912
955
  it('should process locus update correctly when CONTROL object is received with no data', () => {
913
956
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
914
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
957
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
915
958
  updatedObjects: [
916
959
  {
917
960
  htMeta: {elementId: {type: 'controlentry', id: 'some-control-id'}},
@@ -935,7 +978,7 @@ describe('plugin-meetings', () => {
935
978
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
936
979
 
937
980
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
938
- locusInfoUpdateCallback(MEETING_ENDED);
981
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
939
982
 
940
983
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
941
984
  assert.calledOnceWithExactly(
@@ -953,17 +996,48 @@ describe('plugin-meetings', () => {
953
996
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
954
997
 
955
998
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
956
- locusInfoUpdateCallback(MEETING_ENDED);
999
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
957
1000
 
958
1001
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
959
1002
  assert.notCalled(destroyStub);
960
1003
  });
961
1004
 
1005
+ it('should handle LOCUS_NOT_FOUND by calling syncMeetings with skipHashTreeSync', () => {
1006
+ const syncMeetingsStub = sinon.stub(locusInfo.webex.meetings, 'syncMeetings').resolves();
1007
+
1008
+ locusInfoUpdateCallback({updateType: LOCUS_NOT_FOUND});
1009
+
1010
+ assert.calledOnceWithExactly(syncMeetingsStub, {keepOnlyLocusMeetings: false, skipHashTreeSync: true});
1011
+ });
1012
+
1013
+ it('should handle LOCUS_NOT_FOUND and log error if syncMeetings fails', async () => {
1014
+ const syncError = new Error('sync failed');
1015
+ const syncMeetingsStub = sinon.stub(locusInfo.webex.meetings, 'syncMeetings').rejects(syncError);
1016
+ const logErrorStub = LoggerProxy.logger.error?.isSinonProxy
1017
+ ? LoggerProxy.logger.error
1018
+ : sinon.stub(LoggerProxy.logger, 'error');
1019
+
1020
+ logErrorStub.resetHistory();
1021
+
1022
+ locusInfoUpdateCallback({updateType: LOCUS_NOT_FOUND});
1023
+
1024
+ assert.calledOnceWithExactly(syncMeetingsStub, {keepOnlyLocusMeetings: false, skipHashTreeSync: true});
1025
+
1026
+ // wait for the promise rejection to be handled
1027
+ await testUtils.flushPromises();
1028
+
1029
+ assert.calledOnce(logErrorStub);
1030
+ assert.match(
1031
+ logErrorStub.firstCall.args[0],
1032
+ /syncMeetings failed after LOCUS_NOT_FOUND/
1033
+ );
1034
+ });
1035
+
962
1036
  it('should set forceReplaceMembers to true on the first update for a locusUrl (initializedFromHashTree is false)', () => {
963
1037
  const createdHashTreeParser = locusInfo.hashTreeParsers.get('fake-locus-url');
964
1038
  createdHashTreeParser.initializedFromHashTree = false;
965
1039
 
966
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1040
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
967
1041
  updatedObjects: [
968
1042
  {
969
1043
  htMeta: {elementId: {type: 'self'}},
@@ -978,7 +1052,7 @@ describe('plugin-meetings', () => {
978
1052
  });
979
1053
 
980
1054
  it('should set forceReplaceMembers to false on subsequent updates (initializedFromHashTree is true)', () => {
981
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1055
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
982
1056
  updatedObjects: [
983
1057
  {
984
1058
  htMeta: {elementId: {type: 'self'}},
@@ -994,7 +1068,7 @@ describe('plugin-meetings', () => {
994
1068
  it('should copy participant data to self when participant matches self identity and state is LEFT with reason MOVED', () => {
995
1069
  locusInfo.self = {id: 'fake-self', identity: 'user-123'};
996
1070
 
997
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1071
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
998
1072
  updatedObjects: [
999
1073
  {
1000
1074
  htMeta: {elementId: {type: 'participant', id: 99}},
@@ -1289,6 +1363,8 @@ describe('plugin-meetings', () => {
1289
1363
  state: RECORDING_STATE.IDLE,
1290
1364
  modifiedBy: 'George Kittle',
1291
1365
  lastModified: 'TODAY',
1366
+ modifiedByServiceAppName: undefined,
1367
+ modifiedByServiceAppId: undefined,
1292
1368
  }
1293
1369
  );
1294
1370
  });
@@ -1323,6 +1399,8 @@ describe('plugin-meetings', () => {
1323
1399
  state: RECORDING_STATE.RECORDING,
1324
1400
  modifiedBy: 'George Kittle',
1325
1401
  lastModified: 'TODAY',
1402
+ modifiedByServiceAppName: undefined,
1403
+ modifiedByServiceAppId: undefined,
1326
1404
  }
1327
1405
  );
1328
1406
  });
@@ -1358,6 +1436,8 @@ describe('plugin-meetings', () => {
1358
1436
  state: RECORDING_STATE.PAUSED,
1359
1437
  modifiedBy: 'George Kittle',
1360
1438
  lastModified: 'TODAY',
1439
+ modifiedByServiceAppName: undefined,
1440
+ modifiedByServiceAppId: undefined,
1361
1441
  }
1362
1442
  );
1363
1443
  });
@@ -1394,6 +1474,8 @@ describe('plugin-meetings', () => {
1394
1474
  state: RECORDING_STATE.RESUMED,
1395
1475
  modifiedBy: 'George Kittle',
1396
1476
  lastModified: 'TODAY',
1477
+ modifiedByServiceAppName: undefined,
1478
+ modifiedByServiceAppId: undefined,
1397
1479
  }
1398
1480
  );
1399
1481
  });
@@ -1429,6 +1511,44 @@ describe('plugin-meetings', () => {
1429
1511
  state: RECORDING_STATE.IDLE,
1430
1512
  modifiedBy: 'George Kittle',
1431
1513
  lastModified: 'TODAY',
1514
+ modifiedByServiceAppName: undefined,
1515
+ modifiedByServiceAppId: undefined,
1516
+ }
1517
+ );
1518
+ });
1519
+
1520
+ it('should include service app fields in the recording event when present', () => {
1521
+ locusInfo.controls = {
1522
+ record: {
1523
+ recording: false,
1524
+ paused: false,
1525
+ meta: {
1526
+ lastModified: 'TODAY',
1527
+ modifiedBy: 'George Kittle',
1528
+ },
1529
+ },
1530
+ shareControl: {},
1531
+ transcribe: {},
1532
+ };
1533
+ newControls.record.recording = true;
1534
+ newControls.record.meta.modifiedByServiceAppName = 'My Bot';
1535
+ newControls.record.meta.modifiedByServiceAppId = 'app-id-123';
1536
+ locusInfo.emitScoped = sinon.stub();
1537
+ locusInfo.updateControls(newControls);
1538
+
1539
+ assert.calledWith(
1540
+ locusInfo.emitScoped,
1541
+ {
1542
+ file: 'locus-info',
1543
+ function: 'updateControls',
1544
+ },
1545
+ LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
1546
+ {
1547
+ state: RECORDING_STATE.RECORDING,
1548
+ modifiedBy: 'George Kittle',
1549
+ lastModified: 'TODAY',
1550
+ modifiedByServiceAppName: 'My Bot',
1551
+ modifiedByServiceAppId: 'app-id-123',
1432
1552
  }
1433
1553
  );
1434
1554
  });
@@ -2024,7 +2144,7 @@ describe('plugin-meetings', () => {
2024
2144
  function: 'updateSelf',
2025
2145
  },
2026
2146
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2027
- {muted: true, unmuteAllowed: true}
2147
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2028
2148
  );
2029
2149
 
2030
2150
  // but sometimes "previous self" is defined, but without controls.audio.muted, so we test this here:
@@ -2039,7 +2159,7 @@ describe('plugin-meetings', () => {
2039
2159
  function: 'updateSelf',
2040
2160
  },
2041
2161
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2042
- {muted: true, unmuteAllowed: true}
2162
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2043
2163
  );
2044
2164
  });
2045
2165
 
@@ -2098,7 +2218,7 @@ describe('plugin-meetings', () => {
2098
2218
  function: 'updateSelf',
2099
2219
  },
2100
2220
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2101
- {muted: true, unmuteAllowed: true}
2221
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2102
2222
  );
2103
2223
  });
2104
2224
 
@@ -2237,7 +2357,7 @@ describe('plugin-meetings', () => {
2237
2357
  function: 'updateSelf',
2238
2358
  },
2239
2359
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2240
- {muted: true, unmuteAllowed: false}
2360
+ {muted: true, unmuteAllowed: false, modifiedBy: null}
2241
2361
  );
2242
2362
 
2243
2363
  // now change only disallowUnmute
@@ -2255,7 +2375,28 @@ describe('plugin-meetings', () => {
2255
2375
  function: 'updateSelf',
2256
2376
  },
2257
2377
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2258
- {muted: true, unmuteAllowed: true}
2378
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2379
+ );
2380
+ });
2381
+
2382
+ it('should include modifiedBy in payload when muted by host', () => {
2383
+ locusInfo.webex.internal.device.url = self.deviceUrl;
2384
+ locusInfo.updateSelf(self);
2385
+ const newSelf = cloneDeep(self);
2386
+ newSelf.controls.audio.muted = true;
2387
+ newSelf.controls.audio.meta = {modifiedBy: 'host-uuid-123'};
2388
+
2389
+ locusInfo.emitScoped = sinon.stub();
2390
+ locusInfo.updateSelf(newSelf);
2391
+
2392
+ assert.calledWith(
2393
+ locusInfo.emitScoped,
2394
+ {
2395
+ file: 'locus-info',
2396
+ function: 'updateSelf',
2397
+ },
2398
+ LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2399
+ {muted: true, unmuteAllowed: true, modifiedBy: 'host-uuid-123'}
2259
2400
  );
2260
2401
  });
2261
2402
 
@@ -3154,13 +3295,14 @@ describe('plugin-meetings', () => {
3154
3295
  const createMockParser = (state = 'active') => ({
3155
3296
  state,
3156
3297
  stop: sinon.stub(),
3157
- resume: sinon.stub(),
3298
+ resumeFromMessage: sinon.stub(),
3158
3299
  handleMessage: sinon.stub(),
3159
3300
  });
3160
3301
 
3161
3302
  const createSelfElementWithReplaces = (replacedLocusUrl, replacedAt) => ({
3162
3303
  htMeta: {elementId: {type: 'Self'}},
3163
3304
  data: {
3305
+ deviceUrl,
3164
3306
  devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3165
3307
  },
3166
3308
  });
@@ -3236,7 +3378,7 @@ describe('plugin-meetings', () => {
3236
3378
  stateElementsMessage: message,
3237
3379
  });
3238
3380
 
3239
- assert.calledOnce(parserA.resume);
3381
+ assert.calledOnce(parserA.resumeFromMessage);
3240
3382
  assert.calledOnce(parserB.stop);
3241
3383
  });
3242
3384
 
@@ -3259,7 +3401,7 @@ describe('plugin-meetings', () => {
3259
3401
  stateElementsMessage: message,
3260
3402
  });
3261
3403
 
3262
- assert.notCalled(parserA.resume);
3404
+ assert.notCalled(parserA.resumeFromMessage);
3263
3405
  assert.notCalled(parserB.stop);
3264
3406
  });
3265
3407
 
@@ -3278,7 +3420,7 @@ describe('plugin-meetings', () => {
3278
3420
  stateElementsMessage: message,
3279
3421
  });
3280
3422
 
3281
- assert.notCalled(parserA.resume);
3423
+ assert.notCalled(parserA.resumeFromMessage);
3282
3424
  assert.notCalled(parserA.handleMessage);
3283
3425
  });
3284
3426
 
@@ -3362,6 +3504,51 @@ describe('plugin-meetings', () => {
3362
3504
 
3363
3505
  assert.calledOnceWithExactly(parserA.handleMessage, message);
3364
3506
  });
3507
+
3508
+ it('should send mismatch metric when eventType is not HASH_TREE_DATA_UPDATED', () => {
3509
+ const locusUrlA = 'http://locus-url-A.com';
3510
+ const parserA = {state: 'active', handleMessage: sinon.stub()};
3511
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
3512
+
3513
+ locusInfo.parse(mockMeeting, {
3514
+ eventType: LOCUSEVENT.SELF_CHANGED,
3515
+ stateElementsMessage: {locusUrl: locusUrlA, locusStateElements: [], dataSets: []},
3516
+ });
3517
+
3518
+ assert.calledOnceWithExactly(
3519
+ sendBehavioralMetricStub,
3520
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3521
+ {
3522
+ correlationId: mockMeeting.correlationId,
3523
+ message: `got ${LOCUSEVENT.SELF_CHANGED}, expected ${LOCUSEVENT.HASH_TREE_DATA_UPDATED}`,
3524
+ }
3525
+ );
3526
+ assert.notCalled(parserA.handleMessage);
3527
+ });
3528
+ });
3529
+
3530
+ describe('#sendClassicVsHashTreeMismatchMetric', () => {
3531
+ it('should send the metric when called for the first time', () => {
3532
+ locusInfo.sendClassicVsHashTreeMismatchMetric(mockMeeting, 'some mismatch');
3533
+
3534
+ assert.calledOnceWithExactly(
3535
+ sendBehavioralMetricStub,
3536
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3537
+ {
3538
+ correlationId: mockMeeting.correlationId,
3539
+ message: 'some mismatch',
3540
+ }
3541
+ );
3542
+ });
3543
+
3544
+ it('should send the metric up to 5 times and stop after that', () => {
3545
+ for (let i = 0; i < 7; i += 1) {
3546
+ locusInfo.sendClassicVsHashTreeMismatchMetric(mockMeeting, `mismatch ${i}`);
3547
+ }
3548
+
3549
+ assert.callCount(sendBehavioralMetricStub, 5);
3550
+ assert.equal(locusInfo.classicVsHashTreeMismatchMetricCounter, 5);
3551
+ });
3365
3552
  });
3366
3553
 
3367
3554
  describe('#handleLocusAPIResponse', () => {
@@ -3417,19 +3604,174 @@ describe('plugin-meetings', () => {
3417
3604
  assert.calledOnceWithExactly(locusInfo.handleLocusDelta, fakeLocus, mockMeeting);
3418
3605
  });
3419
3606
 
3420
- it('should send mismatch metric when hash tree parser exists but dataSets are missing in wrapped response', () => {
3607
+ it('should send mismatch metric in classic mode when wrapped response has dataSets', () => {
3421
3608
  const fakeLocus = {url: 'http://locus-url.com'};
3422
- const mockHashTreeParser = {handleLocusUpdate: sinon.stub()};
3423
- locusInfo.hashTreeParsers.set(fakeLocus.url, {
3424
- parser: mockHashTreeParser,
3425
- initializedFromHashTree: true,
3609
+ sinon.stub(locusInfo, 'handleLocusDelta');
3610
+
3611
+ locusInfo.handleLocusAPIResponse(mockMeeting, {
3612
+ locus: fakeLocus,
3613
+ dataSets: [{name: 'dataset1', url: 'test-url'}],
3426
3614
  });
3427
- sinon.stub(locusInfo, 'sendClassicVsHashTreeMismatchMetric');
3428
3615
 
3429
- locusInfo.handleLocusAPIResponse(mockMeeting, {locus: fakeLocus});
3616
+ assert.calledOnceWithExactly(
3617
+ sendBehavioralMetricStub,
3618
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3619
+ {
3620
+ correlationId: mockMeeting.correlationId,
3621
+ message: 'unexpected hash tree dataSets in API response',
3622
+ }
3623
+ );
3624
+ assert.calledOnce(locusInfo.handleLocusDelta);
3625
+ });
3430
3626
 
3431
- assert.calledOnce(locusInfo.sendClassicVsHashTreeMismatchMetric);
3432
- assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
3627
+ describe('parser switch via API response', () => {
3628
+ const deviceUrl = 'http://device-url.com';
3629
+ const locusUrlA = 'http://locus-url-A.com';
3630
+ const locusUrlB = 'http://locus-url-B.com';
3631
+
3632
+ let HashTreeParserStub;
3633
+
3634
+ const createMockApiParser = (state = 'active') => ({
3635
+ state,
3636
+ stop: sinon.stub(),
3637
+ resumeFromApiResponse: sinon.stub(),
3638
+ handleLocusUpdate: sinon.stub(),
3639
+ initializeFromGetLociResponse: sinon.stub(),
3640
+ });
3641
+
3642
+ const createLocusWithReplaces = (url, replacedLocusUrl, replacedAt) => ({
3643
+ url,
3644
+ self: {
3645
+ devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3646
+ },
3647
+ });
3648
+
3649
+ const createLocusWithoutReplaces = (url) => ({
3650
+ url,
3651
+ self: {devices: [{url: deviceUrl}]},
3652
+ });
3653
+
3654
+ beforeEach(() => {
3655
+ locusInfo.webex.internal.device.url = deviceUrl;
3656
+ HashTreeParserStub = sinon
3657
+ .stub(HashTreeParserModule, 'default')
3658
+ .returns(createMockApiParser());
3659
+ });
3660
+
3661
+ it('should create a new parser and initialize it when no entry exists for the locusUrl', () => {
3662
+ // existing parser for a different url so hashTreeParsers.size > 0
3663
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: createMockApiParser(), initializedFromHashTree: true});
3664
+
3665
+ const locus = createLocusWithReplaces(locusUrlB, locusUrlA, '2026-01-01T00:00:00Z');
3666
+ sinon.stub(locusInfo, 'handleLocusDelta');
3667
+
3668
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3669
+
3670
+ assert.isTrue(locusInfo.hashTreeParsers.has(locusUrlB));
3671
+ const newEntry = locusInfo.hashTreeParsers.get(locusUrlB);
3672
+ assert.isFalse(newEntry.initializedFromHashTree);
3673
+
3674
+ // the stub returns the mock, so initializeFromGetLociResponse should be called on it
3675
+ const createdParser = HashTreeParserStub.returnValues[0];
3676
+ assert.calledOnceWithExactly(createdParser.initializeFromGetLociResponse, locus);
3677
+ assert.notCalled(locusInfo.handleLocusDelta);
3678
+ });
3679
+
3680
+ it('should reactivate a stopped parser when replaces info is newer', () => {
3681
+ const parserA = createMockApiParser('stopped');
3682
+ const parserB = createMockApiParser('active');
3683
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-01-01T00:00:00Z', initializedFromHashTree: true});
3684
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3685
+
3686
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-02-01T00:00:00Z');
3687
+
3688
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3689
+
3690
+ assert.calledOnce(parserA.resumeFromApiResponse);
3691
+ assert.calledWithExactly(parserA.resumeFromApiResponse, locus);
3692
+ assert.calledOnce(parserB.stop);
3693
+ assert.equal(locusInfo.hashTreeParsers.get(locusUrlB).replacedAt, '2026-02-01T00:00:00Z');
3694
+ assert.isFalse(locusInfo.hashTreeParsers.get(locusUrlA).initializedFromHashTree);
3695
+ });
3696
+
3697
+ it('should not reactivate a stopped parser when replaces info is not newer', () => {
3698
+ const parserA = createMockApiParser('stopped');
3699
+ const parserB = createMockApiParser('active');
3700
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-03-01T00:00:00Z', initializedFromHashTree: true});
3701
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3702
+
3703
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-01-01T00:00:00Z');
3704
+
3705
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3706
+
3707
+ assert.notCalled(parserA.resumeFromApiResponse);
3708
+ assert.notCalled(parserB.stop);
3709
+ });
3710
+
3711
+ it('should not reactivate a stopped parser when no replaces info is available', () => {
3712
+ const parserA = createMockApiParser('stopped');
3713
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
3714
+
3715
+ const locus = createLocusWithoutReplaces(locusUrlA);
3716
+
3717
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3718
+
3719
+ assert.notCalled(parserA.resumeFromApiResponse);
3720
+ });
3721
+ });
3722
+ });
3723
+
3724
+ describe('#syncAllHashTreeDatasets', () => {
3725
+ it('should call syncAllDatasets on each parser that has an entry', async () => {
3726
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3727
+ const parser2 = {syncAllDatasets: sinon.stub().resolves()};
3728
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3729
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3730
+
3731
+ await locusInfo.syncAllHashTreeDatasets();
3732
+
3733
+ assert.calledOnce(parser1.syncAllDatasets);
3734
+ assert.calledOnce(parser2.syncAllDatasets);
3735
+ });
3736
+
3737
+ it('should skip parser entries without a parser object', async () => {
3738
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3739
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3740
+ locusInfo.hashTreeParsers.set('url2', {parser: undefined});
3741
+
3742
+ await locusInfo.syncAllHashTreeDatasets();
3743
+
3744
+ assert.calledOnce(parser1.syncAllDatasets);
3745
+ });
3746
+
3747
+ it('should await each parsers syncAllDatasets sequentially', async () => {
3748
+ const callOrder = [];
3749
+ const parser1 = {syncAllDatasets: sinon.stub().callsFake(() => {
3750
+ callOrder.push('start1');
3751
+ return new Promise((resolve) => {
3752
+ setTimeout(() => {
3753
+ callOrder.push('end1');
3754
+ resolve();
3755
+ }, 100);
3756
+ });
3757
+ })};
3758
+ const parser2 = {syncAllDatasets: sinon.stub().callsFake(() => {
3759
+ callOrder.push('start2');
3760
+ return Promise.resolve();
3761
+ })};
3762
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3763
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3764
+
3765
+ const clock = sinon.useFakeTimers();
3766
+ const promise = locusInfo.syncAllHashTreeDatasets();
3767
+ // parser1 started but parser2 not yet
3768
+ assert.deepEqual(callOrder, ['start1']);
3769
+
3770
+ await clock.tickAsync(100);
3771
+ await promise;
3772
+ // parser1 finished, then parser2 started and finished
3773
+ assert.deepEqual(callOrder, ['start1', 'end1', 'start2']);
3774
+ clock.restore();
3433
3775
  });
3434
3776
  });
3435
3777
 
@@ -3521,49 +3863,23 @@ describe('plugin-meetings', () => {
3521
3863
  assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
3522
3864
  });
3523
3865
 
3524
- it('#updateLocusInfo ignores breakout LEFT message', () => {
3525
- const newLocus = {
3526
- self: {
3527
- reason: 'MOVED',
3528
- state: 'LEFT',
3529
- },
3530
- };
3866
+ it('#updateLocusInfo ignores locus when isSelfMovedOrBreakoutEnded returns true', () => {
3867
+ const newLocus = {self: {state: 'JOINED'}};
3868
+
3869
+ sinon.stub(MeetingsUtil, 'isSelfMovedOrBreakoutEnded').returns(true);
3531
3870
 
3532
3871
  locusInfo.updateControls = sinon.stub();
3533
- locusInfo.updateConversationUrl = sinon.stub();
3534
- locusInfo.updateCreated = sinon.stub();
3535
3872
  locusInfo.updateFullState = sinon.stub();
3536
- locusInfo.updateHostInfo = sinon.stub();
3537
- locusInfo.updateMeetingInfo = sinon.stub();
3538
- locusInfo.updateMediaShares = sinon.stub();
3539
- locusInfo.updateReplaces = sinon.stub();
3540
3873
  locusInfo.updateSelf = sinon.stub();
3541
- locusInfo.updateLocusUrl = sinon.stub();
3542
- locusInfo.updateAclUrl = sinon.stub();
3543
- locusInfo.updateBasequence = sinon.stub();
3544
- locusInfo.updateSequence = sinon.stub();
3545
- locusInfo.updateEmbeddedApps = sinon.stub();
3546
- locusInfo.updateLinks = sinon.stub();
3547
- locusInfo.compareAndUpdate = sinon.stub();
3548
3874
 
3549
3875
  locusInfo.updateLocusInfo(newLocus);
3550
3876
 
3877
+ assert.calledOnceWithExactly(MeetingsUtil.isSelfMovedOrBreakoutEnded, newLocus);
3551
3878
  assert.notCalled(locusInfo.updateControls);
3552
- assert.notCalled(locusInfo.updateConversationUrl);
3553
- assert.notCalled(locusInfo.updateCreated);
3554
3879
  assert.notCalled(locusInfo.updateFullState);
3555
- assert.notCalled(locusInfo.updateHostInfo);
3556
- assert.notCalled(locusInfo.updateMeetingInfo);
3557
- assert.notCalled(locusInfo.updateMediaShares);
3558
- assert.notCalled(locusInfo.updateReplaces);
3559
3880
  assert.notCalled(locusInfo.updateSelf);
3560
- assert.notCalled(locusInfo.updateLocusUrl);
3561
- assert.notCalled(locusInfo.updateAclUrl);
3562
- assert.notCalled(locusInfo.updateBasequence);
3563
- assert.notCalled(locusInfo.updateSequence);
3564
- assert.notCalled(locusInfo.updateEmbeddedApps);
3565
- assert.notCalled(locusInfo.updateLinks);
3566
- assert.notCalled(locusInfo.compareAndUpdate);
3881
+
3882
+ MeetingsUtil.isSelfMovedOrBreakoutEnded.restore();
3567
3883
  });
3568
3884
 
3569
3885
  it('#updateLocusInfo puts the Locus DTO top level properties at the right place in LocusInfo class', () => {
@@ -4413,6 +4729,31 @@ describe('plugin-meetings', () => {
4413
4729
  });
4414
4730
  });
4415
4731
 
4732
+ describe('#cleanUp', () => {
4733
+ it('calls cleanUp on all hash tree parsers and clears maps', () => {
4734
+ const parser1 = {cleanUp: sinon.stub()};
4735
+ const parser2 = {cleanUp: sinon.stub()};
4736
+
4737
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1, initializedFromHashTree: true});
4738
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2, initializedFromHashTree: true});
4739
+ locusInfo.hashTreeObjectId2ParticipantId.set(1, 'participant1');
4740
+
4741
+ locusInfo.cleanUp();
4742
+
4743
+ assert.calledOnce(parser1.cleanUp);
4744
+ assert.calledOnce(parser2.cleanUp);
4745
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4746
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4747
+ });
4748
+
4749
+ it('works when there are no hash tree parsers', () => {
4750
+ locusInfo.cleanUp();
4751
+
4752
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4753
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4754
+ });
4755
+ });
4756
+
4416
4757
  describe('#handleOneonOneEvent', () => {
4417
4758
  beforeEach(() => {
4418
4759
  locusInfo.emitScoped = sinon.stub();
@@ -4455,6 +4796,9 @@ describe('plugin-meetings', () => {
4455
4796
  });
4456
4797
 
4457
4798
  describe('#isMeetingActive', () => {
4799
+ beforeEach(() => {
4800
+ webex.internal.newMetrics.submitClientEvent.resetHistory();
4801
+ });
4458
4802
  forEach([_CALL_, _SIP_BRIDGE_, _SPACE_SHARE_], (type) => {
4459
4803
  describe(`type = ${type}`, () => {
4460
4804
  it('sends client event correctly for state = inactive', () => {
@@ -4521,7 +4865,7 @@ describe('plugin-meetings', () => {
4521
4865
  });
4522
4866
  });
4523
4867
 
4524
- it('sends client event correctly for state = MEETING_INACTIVE_TERMINATING', () => {
4868
+ it('sends client event correctly for state = MEETING_INACTIVE', () => {
4525
4869
  locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
4526
4870
  locusInfo.parsedLocus = {
4527
4871
  fullState: {
@@ -4543,7 +4887,7 @@ describe('plugin-meetings', () => {
4543
4887
  });
4544
4888
  });
4545
4889
 
4546
- it('sends client event correctly for state = FULLSTATE_REMOVED', () => {
4890
+ it('does not send client event when state = INACTIVE and endMeetingReason = BREAKOUT_ENDED', () => {
4547
4891
  locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
4548
4892
  locusInfo.parsedLocus = {
4549
4893
  fullState: {
@@ -4552,17 +4896,41 @@ describe('plugin-meetings', () => {
4552
4896
  };
4553
4897
 
4554
4898
  locusInfo.fullState = {
4555
- removed: true,
4899
+ state: LOCUS.STATE.INACTIVE,
4900
+ endMeetingReason: 'BREAKOUT_ENDED',
4556
4901
  };
4557
4902
 
4558
4903
  locusInfo.isMeetingActive();
4559
4904
 
4560
- assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
4561
- name: 'client.call.remote-ended',
4562
- options: {
4563
- meetingId: locusInfo.meetingId,
4905
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
4906
+ });
4907
+
4908
+ it('sends client event correctly for state self removed', () => {
4909
+ locusInfo.emitScoped = sinon.stub();
4910
+ locusInfo.parsedLocus = {
4911
+ fullState: {
4912
+ type: _MEETING_,
4564
4913
  },
4565
- });
4914
+ self: {
4915
+ removed: true,
4916
+ }
4917
+ };
4918
+
4919
+ locusInfo.isMeetingActive();
4920
+
4921
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
4922
+ assert.calledOnceWithExactly(
4923
+ locusInfo.emitScoped,
4924
+ {
4925
+ file: 'locus-info',
4926
+ function: 'isMeetingActive',
4927
+ },
4928
+ EVENTS.DESTROY_MEETING,
4929
+ {
4930
+ reason: MEETING_REMOVED_REASON.SELF_REMOVED,
4931
+ shouldLeave: false,
4932
+ }
4933
+ );
4566
4934
  });
4567
4935
  });
4568
4936
 
@@ -4954,6 +5322,31 @@ describe('plugin-meetings', () => {
4954
5322
  );
4955
5323
  assert.notCalled(getTheLocusToUpdateStub);
4956
5324
  });
5325
+
5326
+ it('should call handleLocusAPIResponse for SDK_LOCUS_FROM_SYNC_MEETINGS when hash tree parsers exist', () => {
5327
+ const fakeLocusUrl = 'http://locus-url.com';
5328
+ const fakeLocus = {url: fakeLocusUrl, fullState: {state: 'ACTIVE'}};
5329
+ const mockHashTreeParser = {
5330
+ handleMessage: sinon.stub(),
5331
+ handleLocusUpdate: sinon.stub(),
5332
+ };
5333
+ locusInfo.hashTreeParsers.set(fakeLocusUrl, {
5334
+ parser: mockHashTreeParser,
5335
+ initializedFromHashTree: true,
5336
+ });
5337
+
5338
+ sinon.stub(locusInfo, 'handleLocusDelta');
5339
+
5340
+ locusInfo.parse(mockMeeting, {
5341
+ eventType: LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS,
5342
+ locus: fakeLocus,
5343
+ });
5344
+
5345
+ // should route through handleLocusAPIResponse which passes unwrapped LocusDTO to parser
5346
+ assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
5347
+ assert.notCalled(mockHashTreeParser.handleMessage);
5348
+ assert.notCalled(locusInfo.handleLocusDelta);
5349
+ });
4957
5350
  });
4958
5351
  });
4959
5352
 
@@ -5146,6 +5539,7 @@ describe('plugin-meetings', () => {
5146
5539
  return {
5147
5540
  htMeta: {elementId: {type: 'Self'}},
5148
5541
  data: {
5542
+ deviceUrl,
5149
5543
  devices,
5150
5544
  },
5151
5545
  };