@webex/plugin-meetings 3.12.0-next.4 → 3.12.0-next.40

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 (90) 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 +6 -2
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +1 -1
  7. package/dist/constants.js +1 -1
  8. package/dist/constants.js.map +1 -1
  9. package/dist/controls-options-manager/constants.js +11 -1
  10. package/dist/controls-options-manager/constants.js.map +1 -1
  11. package/dist/controls-options-manager/index.js +23 -21
  12. package/dist/controls-options-manager/index.js.map +1 -1
  13. package/dist/controls-options-manager/util.js +91 -0
  14. package/dist/controls-options-manager/util.js.map +1 -1
  15. package/dist/hashTree/constants.js +10 -1
  16. package/dist/hashTree/constants.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +554 -350
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/utils.js +22 -0
  20. package/dist/hashTree/utils.js.map +1 -1
  21. package/dist/interceptors/locusRetry.js +23 -8
  22. package/dist/interceptors/locusRetry.js.map +1 -1
  23. package/dist/interpretation/index.js +1 -1
  24. package/dist/interpretation/siLanguage.js +1 -1
  25. package/dist/locus-info/index.js +274 -85
  26. package/dist/locus-info/index.js.map +1 -1
  27. package/dist/locus-info/types.js +16 -0
  28. package/dist/locus-info/types.js.map +1 -1
  29. package/dist/meeting/index.js +710 -499
  30. package/dist/meeting/index.js.map +1 -1
  31. package/dist/meeting/util.js +1 -0
  32. package/dist/meeting/util.js.map +1 -1
  33. package/dist/meetings/index.js +174 -77
  34. package/dist/meetings/index.js.map +1 -1
  35. package/dist/meetings/util.js +49 -5
  36. package/dist/meetings/util.js.map +1 -1
  37. package/dist/member/index.js +10 -0
  38. package/dist/member/index.js.map +1 -1
  39. package/dist/member/types.js.map +1 -1
  40. package/dist/member/util.js +3 -0
  41. package/dist/member/util.js.map +1 -1
  42. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  43. package/dist/types/hashTree/constants.d.ts +1 -0
  44. package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
  45. package/dist/types/hashTree/utils.d.ts +11 -0
  46. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  47. package/dist/types/locus-info/index.d.ts +46 -6
  48. package/dist/types/locus-info/types.d.ts +17 -1
  49. package/dist/types/meeting/index.d.ts +64 -1
  50. package/dist/types/member/index.d.ts +1 -0
  51. package/dist/types/member/types.d.ts +1 -0
  52. package/dist/types/member/util.d.ts +1 -0
  53. package/dist/webinar/index.js +301 -226
  54. package/dist/webinar/index.js.map +1 -1
  55. package/package.json +22 -22
  56. package/src/aiEnableRequest/index.ts +16 -0
  57. package/src/breakouts/breakout.ts +2 -1
  58. package/src/constants.ts +1 -1
  59. package/src/controls-options-manager/constants.ts +14 -1
  60. package/src/controls-options-manager/index.ts +26 -19
  61. package/src/controls-options-manager/util.ts +81 -1
  62. package/src/hashTree/constants.ts +9 -0
  63. package/src/hashTree/hashTreeParser.ts +278 -160
  64. package/src/hashTree/utils.ts +17 -0
  65. package/src/interceptors/locusRetry.ts +25 -4
  66. package/src/locus-info/index.ts +274 -93
  67. package/src/locus-info/types.ts +19 -1
  68. package/src/meeting/index.ts +206 -22
  69. package/src/meeting/util.ts +1 -0
  70. package/src/meetings/index.ts +77 -43
  71. package/src/meetings/util.ts +56 -1
  72. package/src/member/index.ts +10 -0
  73. package/src/member/types.ts +1 -0
  74. package/src/member/util.ts +3 -0
  75. package/src/webinar/index.ts +75 -1
  76. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  77. package/test/unit/spec/breakouts/breakout.ts +7 -3
  78. package/test/unit/spec/controls-options-manager/index.js +114 -6
  79. package/test/unit/spec/controls-options-manager/util.js +165 -0
  80. package/test/unit/spec/hashTree/hashTreeParser.ts +996 -51
  81. package/test/unit/spec/hashTree/utils.ts +88 -1
  82. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  83. package/test/unit/spec/locus-info/index.js +397 -81
  84. package/test/unit/spec/meeting/index.js +271 -44
  85. package/test/unit/spec/meeting/utils.js +4 -0
  86. package/test/unit/spec/meetings/index.js +195 -13
  87. package/test/unit/spec/meetings/utils.js +137 -0
  88. package/test/unit/spec/member/index.js +7 -0
  89. package/test/unit/spec/member/util.js +24 -0
  90. package/test/unit/spec/webinar/index.ts +60 -0
@@ -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);
@@ -413,7 +455,7 @@ describe('plugin-meetings', () => {
413
455
  };
414
456
 
415
457
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
416
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
458
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
417
459
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
418
460
  });
419
461
 
@@ -440,7 +482,7 @@ describe('plugin-meetings', () => {
440
482
  locusInfo.info.isWebinar = true;
441
483
 
442
484
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
443
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
485
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
444
486
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
445
487
  });
446
488
 
@@ -473,7 +515,7 @@ describe('plugin-meetings', () => {
473
515
  locusInfo.info.isWebinar = true;
474
516
 
475
517
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
476
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
518
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
477
519
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
478
520
  });
479
521
 
@@ -501,7 +543,7 @@ describe('plugin-meetings', () => {
501
543
  };
502
544
 
503
545
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
504
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
546
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
505
547
  updatedObjects: [{htMeta: {elementId: {type: 'fullState'}}, data: newFullState}],
506
548
  });
507
549
 
@@ -519,7 +561,7 @@ describe('plugin-meetings', () => {
519
561
  };
520
562
 
521
563
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
522
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
564
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
523
565
  updatedObjects: [{htMeta: {elementId: {type: 'info'}}, data: newInfo}],
524
566
  });
525
567
 
@@ -537,7 +579,7 @@ describe('plugin-meetings', () => {
537
579
  };
538
580
 
539
581
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
540
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
582
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
541
583
  updatedObjects: [{htMeta: {elementId: {type: 'links'}}, data: newLinks}],
542
584
  });
543
585
 
@@ -557,7 +599,7 @@ describe('plugin-meetings', () => {
557
599
  };
558
600
 
559
601
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
560
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
602
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
561
603
  updatedObjects: [{htMeta: newLocusHtMeta, data: newLocus}],
562
604
  });
563
605
 
@@ -590,7 +632,7 @@ describe('plugin-meetings', () => {
590
632
  };
591
633
 
592
634
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
593
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
635
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
594
636
  updatedObjects: [
595
637
  {
596
638
  htMeta: newLocusHtMeta,
@@ -637,7 +679,7 @@ describe('plugin-meetings', () => {
637
679
  };
638
680
 
639
681
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
640
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
682
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
641
683
  updatedObjects: [
642
684
  // first, a removal of LOCUS object
643
685
  {htMeta: {elementId: {type: 'locus'}}, data: null},
@@ -671,7 +713,7 @@ describe('plugin-meetings', () => {
671
713
  const newLocusHtMeta = {elementId: {type: 'locus', version: 99}};
672
714
 
673
715
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
674
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
716
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
675
717
  updatedObjects: [
676
718
  // first, an update
677
719
  {htMeta: newLocusHtMeta, data: newLocus},
@@ -700,7 +742,7 @@ describe('plugin-meetings', () => {
700
742
  };
701
743
 
702
744
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
703
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
745
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
704
746
  updatedObjects: [
705
747
  // first, an update
706
748
  {htMeta: {elementId: {type: 'locus'}}, data: newLocus1},
@@ -730,7 +772,7 @@ describe('plugin-meetings', () => {
730
772
  };
731
773
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
732
774
  // with 1 participant added, 1 updated, and 1 removed
733
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
775
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
734
776
  updatedObjects: [
735
777
  {htMeta: {elementId: {type: 'participant', id: 'fake-ht-participant-1'}}, data: null},
736
778
  {
@@ -774,7 +816,7 @@ describe('plugin-meetings', () => {
774
816
  };
775
817
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
776
818
  // with 1 participant added, 1 updated, and 1 removed
777
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
819
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
778
820
  updatedObjects: [
779
821
  {htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-1'}}, data: null},
780
822
  {
@@ -807,7 +849,7 @@ describe('plugin-meetings', () => {
807
849
  };
808
850
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
809
851
  // with 1 embedded app added, 1 updated, and 1 removed
810
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
852
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
811
853
  updatedObjects: [
812
854
  {htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1'}}, data: null},
813
855
  {
@@ -844,7 +886,7 @@ describe('plugin-meetings', () => {
844
886
  };
845
887
 
846
888
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
847
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
889
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
848
890
  updatedObjects: [
849
891
  {
850
892
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2'}},
@@ -883,7 +925,7 @@ describe('plugin-meetings', () => {
883
925
  };
884
926
 
885
927
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
886
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
928
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
887
929
  updatedObjects: [
888
930
  {
889
931
  htMeta: {elementId: {type: 'controlentry', id: 'control-1'}},
@@ -911,7 +953,7 @@ describe('plugin-meetings', () => {
911
953
 
912
954
  it('should process locus update correctly when CONTROL object is received with no data', () => {
913
955
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
914
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
956
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
915
957
  updatedObjects: [
916
958
  {
917
959
  htMeta: {elementId: {type: 'controlentry', id: 'some-control-id'}},
@@ -935,7 +977,7 @@ describe('plugin-meetings', () => {
935
977
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
936
978
 
937
979
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
938
- locusInfoUpdateCallback(MEETING_ENDED);
980
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
939
981
 
940
982
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
941
983
  assert.calledOnceWithExactly(
@@ -953,7 +995,7 @@ describe('plugin-meetings', () => {
953
995
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
954
996
 
955
997
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
956
- locusInfoUpdateCallback(MEETING_ENDED);
998
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
957
999
 
958
1000
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
959
1001
  assert.notCalled(destroyStub);
@@ -963,7 +1005,7 @@ describe('plugin-meetings', () => {
963
1005
  const createdHashTreeParser = locusInfo.hashTreeParsers.get('fake-locus-url');
964
1006
  createdHashTreeParser.initializedFromHashTree = false;
965
1007
 
966
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1008
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
967
1009
  updatedObjects: [
968
1010
  {
969
1011
  htMeta: {elementId: {type: 'self'}},
@@ -978,7 +1020,7 @@ describe('plugin-meetings', () => {
978
1020
  });
979
1021
 
980
1022
  it('should set forceReplaceMembers to false on subsequent updates (initializedFromHashTree is true)', () => {
981
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1023
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
982
1024
  updatedObjects: [
983
1025
  {
984
1026
  htMeta: {elementId: {type: 'self'}},
@@ -994,7 +1036,7 @@ describe('plugin-meetings', () => {
994
1036
  it('should copy participant data to self when participant matches self identity and state is LEFT with reason MOVED', () => {
995
1037
  locusInfo.self = {id: 'fake-self', identity: 'user-123'};
996
1038
 
997
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1039
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
998
1040
  updatedObjects: [
999
1041
  {
1000
1042
  htMeta: {elementId: {type: 'participant', id: 99}},
@@ -2024,7 +2066,7 @@ describe('plugin-meetings', () => {
2024
2066
  function: 'updateSelf',
2025
2067
  },
2026
2068
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2027
- {muted: true, unmuteAllowed: true}
2069
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2028
2070
  );
2029
2071
 
2030
2072
  // but sometimes "previous self" is defined, but without controls.audio.muted, so we test this here:
@@ -2039,7 +2081,7 @@ describe('plugin-meetings', () => {
2039
2081
  function: 'updateSelf',
2040
2082
  },
2041
2083
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2042
- {muted: true, unmuteAllowed: true}
2084
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2043
2085
  );
2044
2086
  });
2045
2087
 
@@ -2098,7 +2140,7 @@ describe('plugin-meetings', () => {
2098
2140
  function: 'updateSelf',
2099
2141
  },
2100
2142
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2101
- {muted: true, unmuteAllowed: true}
2143
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2102
2144
  );
2103
2145
  });
2104
2146
 
@@ -2237,7 +2279,7 @@ describe('plugin-meetings', () => {
2237
2279
  function: 'updateSelf',
2238
2280
  },
2239
2281
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2240
- {muted: true, unmuteAllowed: false}
2282
+ {muted: true, unmuteAllowed: false, modifiedBy: null}
2241
2283
  );
2242
2284
 
2243
2285
  // now change only disallowUnmute
@@ -2255,7 +2297,28 @@ describe('plugin-meetings', () => {
2255
2297
  function: 'updateSelf',
2256
2298
  },
2257
2299
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2258
- {muted: true, unmuteAllowed: true}
2300
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2301
+ );
2302
+ });
2303
+
2304
+ it('should include modifiedBy in payload when muted by host', () => {
2305
+ locusInfo.webex.internal.device.url = self.deviceUrl;
2306
+ locusInfo.updateSelf(self);
2307
+ const newSelf = cloneDeep(self);
2308
+ newSelf.controls.audio.muted = true;
2309
+ newSelf.controls.audio.meta = {modifiedBy: 'host-uuid-123'};
2310
+
2311
+ locusInfo.emitScoped = sinon.stub();
2312
+ locusInfo.updateSelf(newSelf);
2313
+
2314
+ assert.calledWith(
2315
+ locusInfo.emitScoped,
2316
+ {
2317
+ file: 'locus-info',
2318
+ function: 'updateSelf',
2319
+ },
2320
+ LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2321
+ {muted: true, unmuteAllowed: true, modifiedBy: 'host-uuid-123'}
2259
2322
  );
2260
2323
  });
2261
2324
 
@@ -3154,13 +3217,14 @@ describe('plugin-meetings', () => {
3154
3217
  const createMockParser = (state = 'active') => ({
3155
3218
  state,
3156
3219
  stop: sinon.stub(),
3157
- resume: sinon.stub(),
3220
+ resumeFromMessage: sinon.stub(),
3158
3221
  handleMessage: sinon.stub(),
3159
3222
  });
3160
3223
 
3161
3224
  const createSelfElementWithReplaces = (replacedLocusUrl, replacedAt) => ({
3162
3225
  htMeta: {elementId: {type: 'Self'}},
3163
3226
  data: {
3227
+ deviceUrl,
3164
3228
  devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3165
3229
  },
3166
3230
  });
@@ -3236,7 +3300,7 @@ describe('plugin-meetings', () => {
3236
3300
  stateElementsMessage: message,
3237
3301
  });
3238
3302
 
3239
- assert.calledOnce(parserA.resume);
3303
+ assert.calledOnce(parserA.resumeFromMessage);
3240
3304
  assert.calledOnce(parserB.stop);
3241
3305
  });
3242
3306
 
@@ -3259,7 +3323,7 @@ describe('plugin-meetings', () => {
3259
3323
  stateElementsMessage: message,
3260
3324
  });
3261
3325
 
3262
- assert.notCalled(parserA.resume);
3326
+ assert.notCalled(parserA.resumeFromMessage);
3263
3327
  assert.notCalled(parserB.stop);
3264
3328
  });
3265
3329
 
@@ -3278,7 +3342,7 @@ describe('plugin-meetings', () => {
3278
3342
  stateElementsMessage: message,
3279
3343
  });
3280
3344
 
3281
- assert.notCalled(parserA.resume);
3345
+ assert.notCalled(parserA.resumeFromMessage);
3282
3346
  assert.notCalled(parserA.handleMessage);
3283
3347
  });
3284
3348
 
@@ -3362,6 +3426,51 @@ describe('plugin-meetings', () => {
3362
3426
 
3363
3427
  assert.calledOnceWithExactly(parserA.handleMessage, message);
3364
3428
  });
3429
+
3430
+ it('should send mismatch metric when eventType is not HASH_TREE_DATA_UPDATED', () => {
3431
+ const locusUrlA = 'http://locus-url-A.com';
3432
+ const parserA = {state: 'active', handleMessage: sinon.stub()};
3433
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
3434
+
3435
+ locusInfo.parse(mockMeeting, {
3436
+ eventType: LOCUSEVENT.SELF_CHANGED,
3437
+ stateElementsMessage: {locusUrl: locusUrlA, locusStateElements: [], dataSets: []},
3438
+ });
3439
+
3440
+ assert.calledOnceWithExactly(
3441
+ sendBehavioralMetricStub,
3442
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3443
+ {
3444
+ correlationId: mockMeeting.correlationId,
3445
+ message: `got ${LOCUSEVENT.SELF_CHANGED}, expected ${LOCUSEVENT.HASH_TREE_DATA_UPDATED}`,
3446
+ }
3447
+ );
3448
+ assert.notCalled(parserA.handleMessage);
3449
+ });
3450
+ });
3451
+
3452
+ describe('#sendClassicVsHashTreeMismatchMetric', () => {
3453
+ it('should send the metric when called for the first time', () => {
3454
+ locusInfo.sendClassicVsHashTreeMismatchMetric(mockMeeting, 'some mismatch');
3455
+
3456
+ assert.calledOnceWithExactly(
3457
+ sendBehavioralMetricStub,
3458
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3459
+ {
3460
+ correlationId: mockMeeting.correlationId,
3461
+ message: 'some mismatch',
3462
+ }
3463
+ );
3464
+ });
3465
+
3466
+ it('should send the metric up to 5 times and stop after that', () => {
3467
+ for (let i = 0; i < 7; i += 1) {
3468
+ locusInfo.sendClassicVsHashTreeMismatchMetric(mockMeeting, `mismatch ${i}`);
3469
+ }
3470
+
3471
+ assert.callCount(sendBehavioralMetricStub, 5);
3472
+ assert.equal(locusInfo.classicVsHashTreeMismatchMetricCounter, 5);
3473
+ });
3365
3474
  });
3366
3475
 
3367
3476
  describe('#handleLocusAPIResponse', () => {
@@ -3417,19 +3526,174 @@ describe('plugin-meetings', () => {
3417
3526
  assert.calledOnceWithExactly(locusInfo.handleLocusDelta, fakeLocus, mockMeeting);
3418
3527
  });
3419
3528
 
3420
- it('should send mismatch metric when hash tree parser exists but dataSets are missing in wrapped response', () => {
3529
+ it('should send mismatch metric in classic mode when wrapped response has dataSets', () => {
3421
3530
  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,
3531
+ sinon.stub(locusInfo, 'handleLocusDelta');
3532
+
3533
+ locusInfo.handleLocusAPIResponse(mockMeeting, {
3534
+ locus: fakeLocus,
3535
+ dataSets: [{name: 'dataset1', url: 'test-url'}],
3426
3536
  });
3427
- sinon.stub(locusInfo, 'sendClassicVsHashTreeMismatchMetric');
3428
3537
 
3429
- locusInfo.handleLocusAPIResponse(mockMeeting, {locus: fakeLocus});
3538
+ assert.calledOnceWithExactly(
3539
+ sendBehavioralMetricStub,
3540
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3541
+ {
3542
+ correlationId: mockMeeting.correlationId,
3543
+ message: 'unexpected hash tree dataSets in API response',
3544
+ }
3545
+ );
3546
+ assert.calledOnce(locusInfo.handleLocusDelta);
3547
+ });
3430
3548
 
3431
- assert.calledOnce(locusInfo.sendClassicVsHashTreeMismatchMetric);
3432
- assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
3549
+ describe('parser switch via API response', () => {
3550
+ const deviceUrl = 'http://device-url.com';
3551
+ const locusUrlA = 'http://locus-url-A.com';
3552
+ const locusUrlB = 'http://locus-url-B.com';
3553
+
3554
+ let HashTreeParserStub;
3555
+
3556
+ const createMockApiParser = (state = 'active') => ({
3557
+ state,
3558
+ stop: sinon.stub(),
3559
+ resumeFromApiResponse: sinon.stub(),
3560
+ handleLocusUpdate: sinon.stub(),
3561
+ initializeFromGetLociResponse: sinon.stub(),
3562
+ });
3563
+
3564
+ const createLocusWithReplaces = (url, replacedLocusUrl, replacedAt) => ({
3565
+ url,
3566
+ self: {
3567
+ devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3568
+ },
3569
+ });
3570
+
3571
+ const createLocusWithoutReplaces = (url) => ({
3572
+ url,
3573
+ self: {devices: [{url: deviceUrl}]},
3574
+ });
3575
+
3576
+ beforeEach(() => {
3577
+ locusInfo.webex.internal.device.url = deviceUrl;
3578
+ HashTreeParserStub = sinon
3579
+ .stub(HashTreeParserModule, 'default')
3580
+ .returns(createMockApiParser());
3581
+ });
3582
+
3583
+ it('should create a new parser and initialize it when no entry exists for the locusUrl', () => {
3584
+ // existing parser for a different url so hashTreeParsers.size > 0
3585
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: createMockApiParser(), initializedFromHashTree: true});
3586
+
3587
+ const locus = createLocusWithReplaces(locusUrlB, locusUrlA, '2026-01-01T00:00:00Z');
3588
+ sinon.stub(locusInfo, 'handleLocusDelta');
3589
+
3590
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3591
+
3592
+ assert.isTrue(locusInfo.hashTreeParsers.has(locusUrlB));
3593
+ const newEntry = locusInfo.hashTreeParsers.get(locusUrlB);
3594
+ assert.isFalse(newEntry.initializedFromHashTree);
3595
+
3596
+ // the stub returns the mock, so initializeFromGetLociResponse should be called on it
3597
+ const createdParser = HashTreeParserStub.returnValues[0];
3598
+ assert.calledOnceWithExactly(createdParser.initializeFromGetLociResponse, locus);
3599
+ assert.notCalled(locusInfo.handleLocusDelta);
3600
+ });
3601
+
3602
+ it('should reactivate a stopped parser when replaces info is newer', () => {
3603
+ const parserA = createMockApiParser('stopped');
3604
+ const parserB = createMockApiParser('active');
3605
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-01-01T00:00:00Z', initializedFromHashTree: true});
3606
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3607
+
3608
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-02-01T00:00:00Z');
3609
+
3610
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3611
+
3612
+ assert.calledOnce(parserA.resumeFromApiResponse);
3613
+ assert.calledWithExactly(parserA.resumeFromApiResponse, locus);
3614
+ assert.calledOnce(parserB.stop);
3615
+ assert.equal(locusInfo.hashTreeParsers.get(locusUrlB).replacedAt, '2026-02-01T00:00:00Z');
3616
+ assert.isFalse(locusInfo.hashTreeParsers.get(locusUrlA).initializedFromHashTree);
3617
+ });
3618
+
3619
+ it('should not reactivate a stopped parser when replaces info is not newer', () => {
3620
+ const parserA = createMockApiParser('stopped');
3621
+ const parserB = createMockApiParser('active');
3622
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-03-01T00:00:00Z', initializedFromHashTree: true});
3623
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3624
+
3625
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-01-01T00:00:00Z');
3626
+
3627
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3628
+
3629
+ assert.notCalled(parserA.resumeFromApiResponse);
3630
+ assert.notCalled(parserB.stop);
3631
+ });
3632
+
3633
+ it('should not reactivate a stopped parser when no replaces info is available', () => {
3634
+ const parserA = createMockApiParser('stopped');
3635
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
3636
+
3637
+ const locus = createLocusWithoutReplaces(locusUrlA);
3638
+
3639
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3640
+
3641
+ assert.notCalled(parserA.resumeFromApiResponse);
3642
+ });
3643
+ });
3644
+ });
3645
+
3646
+ describe('#syncAllHashTreeDatasets', () => {
3647
+ it('should call syncAllDatasets on each parser that has an entry', async () => {
3648
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3649
+ const parser2 = {syncAllDatasets: sinon.stub().resolves()};
3650
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3651
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3652
+
3653
+ await locusInfo.syncAllHashTreeDatasets();
3654
+
3655
+ assert.calledOnce(parser1.syncAllDatasets);
3656
+ assert.calledOnce(parser2.syncAllDatasets);
3657
+ });
3658
+
3659
+ it('should skip parser entries without a parser object', async () => {
3660
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3661
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3662
+ locusInfo.hashTreeParsers.set('url2', {parser: undefined});
3663
+
3664
+ await locusInfo.syncAllHashTreeDatasets();
3665
+
3666
+ assert.calledOnce(parser1.syncAllDatasets);
3667
+ });
3668
+
3669
+ it('should await each parsers syncAllDatasets sequentially', async () => {
3670
+ const callOrder = [];
3671
+ const parser1 = {syncAllDatasets: sinon.stub().callsFake(() => {
3672
+ callOrder.push('start1');
3673
+ return new Promise((resolve) => {
3674
+ setTimeout(() => {
3675
+ callOrder.push('end1');
3676
+ resolve();
3677
+ }, 100);
3678
+ });
3679
+ })};
3680
+ const parser2 = {syncAllDatasets: sinon.stub().callsFake(() => {
3681
+ callOrder.push('start2');
3682
+ return Promise.resolve();
3683
+ })};
3684
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3685
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3686
+
3687
+ const clock = sinon.useFakeTimers();
3688
+ const promise = locusInfo.syncAllHashTreeDatasets();
3689
+ // parser1 started but parser2 not yet
3690
+ assert.deepEqual(callOrder, ['start1']);
3691
+
3692
+ await clock.tickAsync(100);
3693
+ await promise;
3694
+ // parser1 finished, then parser2 started and finished
3695
+ assert.deepEqual(callOrder, ['start1', 'end1', 'start2']);
3696
+ clock.restore();
3433
3697
  });
3434
3698
  });
3435
3699
 
@@ -3521,49 +3785,23 @@ describe('plugin-meetings', () => {
3521
3785
  assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
3522
3786
  });
3523
3787
 
3524
- it('#updateLocusInfo ignores breakout LEFT message', () => {
3525
- const newLocus = {
3526
- self: {
3527
- reason: 'MOVED',
3528
- state: 'LEFT',
3529
- },
3530
- };
3788
+ it('#updateLocusInfo ignores locus when isSelfMovedOrBreakoutEnded returns true', () => {
3789
+ const newLocus = {self: {state: 'JOINED'}};
3790
+
3791
+ sinon.stub(MeetingsUtil, 'isSelfMovedOrBreakoutEnded').returns(true);
3531
3792
 
3532
3793
  locusInfo.updateControls = sinon.stub();
3533
- locusInfo.updateConversationUrl = sinon.stub();
3534
- locusInfo.updateCreated = sinon.stub();
3535
3794
  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
3795
  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
3796
 
3549
3797
  locusInfo.updateLocusInfo(newLocus);
3550
3798
 
3799
+ assert.calledOnceWithExactly(MeetingsUtil.isSelfMovedOrBreakoutEnded, newLocus);
3551
3800
  assert.notCalled(locusInfo.updateControls);
3552
- assert.notCalled(locusInfo.updateConversationUrl);
3553
- assert.notCalled(locusInfo.updateCreated);
3554
3801
  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
3802
  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);
3803
+
3804
+ MeetingsUtil.isSelfMovedOrBreakoutEnded.restore();
3567
3805
  });
3568
3806
 
3569
3807
  it('#updateLocusInfo puts the Locus DTO top level properties at the right place in LocusInfo class', () => {
@@ -4413,6 +4651,31 @@ describe('plugin-meetings', () => {
4413
4651
  });
4414
4652
  });
4415
4653
 
4654
+ describe('#cleanUp', () => {
4655
+ it('calls cleanUp on all hash tree parsers and clears maps', () => {
4656
+ const parser1 = {cleanUp: sinon.stub()};
4657
+ const parser2 = {cleanUp: sinon.stub()};
4658
+
4659
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1, initializedFromHashTree: true});
4660
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2, initializedFromHashTree: true});
4661
+ locusInfo.hashTreeObjectId2ParticipantId.set(1, 'participant1');
4662
+
4663
+ locusInfo.cleanUp();
4664
+
4665
+ assert.calledOnce(parser1.cleanUp);
4666
+ assert.calledOnce(parser2.cleanUp);
4667
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4668
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4669
+ });
4670
+
4671
+ it('works when there are no hash tree parsers', () => {
4672
+ locusInfo.cleanUp();
4673
+
4674
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4675
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4676
+ });
4677
+ });
4678
+
4416
4679
  describe('#handleOneonOneEvent', () => {
4417
4680
  beforeEach(() => {
4418
4681
  locusInfo.emitScoped = sinon.stub();
@@ -4455,6 +4718,9 @@ describe('plugin-meetings', () => {
4455
4718
  });
4456
4719
 
4457
4720
  describe('#isMeetingActive', () => {
4721
+ beforeEach(() => {
4722
+ webex.internal.newMetrics.submitClientEvent.resetHistory();
4723
+ });
4458
4724
  forEach([_CALL_, _SIP_BRIDGE_, _SPACE_SHARE_], (type) => {
4459
4725
  describe(`type = ${type}`, () => {
4460
4726
  it('sends client event correctly for state = inactive', () => {
@@ -4521,7 +4787,7 @@ describe('plugin-meetings', () => {
4521
4787
  });
4522
4788
  });
4523
4789
 
4524
- it('sends client event correctly for state = MEETING_INACTIVE_TERMINATING', () => {
4790
+ it('sends client event correctly for state = MEETING_INACTIVE', () => {
4525
4791
  locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
4526
4792
  locusInfo.parsedLocus = {
4527
4793
  fullState: {
@@ -4543,7 +4809,7 @@ describe('plugin-meetings', () => {
4543
4809
  });
4544
4810
  });
4545
4811
 
4546
- it('sends client event correctly for state = FULLSTATE_REMOVED', () => {
4812
+ it('does not send client event when state = INACTIVE and endMeetingReason = BREAKOUT_ENDED', () => {
4547
4813
  locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
4548
4814
  locusInfo.parsedLocus = {
4549
4815
  fullState: {
@@ -4552,17 +4818,41 @@ describe('plugin-meetings', () => {
4552
4818
  };
4553
4819
 
4554
4820
  locusInfo.fullState = {
4555
- removed: true,
4821
+ state: LOCUS.STATE.INACTIVE,
4822
+ endMeetingReason: 'BREAKOUT_ENDED',
4556
4823
  };
4557
4824
 
4558
4825
  locusInfo.isMeetingActive();
4559
4826
 
4560
- assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
4561
- name: 'client.call.remote-ended',
4562
- options: {
4563
- meetingId: locusInfo.meetingId,
4827
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
4828
+ });
4829
+
4830
+ it('sends client event correctly for state self removed', () => {
4831
+ locusInfo.emitScoped = sinon.stub();
4832
+ locusInfo.parsedLocus = {
4833
+ fullState: {
4834
+ type: _MEETING_,
4564
4835
  },
4565
- });
4836
+ self: {
4837
+ removed: true,
4838
+ }
4839
+ };
4840
+
4841
+ locusInfo.isMeetingActive();
4842
+
4843
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
4844
+ assert.calledOnceWithExactly(
4845
+ locusInfo.emitScoped,
4846
+ {
4847
+ file: 'locus-info',
4848
+ function: 'isMeetingActive',
4849
+ },
4850
+ EVENTS.DESTROY_MEETING,
4851
+ {
4852
+ reason: MEETING_REMOVED_REASON.SELF_REMOVED,
4853
+ shouldLeave: false,
4854
+ }
4855
+ );
4566
4856
  });
4567
4857
  });
4568
4858
 
@@ -4954,6 +5244,31 @@ describe('plugin-meetings', () => {
4954
5244
  );
4955
5245
  assert.notCalled(getTheLocusToUpdateStub);
4956
5246
  });
5247
+
5248
+ it('should call handleLocusAPIResponse for SDK_LOCUS_FROM_SYNC_MEETINGS when hash tree parsers exist', () => {
5249
+ const fakeLocusUrl = 'http://locus-url.com';
5250
+ const fakeLocus = {url: fakeLocusUrl, fullState: {state: 'ACTIVE'}};
5251
+ const mockHashTreeParser = {
5252
+ handleMessage: sinon.stub(),
5253
+ handleLocusUpdate: sinon.stub(),
5254
+ };
5255
+ locusInfo.hashTreeParsers.set(fakeLocusUrl, {
5256
+ parser: mockHashTreeParser,
5257
+ initializedFromHashTree: true,
5258
+ });
5259
+
5260
+ sinon.stub(locusInfo, 'handleLocusDelta');
5261
+
5262
+ locusInfo.parse(mockMeeting, {
5263
+ eventType: LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS,
5264
+ locus: fakeLocus,
5265
+ });
5266
+
5267
+ // should route through handleLocusAPIResponse which passes unwrapped LocusDTO to parser
5268
+ assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
5269
+ assert.notCalled(mockHashTreeParser.handleMessage);
5270
+ assert.notCalled(locusInfo.handleLocusDelta);
5271
+ });
4957
5272
  });
4958
5273
  });
4959
5274
 
@@ -5146,6 +5461,7 @@ describe('plugin-meetings', () => {
5146
5461
  return {
5147
5462
  htMeta: {elementId: {type: 'Self'}},
5148
5463
  data: {
5464
+ deviceUrl,
5149
5465
  devices,
5150
5466
  },
5151
5467
  };