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

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
@@ -34,6 +34,7 @@ import {
34
34
  ONLINE,
35
35
  OFFLINE,
36
36
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
37
+ LOCUS_LLM_EVENT,
37
38
  } from '@webex/plugin-meetings/src/constants';
38
39
  import {
39
40
  ConnectionState,
@@ -1982,11 +1983,12 @@ describe('plugin-meetings', () => {
1982
1983
  describe('#handleLLMOnline', () => {
1983
1984
  beforeEach(() => {
1984
1985
  webex.internal.llm.off = sinon.stub();
1986
+ webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
1987
+ webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
1985
1988
  });
1986
1989
 
1987
- it('turns off llm online, emits transcription connected events', () => {
1990
+ it('emits transcription connected events', () => {
1988
1991
  meeting.handleLLMOnline();
1989
- assert.calledOnceWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
1990
1992
  assert.calledWith(
1991
1993
  TriggerProxy.trigger,
1992
1994
  sinon.match.instanceOf(Meeting),
@@ -1997,6 +1999,24 @@ describe('plugin-meetings', () => {
1997
1999
  EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1998
2000
  );
1999
2001
  });
2002
+
2003
+ it('restores transcription subscription when caption intent is enabled', () => {
2004
+ webex.internal.voicea.getIsCaptionBoxOn.returns(true);
2005
+
2006
+ meeting.handleLLMOnline();
2007
+
2008
+ assert.calledOnceWithExactly(webex.internal.voicea.updateSubchannelSubscriptions, {
2009
+ subscribe: ['transcription'],
2010
+ });
2011
+ });
2012
+
2013
+ it('does not restore transcription subscription when caption intent is disabled', () => {
2014
+ webex.internal.voicea.getIsCaptionBoxOn.returns(false);
2015
+
2016
+ meeting.handleLLMOnline();
2017
+
2018
+ assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
2019
+ });
2000
2020
  });
2001
2021
 
2002
2022
  describe('#join', () => {
@@ -2016,6 +2036,7 @@ describe('plugin-meetings', () => {
2016
2036
  it('should have #join', () => {
2017
2037
  assert.exists(meeting.join);
2018
2038
  });
2039
+
2019
2040
  beforeEach(() => {
2020
2041
  setCorrelationIdSpy = sinon.spy(meeting, 'setCorrelationId');
2021
2042
  meeting.setLocus = sinon.stub().returns(true);
@@ -2169,7 +2190,6 @@ describe('plugin-meetings', () => {
2169
2190
  await meeting.join().catch(() => {
2170
2191
  assert.calledOnce(MeetingUtil.joinMeeting);
2171
2192
 
2172
- // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2173
2193
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2174
2194
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2175
2195
  name: 'client.call.initiated',
@@ -2201,6 +2221,7 @@ describe('plugin-meetings', () => {
2201
2221
  });
2202
2222
  });
2203
2223
  });
2224
+
2204
2225
  describe('lmm, transcription & permissionTokenRefresh decoupling', () => {
2205
2226
  beforeEach(() => {
2206
2227
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
@@ -2271,7 +2292,6 @@ describe('plugin-meetings', () => {
2271
2292
  const locusInfoParseStub = sinon.stub(meeting.locusInfo, 'parse');
2272
2293
  sinon.stub(meeting, 'isJoined').returns(true);
2273
2294
 
2274
- // Set up llm.on stub to capture the registered listener when updateLLMConnection is called
2275
2295
  let locusLLMEventListener;
2276
2296
  meeting.webex.internal.llm.on = sinon.stub().callsFake((eventName, callback) => {
2277
2297
  if (eventName === 'event:locus.state_message') {
@@ -2280,16 +2300,12 @@ describe('plugin-meetings', () => {
2280
2300
  });
2281
2301
  meeting.webex.internal.llm.off = sinon.stub();
2282
2302
 
2283
- // we need the real meeting.updateLLMConnection not the mock
2284
2303
  meeting.updateLLMConnection.restore();
2285
2304
 
2286
- // Call updateLLMConnection to register the listener
2287
2305
  await meeting.updateLLMConnection();
2288
2306
 
2289
- // Verify the listener was registered and we captured it
2290
2307
  assert.isDefined(locusLLMEventListener, 'LLM event listener should be registered');
2291
2308
 
2292
- // Now trigger the event
2293
2309
  const eventData = {
2294
2310
  eventType: 'locus.state_message',
2295
2311
  stateElementsMessage: {
@@ -2309,13 +2325,10 @@ describe('plugin-meetings', () => {
2309
2325
  sinon.stub(meeting.webex.internal.llm, 'hasEverConnected').value(true);
2310
2326
  sinon.stub(meeting.webex.internal.llm, 'registerAndConnect').resolves({});
2311
2327
 
2312
- // Restore the real updateLLMConnection
2313
2328
  meeting.updateLLMConnection.restore();
2314
2329
 
2315
- // Call updateLLMConnection to start the timer
2316
2330
  await meeting.updateLLMConnection();
2317
2331
 
2318
- // Fast forward time by 3 minutes
2319
2332
  fakeClock.tick(3 * 60 * 1000);
2320
2333
 
2321
2334
  assert.calledWith(
@@ -2340,18 +2353,14 @@ describe('plugin-meetings', () => {
2340
2353
  .stub(meeting.webex.internal.llm, 'getDatachannelUrl')
2341
2354
  .returns('https://datachannel1.example.com');
2342
2355
 
2343
- // Restore the real updateLLMConnection
2344
2356
  meeting.updateLLMConnection.restore();
2345
2357
 
2346
- // First, connect LLM and start the timer
2347
2358
  isJoinedStub.returns(true);
2348
2359
  meeting.webex.internal.llm.isConnected.returns(false);
2349
2360
  await meeting.updateLLMConnection();
2350
2361
 
2351
- // Verify timer was started
2352
2362
  assert.exists(meeting.llmHealthCheckTimer);
2353
2363
 
2354
- // Now simulate that we're no longer joined
2355
2364
  isJoinedStub.returns(false);
2356
2365
  meeting.webex.internal.llm.isConnected.returns(true);
2357
2366
 
@@ -2359,10 +2368,8 @@ describe('plugin-meetings', () => {
2359
2368
 
2360
2369
  assert.calledOnce(meeting.webex.internal.llm.disconnectLLM);
2361
2370
 
2362
- // Verify the timer was cleared (should be undefined)
2363
2371
  assert.isUndefined(meeting.llmHealthCheckTimer);
2364
2372
 
2365
- // Fast forward time to ensure no metric is sent
2366
2373
  Metrics.sendBehavioralMetric.resetHistory();
2367
2374
  fakeClock.tick(3 * 60 * 1000);
2368
2375
 
@@ -2397,7 +2404,6 @@ describe('plugin-meetings', () => {
2397
2404
  .stub()
2398
2405
  .rejects(new CaptchaError('bad captcha'));
2399
2406
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2400
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil, 'joinMeetingOptions');
2401
2407
 
2402
2408
  try {
2403
2409
  await meeting.join();
@@ -2411,8 +2417,7 @@ describe('plugin-meetings', () => {
2411
2417
  );
2412
2418
  assert.instanceOf(error, CaptchaError);
2413
2419
  assert.equal(error.message, 'bad captcha');
2414
- // should not get to the end promise chain, which does do the join
2415
- assert.notCalled(joinMeetingOptionsSpy);
2420
+ assert.notCalled(MeetingUtil.joinMeeting);
2416
2421
  }
2417
2422
  });
2418
2423
 
@@ -2421,7 +2426,6 @@ describe('plugin-meetings', () => {
2421
2426
  .stub()
2422
2427
  .rejects(new PasswordError('bad password'));
2423
2428
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2424
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
2425
2429
 
2426
2430
  try {
2427
2431
  await meeting.join();
@@ -2435,8 +2439,7 @@ describe('plugin-meetings', () => {
2435
2439
  );
2436
2440
  assert.instanceOf(error, PasswordError);
2437
2441
  assert.equal(error.message, 'bad password');
2438
- // should not get to the end promise chain, which does do the join
2439
- assert.notCalled(joinMeetingOptionsSpy);
2442
+ assert.notCalled(MeetingUtil.joinMeeting);
2440
2443
  }
2441
2444
  });
2442
2445
 
@@ -2445,7 +2448,6 @@ describe('plugin-meetings', () => {
2445
2448
  .stub()
2446
2449
  .rejects(new PermissionError('bad permission'));
2447
2450
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2448
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
2449
2451
 
2450
2452
  try {
2451
2453
  await meeting.join();
@@ -2459,14 +2461,14 @@ describe('plugin-meetings', () => {
2459
2461
  );
2460
2462
  assert.instanceOf(error, PermissionError);
2461
2463
  assert.equal(error.message, 'bad permission');
2462
- // should not get to the end promise chain, which does do the join
2463
- assert.notCalled(joinMeetingOptionsSpy);
2464
+ assert.notCalled(MeetingUtil.joinMeeting);
2464
2465
  }
2465
2466
  });
2466
2467
  });
2467
2468
  });
2468
2469
  });
2469
2470
 
2471
+
2470
2472
  describe('#addMedia', () => {
2471
2473
  const muteStateStub = {
2472
2474
  handleClientRequest: sinon.stub().returns(Promise.resolve(true)),
@@ -6428,6 +6430,9 @@ describe('plugin-meetings', () => {
6428
6430
 
6429
6431
  meeting.annotation.deregisterEvents = sinon.stub();
6430
6432
  webex.internal.llm.off = sinon.stub();
6433
+ webex.internal.mercury.off = sinon.stub();
6434
+ meeting.mercuryOnlineHandler = sinon.stub();
6435
+ meeting.mercuryOfflineHandler = sinon.stub();
6431
6436
 
6432
6437
  // A meeting needs to be joined to leave
6433
6438
  meeting.meetingState = 'ACTIVE';
@@ -6451,6 +6456,67 @@ describe('plugin-meetings', () => {
6451
6456
  assert.calledOnce(meeting.clearMeetingData);
6452
6457
  });
6453
6458
 
6459
+ it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /leave', async () => {
6460
+ const onlineHandler = meeting.mercuryOnlineHandler;
6461
+ const offlineHandler = meeting.mercuryOfflineHandler;
6462
+
6463
+ await meeting.leave();
6464
+
6465
+ // All llm/mercury consumers (direct listeners, voicea transcription,
6466
+ // annotation) must be detached before the /leave request so that
6467
+ // in-flight events do not trigger unnecessary Locus syncs
6468
+ // (per Locus team recommendation).
6469
+ assert.callOrder(
6470
+ webex.internal.llm.off,
6471
+ webex.internal.mercury.off,
6472
+ meeting.stopTranscription,
6473
+ meeting.annotation.deregisterEvents,
6474
+ meeting.meetingRequest.leaveMeeting
6475
+ );
6476
+ assert.calledWithExactly(
6477
+ webex.internal.llm.off,
6478
+ 'event:relay.event',
6479
+ meeting.processRelayEvent
6480
+ );
6481
+ assert.calledWithExactly(
6482
+ webex.internal.llm.off,
6483
+ LOCUS_LLM_EVENT,
6484
+ meeting.processLocusLLMEvent
6485
+ );
6486
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
6487
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
6488
+ assert.isUndefined(meeting.mercuryOnlineHandler);
6489
+ assert.isUndefined(meeting.mercuryOfflineHandler);
6490
+ assert.calledOnceWithExactly(meeting.stopTranscription);
6491
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
6492
+ assert.isUndefined(meeting.transcription);
6493
+ });
6494
+
6495
+ it('tears down llm/mercury/transcription/annotation even when /leave rejects', async () => {
6496
+ const onlineHandler = meeting.mercuryOnlineHandler;
6497
+ const offlineHandler = meeting.mercuryOfflineHandler;
6498
+ meeting.meetingRequest.leaveMeeting = sinon
6499
+ .stub()
6500
+ .returns(Promise.reject(new Error('leave failed')));
6501
+
6502
+ await meeting.leave().catch(() => {});
6503
+
6504
+ assert.calledWithExactly(
6505
+ webex.internal.llm.off,
6506
+ 'event:relay.event',
6507
+ meeting.processRelayEvent
6508
+ );
6509
+ assert.calledWithExactly(
6510
+ webex.internal.llm.off,
6511
+ LOCUS_LLM_EVENT,
6512
+ meeting.processLocusLLMEvent
6513
+ );
6514
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
6515
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
6516
+ assert.calledOnceWithExactly(meeting.stopTranscription);
6517
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
6518
+ });
6519
+
6454
6520
  it('should reset call diagnostic latencies correctly', async () => {
6455
6521
  const leave = meeting.leave();
6456
6522
 
@@ -8458,6 +8524,9 @@ describe('plugin-meetings', () => {
8458
8524
 
8459
8525
  meeting.annotation.deregisterEvents = sinon.stub();
8460
8526
  webex.internal.llm.off = sinon.stub();
8527
+ webex.internal.mercury.off = sinon.stub();
8528
+ meeting.mercuryOnlineHandler = sinon.stub();
8529
+ meeting.mercuryOfflineHandler = sinon.stub();
8461
8530
 
8462
8531
  // A meeting needs to be joined to end
8463
8532
  meeting.meetingState = 'ACTIVE';
@@ -8480,6 +8549,66 @@ describe('plugin-meetings', () => {
8480
8549
  assert.calledOnce(meeting?.unsetPeerConnections);
8481
8550
  assert.calledOnce(meeting?.clearMeetingData);
8482
8551
  });
8552
+
8553
+ it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /end', async () => {
8554
+ const onlineHandler = meeting.mercuryOnlineHandler;
8555
+ const offlineHandler = meeting.mercuryOfflineHandler;
8556
+
8557
+ await meeting.endMeetingForAll();
8558
+
8559
+ // All llm/mercury consumers (direct listeners, voicea transcription,
8560
+ // annotation) must be detached before the /end request so that
8561
+ // in-flight events do not trigger unnecessary Locus syncs
8562
+ // (per Locus team recommendation).
8563
+ assert.callOrder(
8564
+ webex.internal.llm.off,
8565
+ webex.internal.mercury.off,
8566
+ meeting.stopTranscription,
8567
+ meeting.annotation.deregisterEvents,
8568
+ meeting.meetingRequest.endMeetingForAll
8569
+ );
8570
+ assert.calledWithExactly(
8571
+ webex.internal.llm.off,
8572
+ 'event:relay.event',
8573
+ meeting.processRelayEvent
8574
+ );
8575
+ assert.calledWithExactly(
8576
+ webex.internal.llm.off,
8577
+ LOCUS_LLM_EVENT,
8578
+ meeting.processLocusLLMEvent
8579
+ );
8580
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
8581
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
8582
+ assert.isUndefined(meeting.mercuryOnlineHandler);
8583
+ assert.isUndefined(meeting.mercuryOfflineHandler);
8584
+ assert.calledOnceWithExactly(meeting.stopTranscription);
8585
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
8586
+ });
8587
+
8588
+ it('tears down llm/mercury/transcription/annotation even when /end rejects', async () => {
8589
+ const onlineHandler = meeting.mercuryOnlineHandler;
8590
+ const offlineHandler = meeting.mercuryOfflineHandler;
8591
+ meeting.meetingRequest.endMeetingForAll = sinon
8592
+ .stub()
8593
+ .returns(Promise.reject(new Error('end failed')));
8594
+
8595
+ await meeting.endMeetingForAll().catch(() => {});
8596
+
8597
+ assert.calledWithExactly(
8598
+ webex.internal.llm.off,
8599
+ 'event:relay.event',
8600
+ meeting.processRelayEvent
8601
+ );
8602
+ assert.calledWithExactly(
8603
+ webex.internal.llm.off,
8604
+ LOCUS_LLM_EVENT,
8605
+ meeting.processLocusLLMEvent
8606
+ );
8607
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
8608
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
8609
+ assert.calledOnceWithExactly(meeting.stopTranscription);
8610
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
8611
+ });
8483
8612
  });
8484
8613
 
8485
8614
  describe('#moveTo', () => {
@@ -10416,14 +10545,24 @@ describe('plugin-meetings', () => {
10416
10545
  );
10417
10546
  done();
10418
10547
  });
10419
- it('listens to the self admitted guest event', (done) => {
10548
+ it('listens to the self admitted guest event without blocking on token prefetch', async () => {
10420
10549
  meeting.stopKeepAlive = sinon.stub();
10421
10550
  meeting.updateLLMConnection = sinon.stub();
10551
+ let resolvePrefetch;
10552
+
10553
+ meeting.ensureDefaultDatachannelTokenAfterAdmit = sinon
10554
+ .stub()
10555
+ .returns(new Promise((resolve) => {
10556
+ resolvePrefetch = resolve;
10557
+ }));
10422
10558
  meeting.rtcMetrics = {
10423
10559
  sendNextMetrics: sinon.stub(),
10424
10560
  };
10561
+
10425
10562
  meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
10563
+
10426
10564
  assert.calledOnceWithExactly(meeting.stopKeepAlive);
10565
+ assert.calledOnceWithExactly(meeting.ensureDefaultDatachannelTokenAfterAdmit);
10427
10566
  assert.calledThrice(TriggerProxy.trigger);
10428
10567
  assert.calledWith(
10429
10568
  TriggerProxy.trigger,
@@ -10442,7 +10581,11 @@ describe('plugin-meetings', () => {
10442
10581
  correlation_id: meeting.correlationId,
10443
10582
  }
10444
10583
  );
10445
- done();
10584
+
10585
+ resolvePrefetch(false);
10586
+ await Promise.resolve();
10587
+
10588
+ assert.calledOnce(meeting.updateLLMConnection);
10446
10589
  });
10447
10590
 
10448
10591
  it('listens to the breakouts changed event', () => {
@@ -11009,6 +11152,7 @@ describe('plugin-meetings', () => {
11009
11152
  meeting.annotation.locusUrlUpdate = sinon.stub();
11010
11153
  meeting.simultaneousInterpretation.locusUrlUpdate = sinon.stub();
11011
11154
  meeting.webinar.locusUrlUpdate = sinon.stub();
11155
+ meeting.aiEnableRequest.locusUrlUpdate = sinon.stub();
11012
11156
 
11013
11157
  meeting.locusInfo.emit(
11014
11158
  {function: 'test', file: 'test'},
@@ -11023,6 +11167,7 @@ describe('plugin-meetings', () => {
11023
11167
  assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, false);
11024
11168
  assert.calledWith(meeting.simultaneousInterpretation.locusUrlUpdate, newLocusUrl);
11025
11169
  assert.calledWith(meeting.webinar.locusUrlUpdate, newLocusUrl);
11170
+ assert.calledWith(meeting.aiEnableRequest.locusUrlUpdate, newLocusUrl);
11026
11171
  assert.equal(meeting.locusUrl, newLocusUrl);
11027
11172
  assert(meeting.locusId, '12345');
11028
11173
 
@@ -11338,6 +11483,93 @@ describe('plugin-meetings', () => {
11338
11483
  });
11339
11484
  });
11340
11485
 
11486
+ describe('#finalizeMeetingAfterInitialLocusSetup', () => {
11487
+ it('refreshes destination from synced locus when destination type is LOCUS_ID', () => {
11488
+ const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11489
+
11490
+ meeting.destinationType = DESTINATION_TYPE.LOCUS_ID;
11491
+ meeting.destination = {info: {topic: 'old'}};
11492
+
11493
+ meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
11494
+
11495
+ assert.equal(meeting.destination, syncedLocus);
11496
+ });
11497
+
11498
+ it('does not refresh destination when destination type is not LOCUS_ID', () => {
11499
+ const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11500
+ const originalDestination = {destination: 'original-destination'};
11501
+
11502
+ meeting.destinationType = DESTINATION_TYPE.CONVERSATION_URL;
11503
+ meeting.destination = originalDestination;
11504
+
11505
+ meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
11506
+
11507
+ assert.equal(meeting.destination, originalDestination);
11508
+ });
11509
+
11510
+ it('fetches meeting info when meetingInfo is empty and destination has info', () => {
11511
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11512
+
11513
+ meeting.meetingInfo = {};
11514
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11515
+
11516
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11517
+
11518
+ assert.calledOnceWithExactly(fetchMeetingInfoStub, {});
11519
+ });
11520
+
11521
+ it('does not fetch meeting info when destination has no info', () => {
11522
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11523
+
11524
+ meeting.meetingInfo = {};
11525
+ meeting.destination = {url: 'https://locus.example.com/locus/123'};
11526
+
11527
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11528
+
11529
+ assert.notCalled(fetchMeetingInfoStub);
11530
+ });
11531
+
11532
+ it('does not fetch meeting info when meetingInfo is already populated', () => {
11533
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11534
+
11535
+ meeting.meetingInfo = {meetingJoinUrl: 'https://example.com/join/abc'};
11536
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11537
+
11538
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11539
+
11540
+ assert.notCalled(fetchMeetingInfoStub);
11541
+ });
11542
+
11543
+ it('does not fetch meeting info when delayed fetch timer is already scheduled', () => {
11544
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11545
+
11546
+ meeting.meetingInfo = {};
11547
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11548
+ meeting.fetchMeetingInfoTimeoutId = 42;
11549
+
11550
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11551
+
11552
+ assert.notCalled(fetchMeetingInfoStub);
11553
+ });
11554
+
11555
+ it('swallows async fetchMeetingInfo errors and logs info', async () => {
11556
+ const error = new Error('fetch failed');
11557
+
11558
+ meeting.meetingInfo = {};
11559
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11560
+ sinon.stub(meeting, 'fetchMeetingInfo').returns(Promise.reject(error));
11561
+ const loggerInfoStub = sinon.stub(LoggerProxy.logger, 'info');
11562
+
11563
+ await meeting.finalizeMeetingAfterInitialLocusSetup({});
11564
+
11565
+ assert.calledOnce(loggerInfoStub);
11566
+ assert.match(
11567
+ loggerInfoStub.firstCall.args[0],
11568
+ /Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: fetch failed/
11569
+ );
11570
+ });
11571
+ });
11572
+
11341
11573
  describe('#emailInput', () => {
11342
11574
  it('should set the email input', () => {
11343
11575
  assert.notOk(meeting.emailInput);
@@ -13124,7 +13356,9 @@ describe('plugin-meetings', () => {
13124
13356
  info: {datachannelUrl: 'a datachannel url'},
13125
13357
  };
13126
13358
 
13127
- webex.internal.llm.getDatachannelToken.withArgs('llm-default-session').returns('token-123');
13359
+ webex.internal.llm.getDatachannelToken
13360
+ .withArgs('llm-default-session')
13361
+ .returns('token-123');
13128
13362
 
13129
13363
  await meeting.updateLLMConnection();
13130
13364
 
@@ -13209,10 +13443,13 @@ describe('plugin-meetings', () => {
13209
13443
  meeting.processLocusLLMEvent
13210
13444
  );
13211
13445
  assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13212
- assert.calledOnce(meeting.stopTranscription);
13213
- assert.isUndefined(meeting.transcription);
13214
13446
  assert.calledOnce(meeting.clearDataChannelToken);
13215
- assert.calledOnce(meeting.annotation.deregisterEvents);
13447
+ // stopTranscription and annotation.deregisterEvents are not
13448
+ // called here: they run in stopListeningForMeetingEvents()
13449
+ // before /leave to avoid double-emitting
13450
+ // MEETING_STOPPED_RECEIVING_TRANSCRIPTION.
13451
+ assert.notCalled(meeting.stopTranscription);
13452
+ assert.notCalled(meeting.annotation.deregisterEvents);
13216
13453
  });
13217
13454
  it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
13218
13455
  webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
@@ -13231,19 +13468,9 @@ describe('plugin-meetings', () => {
13231
13468
  meeting.processLocusLLMEvent
13232
13469
  );
13233
13470
  assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13234
- assert.calledOnce(meeting.stopTranscription);
13235
- assert.isUndefined(meeting.transcription);
13236
- assert.calledOnce(meeting.clearDataChannelToken);
13237
- assert.calledOnce(meeting.annotation.deregisterEvents);
13238
- });
13239
- it('always calls stopTranscription even when transcription is undefined', async () => {
13240
- meeting.transcription = undefined;
13241
-
13242
- await meeting.clearMeetingData();
13243
-
13244
- assert.calledOnce(meeting.stopTranscription);
13245
- assert.isUndefined(meeting.transcription);
13246
13471
  assert.calledOnce(meeting.clearDataChannelToken);
13472
+ assert.notCalled(meeting.stopTranscription);
13473
+ assert.notCalled(meeting.annotation.deregisterEvents);
13247
13474
  });
13248
13475
  });
13249
13476
  });
@@ -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,