@webex/plugin-meetings 3.12.0-next.65 → 3.12.0-next.67

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 (40) hide show
  1. package/dist/aiEnableRequest/index.js +1 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/constants.js +24 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  8. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  9. package/dist/interpretation/index.js +1 -1
  10. package/dist/interpretation/index.js.map +1 -1
  11. package/dist/interpretation/interpretation.types.js +7 -0
  12. package/dist/interpretation/interpretation.types.js.map +1 -0
  13. package/dist/interpretation/siLanguage.js +1 -1
  14. package/dist/meeting/index.js +738 -679
  15. package/dist/meeting/index.js.map +1 -1
  16. package/dist/meeting/request.js +5 -2
  17. package/dist/meeting/request.js.map +1 -1
  18. package/dist/meeting/util.js +1 -0
  19. package/dist/meeting/util.js.map +1 -1
  20. package/dist/types/constants.d.ts +7 -1
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  23. package/dist/types/meeting/index.d.ts +2 -2
  24. package/dist/types/meeting/request.d.ts +1 -0
  25. package/dist/webinar/index.js +219 -146
  26. package/dist/webinar/index.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/constants.ts +8 -1
  29. package/src/index.ts +1 -0
  30. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  31. package/src/interpretation/index.ts +2 -1
  32. package/src/interpretation/interpretation.types.ts +11 -0
  33. package/src/meeting/index.ts +111 -49
  34. package/src/meeting/request.ts +11 -0
  35. package/src/meeting/util.ts +1 -0
  36. package/src/webinar/index.ts +114 -16
  37. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  38. package/test/unit/spec/meeting/index.js +139 -23
  39. package/test/unit/spec/meeting/request.js +12 -0
  40. package/test/unit/spec/webinar/index.ts +197 -14
@@ -7,6 +7,8 @@ import sinon from 'sinon';
7
7
  import {DataChannelTokenType} from '@webex/internal-plugin-llm';
8
8
  import {LLM_PRACTICE_SESSION, LOCUS_LLM_EVENT, SHARE_STATUS} from '@webex/plugin-meetings/src/constants';
9
9
 
10
+ const PRACTICE_SESSION_KEY = LLM_PRACTICE_SESSION || DataChannelTokenType.PracticeSession;
11
+
10
12
  describe('plugin-meetings', () => {
11
13
  describe('Webinar', () => {
12
14
 
@@ -33,6 +35,17 @@ describe('plugin-meetings', () => {
33
35
  webex.internal.llm = {
34
36
  getDatachannelToken: sinon.stub().returns(undefined),
35
37
  setDatachannelToken: sinon.stub(),
38
+ setRefreshHandler: sinon.stub(),
39
+ getOwnerMeetingId: sinon.stub().returns(undefined),
40
+ resolveSessionOwnership: sinon.stub().callsFake((ownerMeetingId, sessionId) => {
41
+ const currentOwner = webex.internal.llm.getOwnerMeetingId(sessionId);
42
+
43
+ return {
44
+ currentOwner,
45
+ isOwner: !currentOwner || !ownerMeetingId || currentOwner === ownerMeetingId,
46
+ };
47
+ }),
48
+ setOwnerMeetingId: sinon.stub(),
36
49
  isDataChannelTokenEnabled: sinon.stub().resolves(false),
37
50
  isConnected: sinon.stub().returns(false),
38
51
  disconnectLLM: sinon.stub().resolves(),
@@ -226,6 +239,7 @@ describe('plugin-meetings', () => {
226
239
  let relayListener;
227
240
 
228
241
  beforeEach(() => {
242
+ webinar.meetingId = 'meeting-id';
229
243
  relayListener = sinon.stub();
230
244
  webinar.llmListeners = {relay: relayListener, locusLLM: null};
231
245
  });
@@ -236,27 +250,74 @@ describe('plugin-meetings', () => {
236
250
  assert.calledOnceWithExactly(
237
251
  webex.internal.llm.disconnectLLM,
238
252
  {code: 3050, reason: 'done (permanent)'},
239
- LLM_PRACTICE_SESSION
253
+ PRACTICE_SESSION_KEY,
254
+ webinar.meetingId
240
255
  );
241
256
  assert.calledWithExactly(
242
257
  webex.internal.llm.off,
243
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
258
+ `event:relay.event:${PRACTICE_SESSION_KEY}`,
244
259
  relayListener
245
260
  );
246
261
  assert.isNull(webinar.llmListeners.relay);
247
262
  });
248
263
 
264
+ it('skips disconnect when practice-session owner is another meeting', async () => {
265
+ webex.internal.llm.getOwnerMeetingId.returns('other-meeting-id');
266
+ webex.internal.llm.disconnectLLM.resolves(false);
267
+
268
+ await webinar.cleanupPSDataChannel();
269
+
270
+ assert.calledOnceWithExactly(
271
+ webex.internal.llm.disconnectLLM,
272
+ {code: 3050, reason: 'done (permanent)'},
273
+ PRACTICE_SESSION_KEY,
274
+ webinar.meetingId
275
+ );
276
+ assert.notCalled(webex.internal.llm.setOwnerMeetingId);
277
+ assert.calledOnceWithExactly(
278
+ webex.internal.llm.off,
279
+ `event:relay.event:${PRACTICE_SESSION_KEY}`,
280
+ relayListener
281
+ );
282
+ });
283
+
249
284
  it('skips relay listener removal when no listener has been tracked', async () => {
250
285
  webinar.llmListeners.relay = null;
251
286
 
252
287
  await webinar.cleanupPSDataChannel();
253
288
 
254
289
  const relayOffCalls = webex.internal.llm.off.args.filter(
255
- ([event]) => event === `event:relay.event:${LLM_PRACTICE_SESSION}`
290
+ ([event]) => event === `event:relay.event:${PRACTICE_SESSION_KEY}`
256
291
  );
257
292
  assert.equal(relayOffCalls.length, 0);
258
293
  });
259
294
 
295
+ it('removes tracked relay listener even when disconnect throws', async () => {
296
+ const disconnectError = new Error('disconnect failed');
297
+ webex.internal.llm.disconnectLLM.rejects(disconnectError);
298
+
299
+ let caughtError;
300
+
301
+ try {
302
+ await webinar.cleanupPSDataChannel();
303
+ } catch (error) {
304
+ caughtError = error;
305
+ }
306
+
307
+ assert.equal(caughtError, disconnectError);
308
+ assert.calledOnceWithExactly(
309
+ webex.internal.llm.setOwnerMeetingId,
310
+ undefined,
311
+ PRACTICE_SESSION_KEY
312
+ );
313
+ assert.calledOnceWithExactly(
314
+ webex.internal.llm.off,
315
+ `event:relay.event:${PRACTICE_SESSION_KEY}`,
316
+ relayListener
317
+ );
318
+ assert.notOk(webinar._practiceSessionRelayListener);
319
+ });
320
+
260
321
  it('disconnects and removes the tracked locusLLM listener', async () => {
261
322
  const locusLLMListener = sinon.stub();
262
323
  webinar.llmListeners.locusLLM = locusLLMListener;
@@ -317,9 +378,11 @@ describe('plugin-meetings', () => {
317
378
  let processLocusLLMEvent;
318
379
 
319
380
  beforeEach(() => {
381
+ webinar.meetingId = 'meeting-id';
320
382
  processRelayEvent = sinon.stub();
321
383
  processLocusLLMEvent = sinon.stub();
322
384
  meeting = {
385
+ id: 'meeting-id',
323
386
  locusUrl: 'locusUrl',
324
387
  isJoined: sinon.stub().returns(true),
325
388
  processRelayEvent,
@@ -334,7 +397,7 @@ describe('plugin-meetings', () => {
334
397
 
335
398
  // Default session is connected by default; practice session is not
336
399
  webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
337
- return sessionId !== LLM_PRACTICE_SESSION;
400
+ return sessionId !== PRACTICE_SESSION_KEY;
338
401
  });
339
402
 
340
403
  // Token is pre-saved into LLM by saveDataChannelToken
@@ -370,14 +433,15 @@ describe('plugin-meetings', () => {
370
433
  assert.calledWithExactly(
371
434
  webex.internal.llm.setDatachannelToken,
372
435
  'ps-token-from-refresh',
373
- DataChannelTokenType.PracticeSession
436
+ DataChannelTokenType.PracticeSession,
437
+ 'meeting-id'
374
438
  );
375
439
  assert.calledWith(
376
440
  webex.internal.llm.registerAndConnect,
377
441
  'locus-url',
378
442
  'dc-url',
379
443
  'ps-token-from-refresh',
380
- LLM_PRACTICE_SESSION
444
+ PRACTICE_SESSION_KEY
381
445
  );
382
446
  });
383
447
 
@@ -437,6 +501,8 @@ describe('plugin-meetings', () => {
437
501
  const result = await webinar.updatePSDataChannel();
438
502
 
439
503
  assert.isUndefined(result);
504
+ assert.notCalled(webex.internal.llm.setRefreshHandler);
505
+ assert.notCalled(webex.internal.llm.setOwnerMeetingId);
440
506
  assert.notCalled(webex.internal.llm.registerAndConnect);
441
507
  });
442
508
 
@@ -457,12 +523,17 @@ describe('plugin-meetings', () => {
457
523
  const result = await webinar.updatePSDataChannel();
458
524
 
459
525
  assert.calledOnce(webex.internal.llm.registerAndConnect);
526
+ assert.calledWithExactly(
527
+ webex.internal.llm.setOwnerMeetingId,
528
+ 'meeting-id',
529
+ PRACTICE_SESSION_KEY
530
+ );
460
531
  assert.calledWith(
461
532
  webex.internal.llm.registerAndConnect,
462
533
  'locus-url',
463
534
  'dc-url',
464
535
  'ps-token',
465
- LLM_PRACTICE_SESSION
536
+ PRACTICE_SESSION_KEY
466
537
  );
467
538
  assert.calledOnceWithExactly(webex.internal.voicea.announce);
468
539
  assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
@@ -478,7 +549,8 @@ describe('plugin-meetings', () => {
478
549
 
479
550
  assert.calledWithExactly(
480
551
  webex.internal.llm.getDatachannelToken,
481
- DataChannelTokenType.PracticeSession
552
+ DataChannelTokenType.PracticeSession,
553
+ webinar.meetingId
482
554
  );
483
555
  assert.notCalled(webex.internal.llm.setDatachannelToken);
484
556
  assert.calledWith(
@@ -486,7 +558,7 @@ describe('plugin-meetings', () => {
486
558
  'locus-url',
487
559
  'dc-url',
488
560
  'cached-token',
489
- LLM_PRACTICE_SESSION
561
+ PRACTICE_SESSION_KEY
490
562
  );
491
563
  });
492
564
 
@@ -507,7 +579,7 @@ describe('plugin-meetings', () => {
507
579
  assert.equal(webinar.llmListeners.relay, processRelayEvent);
508
580
  assert.calledWith(
509
581
  webex.internal.llm.on,
510
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
582
+ `event:relay.event:${PRACTICE_SESSION_KEY}`,
511
583
  processRelayEvent
512
584
  );
513
585
  });
@@ -520,7 +592,7 @@ describe('plugin-meetings', () => {
520
592
 
521
593
  assert.calledWith(
522
594
  webex.internal.llm.off,
523
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
595
+ `event:relay.event:${PRACTICE_SESSION_KEY}`,
524
596
  previousListener
525
597
  );
526
598
  assert.equal(webinar.llmListeners.relay, processRelayEvent);
@@ -578,6 +650,8 @@ describe('plugin-meetings', () => {
578
650
  // Should register an 'online' listener but NOT call registerAndConnect yet
579
651
  assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
580
652
  assert.notCalled(webex.internal.llm.registerAndConnect);
653
+ assert.notCalled(webex.internal.llm.setRefreshHandler);
654
+ assert.notCalled(webex.internal.llm.setOwnerMeetingId);
581
655
  // Should store the pending listener
582
656
  assert.isNotNull(webinar._pendingOnlineListener);
583
657
  });
@@ -609,7 +683,7 @@ describe('plugin-meetings', () => {
609
683
 
610
684
  // Now simulate default session coming online
611
685
  webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
612
- return sessionId !== LLM_PRACTICE_SESSION;
686
+ return sessionId !== PRACTICE_SESSION_KEY;
613
687
  });
614
688
 
615
689
  // Fire the captured listener
@@ -636,7 +710,7 @@ describe('plugin-meetings', () => {
636
710
 
637
711
  // Now default session comes online
638
712
  webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
639
- return sessionId !== LLM_PRACTICE_SESSION;
713
+ return sessionId !== PRACTICE_SESSION_KEY;
640
714
  });
641
715
 
642
716
  // Fire the listener — re-invokes updatePSDataChannel which will see isPracticeSession = false
@@ -649,7 +723,7 @@ describe('plugin-meetings', () => {
649
723
  it('proceeds immediately when default session is already connected', async () => {
650
724
  // Default session already connected, practice session not
651
725
  webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
652
- return sessionId !== LLM_PRACTICE_SESSION;
726
+ return sessionId !== PRACTICE_SESSION_KEY;
653
727
  });
654
728
 
655
729
  const result = await webinar.updatePSDataChannel();
@@ -661,6 +735,115 @@ describe('plugin-meetings', () => {
661
735
  assert.calledOnce(webex.internal.llm.registerAndConnect);
662
736
  assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
663
737
  });
738
+
739
+ it('does not override practice refresh handler or reconnect when owned by another meeting', async () => {
740
+ webex.internal.llm.getOwnerMeetingId.returns('other-meeting-id');
741
+ webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
742
+ return sessionId !== undefined;
743
+ });
744
+
745
+ const result = await webinar.updatePSDataChannel();
746
+
747
+ assert.isUndefined(result);
748
+ assert.notCalled(webex.internal.llm.setRefreshHandler);
749
+ assert.notCalled(webex.internal.llm.registerAndConnect);
750
+ });
751
+
752
+ it('does not reconnect when practice session is disconnected but owned by another meeting', async () => {
753
+ webex.internal.llm.getOwnerMeetingId.returns('other-meeting-id');
754
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
755
+
756
+ const result = await webinar.updatePSDataChannel();
757
+
758
+ assert.isUndefined(result);
759
+ assert.notCalled(webex.internal.llm.setRefreshHandler);
760
+ assert.notCalled(webex.internal.llm.setOwnerMeetingId);
761
+ assert.notCalled(webex.internal.llm.registerAndConnect);
762
+ });
763
+
764
+ it('does not write owner or connect if ownership changes before pre-connect owner write', async () => {
765
+ let ownerMeetingId = 'meeting-id';
766
+
767
+ webex.internal.llm.getOwnerMeetingId.callsFake(() => ownerMeetingId);
768
+ webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
769
+ webex.internal.llm.getDatachannelToken = sinon.stub().returns(undefined);
770
+
771
+ let resolveRefresh;
772
+ meeting.refreshDataChannelToken = sinon.stub().returns(
773
+ new Promise((resolve) => {
774
+ resolveRefresh = resolve;
775
+ })
776
+ );
777
+
778
+ const updatePromise = webinar.updatePSDataChannel();
779
+
780
+ ownerMeetingId = 'other-meeting-id';
781
+ resolveRefresh({
782
+ body: {
783
+ datachannelToken: 'ps-token-from-refresh',
784
+ dataChannelTokenType: DataChannelTokenType.PracticeSession,
785
+ },
786
+ });
787
+
788
+ const result = await updatePromise;
789
+
790
+ assert.isUndefined(result);
791
+ assert.notCalled(webex.internal.llm.setRefreshHandler);
792
+ assert.notCalled(webex.internal.llm.setOwnerMeetingId);
793
+ assert.notCalled(webex.internal.llm.registerAndConnect);
794
+ });
795
+
796
+ it('does not overwrite owner after connect when ownership changed during registerAndConnect', async () => {
797
+ let ownerMeetingId = 'meeting-id';
798
+
799
+ webex.internal.llm.getOwnerMeetingId.callsFake(() => ownerMeetingId);
800
+ webex.internal.llm.registerAndConnect = sinon.stub().callsFake(async () => {
801
+ ownerMeetingId = 'other-meeting-id';
802
+
803
+ return 'REGISTER_AND_CONNECT_RESULT';
804
+ });
805
+
806
+ const result = await webinar.updatePSDataChannel();
807
+
808
+ assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
809
+ assert.calledOnce(webex.internal.llm.setOwnerMeetingId);
810
+ assert.calledWithExactly(
811
+ webex.internal.llm.setOwnerMeetingId,
812
+ 'meeting-id',
813
+ PRACTICE_SESSION_KEY
814
+ );
815
+ });
816
+
817
+ it('clears pre-claimed owner when registerAndConnect rejects', async () => {
818
+ const registerError = new Error('register failed');
819
+ let ownerMeetingId = 'meeting-id';
820
+
821
+ webex.internal.llm.getOwnerMeetingId.callsFake(() => ownerMeetingId);
822
+ webex.internal.llm.setOwnerMeetingId.callsFake((id) => {
823
+ ownerMeetingId = id;
824
+ });
825
+ webex.internal.llm.registerAndConnect = sinon.stub().rejects(registerError);
826
+
827
+ try {
828
+ await webinar.updatePSDataChannel();
829
+ assert.fail('Expected updatePSDataChannel to reject when registerAndConnect fails');
830
+ } catch (error) {
831
+ assert.equal(error, registerError);
832
+ }
833
+
834
+ assert.calledTwice(webex.internal.llm.setOwnerMeetingId);
835
+ assert.calledWithExactly(
836
+ webex.internal.llm.setOwnerMeetingId.firstCall,
837
+ 'meeting-id',
838
+ PRACTICE_SESSION_KEY
839
+ );
840
+ assert.calledWithExactly(
841
+ webex.internal.llm.setOwnerMeetingId.secondCall,
842
+ undefined,
843
+ PRACTICE_SESSION_KEY
844
+ );
845
+ assert.isUndefined(ownerMeetingId);
846
+ });
664
847
  });
665
848
 
666
849
  describe('#updateStatusByRole', () => {