@webex/plugin-meetings 3.12.0-next.3 → 3.12.0-next.31

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 +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/constants.js +3 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/constants.js +11 -1
  8. package/dist/controls-options-manager/constants.js.map +1 -1
  9. package/dist/controls-options-manager/index.js +23 -21
  10. package/dist/controls-options-manager/index.js.map +1 -1
  11. package/dist/controls-options-manager/util.js +91 -0
  12. package/dist/controls-options-manager/util.js.map +1 -1
  13. package/dist/hashTree/constants.js +10 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTreeParser.js +550 -346
  16. package/dist/hashTree/hashTreeParser.js.map +1 -1
  17. package/dist/hashTree/utils.js +22 -0
  18. package/dist/hashTree/utils.js.map +1 -1
  19. package/dist/interceptors/locusRetry.js +23 -8
  20. package/dist/interceptors/locusRetry.js.map +1 -1
  21. package/dist/interpretation/index.js +1 -1
  22. package/dist/interpretation/siLanguage.js +1 -1
  23. package/dist/locus-info/index.js +222 -61
  24. package/dist/locus-info/index.js.map +1 -1
  25. package/dist/meeting/index.js +372 -292
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/util.js +1 -0
  28. package/dist/meeting/util.js.map +1 -1
  29. package/dist/meetings/index.js +146 -62
  30. package/dist/meetings/index.js.map +1 -1
  31. package/dist/meetings/util.js +39 -5
  32. package/dist/meetings/util.js.map +1 -1
  33. package/dist/member/index.js +10 -0
  34. package/dist/member/index.js.map +1 -1
  35. package/dist/member/types.js.map +1 -1
  36. package/dist/member/util.js +3 -0
  37. package/dist/member/util.js.map +1 -1
  38. package/dist/metrics/constants.js +5 -1
  39. package/dist/metrics/constants.js.map +1 -1
  40. package/dist/multistream/sendSlotManager.js +116 -2
  41. package/dist/multistream/sendSlotManager.js.map +1 -1
  42. package/dist/types/constants.d.ts +1 -0
  43. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  44. package/dist/types/hashTree/constants.d.ts +1 -0
  45. package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
  46. package/dist/types/hashTree/utils.d.ts +11 -0
  47. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  48. package/dist/types/locus-info/index.d.ts +38 -5
  49. package/dist/types/meeting/index.d.ts +11 -0
  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/types/metrics/constants.d.ts +4 -0
  54. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  55. package/dist/webinar/index.js +301 -226
  56. package/dist/webinar/index.js.map +1 -1
  57. package/package.json +16 -16
  58. package/src/constants.ts +1 -0
  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 +273 -154
  64. package/src/hashTree/utils.ts +17 -0
  65. package/src/interceptors/locusRetry.ts +25 -4
  66. package/src/locus-info/index.ts +233 -79
  67. package/src/meeting/index.ts +98 -11
  68. package/src/meeting/util.ts +1 -0
  69. package/src/meetings/index.ts +58 -34
  70. package/src/meetings/util.ts +44 -1
  71. package/src/member/index.ts +10 -0
  72. package/src/member/types.ts +1 -0
  73. package/src/member/util.ts +3 -0
  74. package/src/metrics/constants.ts +5 -0
  75. package/src/multistream/sendSlotManager.ts +97 -3
  76. package/src/webinar/index.ts +75 -1
  77. package/test/unit/spec/controls-options-manager/index.js +114 -6
  78. package/test/unit/spec/controls-options-manager/util.js +165 -0
  79. package/test/unit/spec/hashTree/hashTreeParser.ts +839 -37
  80. package/test/unit/spec/hashTree/utils.ts +88 -1
  81. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  82. package/test/unit/spec/locus-info/index.js +262 -64
  83. package/test/unit/spec/meeting/index.js +54 -36
  84. package/test/unit/spec/meeting/utils.js +4 -0
  85. package/test/unit/spec/meetings/index.js +190 -8
  86. package/test/unit/spec/meetings/utils.js +124 -0
  87. package/test/unit/spec/member/index.js +7 -0
  88. package/test/unit/spec/member/util.js +24 -0
  89. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  90. package/test/unit/spec/webinar/index.ts +60 -0
@@ -38,6 +38,7 @@ import {
38
38
  import {
39
39
  ConnectionState,
40
40
  MediaConnectionEventNames,
41
+ MediaCodecMimeType,
41
42
  StatsAnalyzerEventNames,
42
43
  StatsMonitorEventNames,
43
44
  Errors,
@@ -1981,11 +1982,12 @@ describe('plugin-meetings', () => {
1981
1982
  describe('#handleLLMOnline', () => {
1982
1983
  beforeEach(() => {
1983
1984
  webex.internal.llm.off = sinon.stub();
1985
+ webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
1986
+ webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
1984
1987
  });
1985
1988
 
1986
- it('turns off llm online, emits transcription connected events', () => {
1989
+ it('emits transcription connected events', () => {
1987
1990
  meeting.handleLLMOnline();
1988
- assert.calledOnceWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
1989
1991
  assert.calledWith(
1990
1992
  TriggerProxy.trigger,
1991
1993
  sinon.match.instanceOf(Meeting),
@@ -1996,6 +1998,24 @@ describe('plugin-meetings', () => {
1996
1998
  EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1997
1999
  );
1998
2000
  });
2001
+
2002
+ it('restores transcription subscription when caption intent is enabled', () => {
2003
+ webex.internal.voicea.getIsCaptionBoxOn.returns(true);
2004
+
2005
+ meeting.handleLLMOnline();
2006
+
2007
+ assert.calledOnceWithExactly(webex.internal.voicea.updateSubchannelSubscriptions, {
2008
+ subscribe: ['transcription'],
2009
+ });
2010
+ });
2011
+
2012
+ it('does not restore transcription subscription when caption intent is disabled', () => {
2013
+ webex.internal.voicea.getIsCaptionBoxOn.returns(false);
2014
+
2015
+ meeting.handleLLMOnline();
2016
+
2017
+ assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
2018
+ });
1999
2019
  });
2000
2020
 
2001
2021
  describe('#join', () => {
@@ -2015,6 +2035,7 @@ describe('plugin-meetings', () => {
2015
2035
  it('should have #join', () => {
2016
2036
  assert.exists(meeting.join);
2017
2037
  });
2038
+
2018
2039
  beforeEach(() => {
2019
2040
  setCorrelationIdSpy = sinon.spy(meeting, 'setCorrelationId');
2020
2041
  meeting.setLocus = sinon.stub().returns(true);
@@ -2168,7 +2189,6 @@ describe('plugin-meetings', () => {
2168
2189
  await meeting.join().catch(() => {
2169
2190
  assert.calledOnce(MeetingUtil.joinMeeting);
2170
2191
 
2171
- // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2172
2192
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2173
2193
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2174
2194
  name: 'client.call.initiated',
@@ -2200,6 +2220,7 @@ describe('plugin-meetings', () => {
2200
2220
  });
2201
2221
  });
2202
2222
  });
2223
+
2203
2224
  describe('lmm, transcription & permissionTokenRefresh decoupling', () => {
2204
2225
  beforeEach(() => {
2205
2226
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
@@ -2270,7 +2291,6 @@ describe('plugin-meetings', () => {
2270
2291
  const locusInfoParseStub = sinon.stub(meeting.locusInfo, 'parse');
2271
2292
  sinon.stub(meeting, 'isJoined').returns(true);
2272
2293
 
2273
- // Set up llm.on stub to capture the registered listener when updateLLMConnection is called
2274
2294
  let locusLLMEventListener;
2275
2295
  meeting.webex.internal.llm.on = sinon.stub().callsFake((eventName, callback) => {
2276
2296
  if (eventName === 'event:locus.state_message') {
@@ -2279,16 +2299,12 @@ describe('plugin-meetings', () => {
2279
2299
  });
2280
2300
  meeting.webex.internal.llm.off = sinon.stub();
2281
2301
 
2282
- // we need the real meeting.updateLLMConnection not the mock
2283
2302
  meeting.updateLLMConnection.restore();
2284
2303
 
2285
- // Call updateLLMConnection to register the listener
2286
2304
  await meeting.updateLLMConnection();
2287
2305
 
2288
- // Verify the listener was registered and we captured it
2289
2306
  assert.isDefined(locusLLMEventListener, 'LLM event listener should be registered');
2290
2307
 
2291
- // Now trigger the event
2292
2308
  const eventData = {
2293
2309
  eventType: 'locus.state_message',
2294
2310
  stateElementsMessage: {
@@ -2308,13 +2324,10 @@ describe('plugin-meetings', () => {
2308
2324
  sinon.stub(meeting.webex.internal.llm, 'hasEverConnected').value(true);
2309
2325
  sinon.stub(meeting.webex.internal.llm, 'registerAndConnect').resolves({});
2310
2326
 
2311
- // Restore the real updateLLMConnection
2312
2327
  meeting.updateLLMConnection.restore();
2313
2328
 
2314
- // Call updateLLMConnection to start the timer
2315
2329
  await meeting.updateLLMConnection();
2316
2330
 
2317
- // Fast forward time by 3 minutes
2318
2331
  fakeClock.tick(3 * 60 * 1000);
2319
2332
 
2320
2333
  assert.calledWith(
@@ -2339,18 +2352,14 @@ describe('plugin-meetings', () => {
2339
2352
  .stub(meeting.webex.internal.llm, 'getDatachannelUrl')
2340
2353
  .returns('https://datachannel1.example.com');
2341
2354
 
2342
- // Restore the real updateLLMConnection
2343
2355
  meeting.updateLLMConnection.restore();
2344
2356
 
2345
- // First, connect LLM and start the timer
2346
2357
  isJoinedStub.returns(true);
2347
2358
  meeting.webex.internal.llm.isConnected.returns(false);
2348
2359
  await meeting.updateLLMConnection();
2349
2360
 
2350
- // Verify timer was started
2351
2361
  assert.exists(meeting.llmHealthCheckTimer);
2352
2362
 
2353
- // Now simulate that we're no longer joined
2354
2363
  isJoinedStub.returns(false);
2355
2364
  meeting.webex.internal.llm.isConnected.returns(true);
2356
2365
 
@@ -2358,10 +2367,8 @@ describe('plugin-meetings', () => {
2358
2367
 
2359
2368
  assert.calledOnce(meeting.webex.internal.llm.disconnectLLM);
2360
2369
 
2361
- // Verify the timer was cleared (should be undefined)
2362
2370
  assert.isUndefined(meeting.llmHealthCheckTimer);
2363
2371
 
2364
- // Fast forward time to ensure no metric is sent
2365
2372
  Metrics.sendBehavioralMetric.resetHistory();
2366
2373
  fakeClock.tick(3 * 60 * 1000);
2367
2374
 
@@ -2396,7 +2403,6 @@ describe('plugin-meetings', () => {
2396
2403
  .stub()
2397
2404
  .rejects(new CaptchaError('bad captcha'));
2398
2405
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2399
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil, 'joinMeetingOptions');
2400
2406
 
2401
2407
  try {
2402
2408
  await meeting.join();
@@ -2410,8 +2416,7 @@ describe('plugin-meetings', () => {
2410
2416
  );
2411
2417
  assert.instanceOf(error, CaptchaError);
2412
2418
  assert.equal(error.message, 'bad captcha');
2413
- // should not get to the end promise chain, which does do the join
2414
- assert.notCalled(joinMeetingOptionsSpy);
2419
+ assert.notCalled(MeetingUtil.joinMeeting);
2415
2420
  }
2416
2421
  });
2417
2422
 
@@ -2420,7 +2425,6 @@ describe('plugin-meetings', () => {
2420
2425
  .stub()
2421
2426
  .rejects(new PasswordError('bad password'));
2422
2427
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2423
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
2424
2428
 
2425
2429
  try {
2426
2430
  await meeting.join();
@@ -2434,8 +2438,7 @@ describe('plugin-meetings', () => {
2434
2438
  );
2435
2439
  assert.instanceOf(error, PasswordError);
2436
2440
  assert.equal(error.message, 'bad password');
2437
- // should not get to the end promise chain, which does do the join
2438
- assert.notCalled(joinMeetingOptionsSpy);
2441
+ assert.notCalled(MeetingUtil.joinMeeting);
2439
2442
  }
2440
2443
  });
2441
2444
 
@@ -2444,7 +2447,6 @@ describe('plugin-meetings', () => {
2444
2447
  .stub()
2445
2448
  .rejects(new PermissionError('bad permission'));
2446
2449
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2447
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
2448
2450
 
2449
2451
  try {
2450
2452
  await meeting.join();
@@ -2458,14 +2460,14 @@ describe('plugin-meetings', () => {
2458
2460
  );
2459
2461
  assert.instanceOf(error, PermissionError);
2460
2462
  assert.equal(error.message, 'bad permission');
2461
- // should not get to the end promise chain, which does do the join
2462
- assert.notCalled(joinMeetingOptionsSpy);
2463
+ assert.notCalled(MeetingUtil.joinMeeting);
2463
2464
  }
2464
2465
  });
2465
2466
  });
2466
2467
  });
2467
2468
  });
2468
2469
 
2470
+
2469
2471
  describe('#addMedia', () => {
2470
2472
  const muteStateStub = {
2471
2473
  handleClientRequest: sinon.stub().returns(Promise.resolve(true)),
@@ -9214,8 +9216,8 @@ describe('plugin-meetings', () => {
9214
9216
  const fakeMultistreamRoapMediaConnection = {
9215
9217
  createSendSlot: () => {
9216
9218
  return {
9217
- setCodecParameters: sinon.stub().resolves(),
9218
- deleteCodecParameters: sinon.stub().resolves(),
9219
+ setCustomCodecParameters: sinon.stub().resolves(),
9220
+ markCustomCodecParametersForDeletion: sinon.stub().resolves(),
9219
9221
  };
9220
9222
  },
9221
9223
  };
@@ -9238,27 +9240,29 @@ describe('plugin-meetings', () => {
9238
9240
  }
9239
9241
  );
9240
9242
 
9241
- it('should set the codec parameters when shouldEnableMusicMode is true', async () => {
9243
+ it('should set custom codec parameters when shouldEnableMusicMode is true', async () => {
9242
9244
  await meeting.enableMusicMode(true);
9243
9245
  assert.calledOnceWithExactly(
9244
- meeting.sendSlotManager.getSlot(MediaType.AudioMain).setCodecParameters,
9246
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).setCustomCodecParameters,
9247
+ MediaCodecMimeType.OPUS,
9245
9248
  {
9246
9249
  maxaveragebitrate: '64000',
9247
9250
  maxplaybackrate: '48000',
9248
9251
  }
9249
9252
  );
9250
9253
  assert.notCalled(
9251
- meeting.sendSlotManager.getSlot(MediaType.AudioMain).deleteCodecParameters
9254
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).markCustomCodecParametersForDeletion
9252
9255
  );
9253
9256
  });
9254
9257
 
9255
- it('should set the codec parameters when shouldEnableMusicMode is false', async () => {
9258
+ it('should mark custom codec parameters for deletion when shouldEnableMusicMode is false', async () => {
9256
9259
  await meeting.enableMusicMode(false);
9257
9260
  assert.calledOnceWithExactly(
9258
- meeting.sendSlotManager.getSlot(MediaType.AudioMain).deleteCodecParameters,
9261
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).markCustomCodecParametersForDeletion,
9262
+ MediaCodecMimeType.OPUS,
9259
9263
  ['maxaveragebitrate', 'maxplaybackrate']
9260
9264
  );
9261
- assert.notCalled(meeting.sendSlotManager.getSlot(MediaType.AudioMain).setCodecParameters);
9265
+ assert.notCalled(meeting.sendSlotManager.getSlot(MediaType.AudioMain).setCustomCodecParameters);
9262
9266
  });
9263
9267
  });
9264
9268
 
@@ -10413,14 +10417,24 @@ describe('plugin-meetings', () => {
10413
10417
  );
10414
10418
  done();
10415
10419
  });
10416
- it('listens to the self admitted guest event', (done) => {
10420
+ it('listens to the self admitted guest event without blocking on token prefetch', async () => {
10417
10421
  meeting.stopKeepAlive = sinon.stub();
10418
10422
  meeting.updateLLMConnection = sinon.stub();
10423
+ let resolvePrefetch;
10424
+
10425
+ meeting.ensureDefaultDatachannelTokenAfterAdmit = sinon
10426
+ .stub()
10427
+ .returns(new Promise((resolve) => {
10428
+ resolvePrefetch = resolve;
10429
+ }));
10419
10430
  meeting.rtcMetrics = {
10420
10431
  sendNextMetrics: sinon.stub(),
10421
10432
  };
10433
+
10422
10434
  meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
10435
+
10423
10436
  assert.calledOnceWithExactly(meeting.stopKeepAlive);
10437
+ assert.calledOnceWithExactly(meeting.ensureDefaultDatachannelTokenAfterAdmit);
10424
10438
  assert.calledThrice(TriggerProxy.trigger);
10425
10439
  assert.calledWith(
10426
10440
  TriggerProxy.trigger,
@@ -10439,7 +10453,11 @@ describe('plugin-meetings', () => {
10439
10453
  correlation_id: meeting.correlationId,
10440
10454
  }
10441
10455
  );
10442
- done();
10456
+
10457
+ resolvePrefetch(false);
10458
+ await Promise.resolve();
10459
+
10460
+ assert.calledOnce(meeting.updateLLMConnection);
10443
10461
  });
10444
10462
 
10445
10463
  it('listens to the breakouts changed event', () => {
@@ -60,6 +60,7 @@ describe('plugin-meetings', () => {
60
60
  meeting.annotaion = {cleanUp: sinon.stub()};
61
61
  meeting.getWebexObject = sinon.stub().returns(webex);
62
62
  meeting.simultaneousInterpretation = {cleanUp: sinon.stub()};
63
+ meeting.locusInfo = {cleanUp: sinon.stub()};
63
64
  meeting.trigger = sinon.stub();
64
65
  meeting.webex = webex;
65
66
  meeting.webex.internal.newMetrics.callDiagnosticMetrics =
@@ -89,6 +90,7 @@ describe('plugin-meetings', () => {
89
90
  assert.calledOnceWithExactly(meeting.cleanupLLMConneciton, {throwOnError: false});
90
91
  assert.calledOnce(meeting.breakouts.cleanUp);
91
92
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
93
+ assert.calledOnce(meeting.locusInfo.cleanUp);
92
94
  assert.calledOnce(webex.internal.device.meetingEnded);
93
95
  assert.calledOnceWithExactly(
94
96
  meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
@@ -110,6 +112,7 @@ describe('plugin-meetings', () => {
110
112
  assert.notCalled(meeting.cleanupLLMConneciton);
111
113
  assert.calledOnce(meeting.breakouts.cleanUp);
112
114
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
115
+ assert.calledOnce(meeting.locusInfo.cleanUp);
113
116
  assert.calledOnce(webex.internal.device.meetingEnded);
114
117
  assert.calledOnceWithExactly(
115
118
  meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
@@ -130,6 +133,7 @@ describe('plugin-meetings', () => {
130
133
  assert.notCalled(meeting.cleanupLLMConneciton);
131
134
  assert.calledOnce(meeting.breakouts.cleanUp);
132
135
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
136
+ assert.calledOnce(meeting.locusInfo.cleanUp);
133
137
  assert.calledOnce(webex.internal.device.meetingEnded);
134
138
  assert.calledOnceWithExactly(
135
139
  meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
@@ -91,6 +91,7 @@ describe('plugin-meetings', () => {
91
91
  locusInfo = {
92
92
  parse: sinon.stub().returns(true),
93
93
  updateMainSessionLocusCache: sinon.stub(),
94
+ syncAllHashTreeDatasets: sinon.stub(),
94
95
  };
95
96
  webex = new MockWebex({
96
97
  children: {
@@ -1285,10 +1286,10 @@ describe('plugin-meetings', () => {
1285
1286
  assert.exists(result.dispose);
1286
1287
  });
1287
1288
 
1288
- it('creates noise reduction effect with ST model', async () => {
1289
+ it('creates noise reduction effect with OFMV model', async () => {
1289
1290
  const result = await webex.meetings.createNoiseReductionEffect({
1290
1291
  audioContext: {},
1291
- model: 'st',
1292
+ model: 'ofmv',
1292
1293
  });
1293
1294
 
1294
1295
  assert.exists(result);
@@ -1300,7 +1301,7 @@ describe('plugin-meetings', () => {
1300
1301
  authToken: 'fake_token',
1301
1302
  mode: 'WORKLET',
1302
1303
  avoidSimd: false,
1303
- model: 'st',
1304
+ model: 'ofmv',
1304
1305
  });
1305
1306
  assert.exists(result.enable);
1306
1307
  assert.exists(result.disable);
@@ -1391,7 +1392,7 @@ describe('plugin-meetings', () => {
1391
1392
  it('should have #syncMeetings', () => {
1392
1393
  assert.exists(webex.meetings.syncMeetings);
1393
1394
  });
1394
- it('should do nothing and return a resolved promise if unverified guest', async () => {
1395
+ it('should skip getActiveMeetings but still call syncAllHashTreeDatasets if unverified guest', async () => {
1395
1396
  webex.meetings.request.getActiveMeetings = sinon.stub().returns(
1396
1397
  Promise.resolve({
1397
1398
  loci: [
@@ -1404,13 +1405,23 @@ describe('plugin-meetings', () => {
1404
1405
  webex.credentials.isUnverifiedGuest = true;
1405
1406
  LoggerProxy.logger.info = sinon.stub();
1406
1407
 
1408
+ const mockLocusInfo = {
1409
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1410
+ };
1411
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1412
+ meeting1: {locusInfo: mockLocusInfo},
1413
+ meeting2: {locusInfo: undefined},
1414
+ meeting3: {},
1415
+ });
1416
+
1407
1417
  await webex.meetings.syncMeetings();
1408
1418
 
1409
1419
  assert.notCalled(webex.meetings.request.getActiveMeetings);
1410
1420
  assert.calledWith(
1411
1421
  LoggerProxy.logger.info,
1412
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1422
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1413
1423
  );
1424
+ assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
1414
1425
  });
1415
1426
  describe('succesful requests', () => {
1416
1427
  beforeEach(() => {
@@ -1429,6 +1440,9 @@ describe('plugin-meetings', () => {
1429
1440
  webex.meetings.meetingCollection.getByKey = sinon.stub().returns({
1430
1441
  locusInfo,
1431
1442
  });
1443
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1444
+ meeting1: {locusInfo, locusUrl: url1},
1445
+ });
1432
1446
  });
1433
1447
  it('tests the sync meeting calls for existing meeting', async () => {
1434
1448
  await webex.meetings.syncMeetings();
@@ -1436,6 +1450,7 @@ describe('plugin-meetings', () => {
1436
1450
  assert.calledOnce(webex.meetings.meetingCollection.getByKey);
1437
1451
  assert.calledOnce(locusInfo.parse);
1438
1452
  assert.calledWith(webex.meetings.meetingCollection.getByKey, 'locusUrl', url1);
1453
+ assert.calledOnce(locusInfo.syncAllHashTreeDatasets);
1439
1454
  });
1440
1455
  });
1441
1456
  describe('when meeting is not returned', () => {
@@ -1520,7 +1535,7 @@ describe('plugin-meetings', () => {
1520
1535
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings is not defined', async () => {
1521
1536
  await webex.meetings.syncMeetings();
1522
1537
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1523
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1538
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1524
1539
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1525
1540
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1526
1541
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1532,7 +1547,7 @@ describe('plugin-meetings', () => {
1532
1547
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings === true', async () => {
1533
1548
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: true});
1534
1549
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1535
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1550
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1536
1551
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1537
1552
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1538
1553
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1544,7 +1559,7 @@ describe('plugin-meetings', () => {
1544
1559
  it('destroy any LOCUS meetings that have no active locus url if keepOnlyLocusMeetings === false', async () => {
1545
1560
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1546
1561
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1547
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1562
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1548
1563
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1549
1564
  assert.callCount(destroySpy, 1);
1550
1565
 
@@ -1552,6 +1567,113 @@ describe('plugin-meetings', () => {
1552
1567
  });
1553
1568
  });
1554
1569
  });
1570
+
1571
+ describe('when globalMeetingId preserves breakout meetings', () => {
1572
+ let destroySpy;
1573
+ let cleanUpSpy;
1574
+
1575
+ beforeEach(() => {
1576
+ destroySpy = sinon.spy(webex.meetings, 'destroy');
1577
+ cleanUpSpy = sinon.stub(MeetingUtil, 'cleanUp').returns(Promise.resolve());
1578
+ });
1579
+
1580
+ afterEach(() => {
1581
+ cleanUpSpy.restore();
1582
+ });
1583
+
1584
+ it('should not destroy a meeting whose globalMeetingId matches an active locus', async () => {
1585
+ const meetingCollectionMeetings = {
1586
+ breakoutMeeting: {
1587
+ locusUrl: 'breakout-url',
1588
+ locusInfo: {
1589
+ info: {globalMeetingId: 'gmid-123'},
1590
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1591
+ },
1592
+ sendCallAnalyzerMetrics: sinon.stub(),
1593
+ },
1594
+ };
1595
+
1596
+ webex.meetings.meetingCollection.getAll = sinon
1597
+ .stub()
1598
+ .returns(meetingCollectionMeetings);
1599
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1600
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1601
+ });
1602
+
1603
+ await webex.meetings.syncMeetings();
1604
+
1605
+ assert.notCalled(destroySpy);
1606
+ });
1607
+
1608
+ it('should destroy a meeting whose globalMeetingId does NOT match any active locus', async () => {
1609
+ const meetingCollectionMeetings = {
1610
+ breakoutMeeting: {
1611
+ locusUrl: 'breakout-url',
1612
+ locusInfo: {
1613
+ info: {globalMeetingId: 'gmid-other'},
1614
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1615
+ },
1616
+ sendCallAnalyzerMetrics: sinon.stub(),
1617
+ },
1618
+ };
1619
+
1620
+ webex.meetings.meetingCollection.getAll = sinon
1621
+ .stub()
1622
+ .returns(meetingCollectionMeetings);
1623
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1624
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1625
+ });
1626
+
1627
+ await webex.meetings.syncMeetings();
1628
+
1629
+ assert.calledOnce(destroySpy);
1630
+ assert.calledWith(destroySpy, meetingCollectionMeetings.breakoutMeeting);
1631
+ });
1632
+ });
1633
+
1634
+ describe('syncAllHashTreeDatasets in syncMeetings', () => {
1635
+ it('should call syncAllHashTreeDatasets for multiple meetings, skipping those without locusInfo', async () => {
1636
+ const mockLocusInfo1 = {
1637
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1638
+ };
1639
+ const mockLocusInfo2 = {
1640
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1641
+ };
1642
+
1643
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1644
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1645
+ meeting1: {locusInfo: mockLocusInfo1},
1646
+ meeting2: {locusInfo: undefined},
1647
+ meeting3: {locusInfo: mockLocusInfo2},
1648
+ meeting4: {},
1649
+ });
1650
+
1651
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1652
+
1653
+ assert.calledOnce(mockLocusInfo1.syncAllHashTreeDatasets);
1654
+ assert.calledOnce(mockLocusInfo2.syncAllHashTreeDatasets);
1655
+ });
1656
+
1657
+ it('should not call syncAllHashTreeDatasets when getActiveMeetings throws an error', async () => {
1658
+ const mockLocusInfo = {
1659
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1660
+ };
1661
+
1662
+ webex.meetings.request.getActiveMeetings = sinon.stub().rejects(new Error('network error'));
1663
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1664
+ meeting1: {locusInfo: mockLocusInfo},
1665
+ });
1666
+
1667
+ try {
1668
+ await webex.meetings.syncMeetings();
1669
+ assert.fail('should have thrown');
1670
+ } catch (err) {
1671
+ assert.equal(err.message, 'network error');
1672
+ }
1673
+
1674
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1675
+ });
1676
+ });
1555
1677
  });
1556
1678
  describe('#fetchStaticMeetingLink', () => {
1557
1679
  const conversationUrl = 'conv.fakeconversationurl.com';
@@ -2833,6 +2955,39 @@ describe('plugin-meetings', () => {
2833
2955
  checkCreateMeetingWithNoMeetingInfo(true, true);
2834
2956
  });
2835
2957
 
2958
+ it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
2959
+ // Make destroy actually remove the meeting from the collection
2960
+ // so that getMeetingByType returns null in the finally block
2961
+ webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
2962
+ webex.meetings.meetingCollection.delete(meeting.id);
2963
+ });
2964
+
2965
+ try {
2966
+ await webex.meetings.createMeeting(
2967
+ 'test destination',
2968
+ 'test type',
2969
+ undefined,
2970
+ undefined,
2971
+ undefined,
2972
+ true
2973
+ );
2974
+ assert.fail('should have thrown NoMeetingInfoError');
2975
+ } catch (err) {
2976
+ assert.instanceOf(err, NoMeetingInfoError);
2977
+ }
2978
+
2979
+ assert.calledOnce(webex.meetings.destroy);
2980
+
2981
+ // meeting:added should NOT have been triggered since the meeting was destroyed
2982
+ assert.neverCalledWith(
2983
+ TriggerProxy.trigger,
2984
+ sinon.match.any,
2985
+ sinon.match({function: 'createMeeting'}),
2986
+ 'meeting:added',
2987
+ sinon.match.any
2988
+ );
2989
+ });
2990
+
2836
2991
  it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
2837
2992
  const meeting = await webex.meetings.createMeeting(
2838
2993
  'test destination',
@@ -3426,6 +3581,21 @@ describe('plugin-meetings', () => {
3426
3581
  'Meetings:index#isNeedHandleMainLocus --> self device left&moved in main locus with self joined status, not need to handle'
3427
3582
  );
3428
3583
  });
3584
+
3585
+ it('check breakout ended with self removed, return false', () => {
3586
+ webex.meetings.meetingCollection.getActiveBreakoutLocus = sinon.stub().returns(null);
3587
+ newLocus.self.state = 'LEFT';
3588
+ newLocus.self.reason = 'OTHER';
3589
+ newLocus.self.removed = true;
3590
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3591
+ LoggerProxy.logger.log = sinon.stub();
3592
+ const result = webex.meetings.isNeedHandleMainLocus(meeting, newLocus);
3593
+ assert.equal(result, false);
3594
+ assert.calledWith(
3595
+ LoggerProxy.logger.log,
3596
+ 'Meetings:index#isNeedHandleMainLocus --> self moved main locus with self removed status or with device resource moved, not need to handle'
3597
+ );
3598
+ });
3429
3599
  });
3430
3600
 
3431
3601
  describe('#isNeedHandleLocusDTO', () => {
@@ -3486,6 +3656,18 @@ describe('plugin-meetings', () => {
3486
3656
  const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3487
3657
  assert.equal(result, false);
3488
3658
  });
3659
+ it('breakout session with breakout ended, return false', () => {
3660
+ newLocus.controls.breakout = {
3661
+ sessionType: 'BREAKOUT',
3662
+ };
3663
+ newLocus.self.state = 'LEFT';
3664
+ newLocus.self.reason = 'OTHER';
3665
+ newLocus.self.devices = [];
3666
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3667
+ LoggerProxy.logger.log = sinon.stub();
3668
+ const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3669
+ assert.equal(result, false);
3670
+ });
3489
3671
  it('moved to lobby, return true', () => {
3490
3672
  newLocus.controls.breakout = {
3491
3673
  sessionType: 'MAIN',