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

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.
@@ -1552,6 +1552,22 @@ describe('plugin-meetings', () => {
1552
1552
  EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION
1553
1553
  );
1554
1554
  });
1555
+
1556
+ it('should stop listening to voicea events even when transcription is undefined', () => {
1557
+ meeting.transcription = undefined;
1558
+ meeting.stopTranscription();
1559
+ assert.equal(webex.internal.voicea.off.callCount, 4);
1560
+ assert.equal(meeting.areVoiceaEventsSetup, false);
1561
+ assert.calledWith(
1562
+ TriggerProxy.trigger,
1563
+ sinon.match.instanceOf(Meeting),
1564
+ {
1565
+ file: 'meeting/index',
1566
+ function: 'triggerStopReceivingTranscriptionEvent',
1567
+ },
1568
+ EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION
1569
+ );
1570
+ });
1555
1571
  });
1556
1572
 
1557
1573
  describe('#setCaptionLanguage', () => {
@@ -12745,6 +12761,93 @@ describe('plugin-meetings', () => {
12745
12761
  });
12746
12762
  });
12747
12763
 
12764
+ describe('#saveDataChannelToken', () => {
12765
+ beforeEach(() => {
12766
+ webex.internal.llm.setDatachannelToken = sinon.stub();
12767
+ });
12768
+
12769
+ it('saves datachannelToken into LLM as Default', () => {
12770
+ meeting.saveDataChannelToken({
12771
+ locus: {
12772
+ self: {datachannelToken: 'default-token'},
12773
+ },
12774
+ });
12775
+
12776
+ assert.calledWithExactly(
12777
+ webex.internal.llm.setDatachannelToken,
12778
+ 'default-token',
12779
+ 'llm-default-session'
12780
+ );
12781
+ });
12782
+
12783
+ it('saves practiceSessionDatachannelToken into LLM as PracticeSession', () => {
12784
+ meeting.saveDataChannelToken({
12785
+ locus: {
12786
+ self: {practiceSessionDatachannelToken: 'ps-token'},
12787
+ },
12788
+ });
12789
+
12790
+ assert.calledWithExactly(
12791
+ webex.internal.llm.setDatachannelToken,
12792
+ 'ps-token',
12793
+ 'llm-practice-session'
12794
+ );
12795
+ });
12796
+
12797
+ it('saves both tokens when both are present', () => {
12798
+ meeting.saveDataChannelToken({
12799
+ locus: {
12800
+ self: {
12801
+ datachannelToken: 'default-token',
12802
+ practiceSessionDatachannelToken: 'ps-token',
12803
+ },
12804
+ },
12805
+ });
12806
+
12807
+ assert.calledTwice(webex.internal.llm.setDatachannelToken);
12808
+ assert.calledWithExactly(
12809
+ webex.internal.llm.setDatachannelToken,
12810
+ 'default-token',
12811
+ 'llm-default-session'
12812
+ );
12813
+ assert.calledWithExactly(
12814
+ webex.internal.llm.setDatachannelToken,
12815
+ 'ps-token',
12816
+ 'llm-practice-session'
12817
+ );
12818
+ });
12819
+
12820
+ it('does not call setDatachannelToken when no tokens are present', () => {
12821
+ meeting.saveDataChannelToken({locus: {self: {}}});
12822
+
12823
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
12824
+ });
12825
+
12826
+ it('handles undefined join gracefully', () => {
12827
+ meeting.saveDataChannelToken(undefined);
12828
+
12829
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
12830
+ });
12831
+
12832
+ it('handles missing locus.self gracefully', () => {
12833
+ meeting.saveDataChannelToken({locus: {}});
12834
+
12835
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
12836
+ });
12837
+ });
12838
+
12839
+ describe('#clearDataChannelToken', () => {
12840
+ beforeEach(() => {
12841
+ webex.internal.llm.resetDatachannelTokens = sinon.stub();
12842
+ });
12843
+
12844
+ it('calls resetDatachannelTokens on LLM', () => {
12845
+ meeting.clearDataChannelToken();
12846
+
12847
+ assert.calledOnce(webex.internal.llm.resetDatachannelTokens);
12848
+ });
12849
+ });
12850
+
12748
12851
  describe('#updateLLMConnection', () => {
12749
12852
  beforeEach(() => {
12750
12853
  webex.internal.llm.isConnected = sinon.stub().returns(false);
@@ -13011,15 +13114,14 @@ describe('plugin-meetings', () => {
13011
13114
  undefined
13012
13115
  );
13013
13116
  });
13014
- it('passes dataChannelToken to registerAndConnect', async () => {
13117
+ it('passes dataChannelToken from LLM to registerAndConnect', async () => {
13015
13118
  meeting.joinedWith = {state: 'JOINED'};
13016
13119
  meeting.locusInfo = {
13017
13120
  url: 'a url',
13018
13121
  info: {datachannelUrl: 'a datachannel url'},
13019
- self: {datachannelToken: 'token-123'},
13020
13122
  };
13021
13123
 
13022
- webex.internal.llm.getDatachannelToken.returns(undefined);
13124
+ webex.internal.llm.getDatachannelToken.withArgs('llm-default-session').returns('token-123');
13023
13125
 
13024
13126
  await meeting.updateLLMConnection();
13025
13127
 
@@ -13029,17 +13131,16 @@ describe('plugin-meetings', () => {
13029
13131
  'a datachannel url',
13030
13132
  'token-123'
13031
13133
  );
13032
- assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'llm-default-session');
13134
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
13033
13135
  });
13034
- it('prefers refreshed token over locus self token', async () => {
13136
+ it('passes undefined token when LLM has no token stored', async () => {
13035
13137
  meeting.joinedWith = {state: 'JOINED'};
13036
13138
  meeting.locusInfo = {
13037
13139
  url: 'a url',
13038
13140
  info: {datachannelUrl: 'a datachannel url'},
13039
- self: {datachannelToken: 'locus-token'},
13040
13141
  };
13041
13142
 
13042
- webex.internal.llm.getDatachannelToken.withArgs('llm-default-session').returns('refreshed-token');
13143
+ webex.internal.llm.getDatachannelToken.returns(undefined);
13043
13144
 
13044
13145
  await meeting.updateLLMConnection();
13045
13146
 
@@ -13047,7 +13148,7 @@ describe('plugin-meetings', () => {
13047
13148
  webex.internal.llm.registerAndConnect,
13048
13149
  'a url',
13049
13150
  'a datachannel url',
13050
- 'refreshed-token'
13151
+ undefined
13051
13152
  );
13052
13153
 
13053
13154
  assert.notCalled(webex.internal.llm.setDatachannelToken);
@@ -13058,7 +13159,6 @@ describe('plugin-meetings', () => {
13058
13159
  meeting.locusInfo = {
13059
13160
  url: 'a url',
13060
13161
  info: {datachannelUrl: 'a datachannel url'},
13061
- self: {datachannelToken: 'token-123'},
13062
13162
  };
13063
13163
 
13064
13164
  webex.internal.llm.getDatachannelToken.returns(undefined);
@@ -13070,9 +13170,9 @@ describe('plugin-meetings', () => {
13070
13170
  webex.internal.llm.registerAndConnect,
13071
13171
  'a url',
13072
13172
  'a datachannel url',
13073
- 'token-123'
13173
+ undefined
13074
13174
  );
13075
- assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'llm-default-session');
13175
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
13076
13176
  });
13077
13177
 
13078
13178
  describe('#clearMeetingData', () => {
@@ -13083,7 +13183,7 @@ describe('plugin-meetings', () => {
13083
13183
  meeting.annotation.deregisterEvents = sinon.stub();
13084
13184
  meeting.clearLLMHealthCheckTimer = sinon.stub();
13085
13185
  meeting.stopTranscription = sinon.stub();
13086
- meeting.transcription = {};
13186
+ meeting.clearDataChannelToken = sinon.stub();
13087
13187
  meeting.shareStatus = 'no-share';
13088
13188
  });
13089
13189
 
@@ -13107,6 +13207,8 @@ describe('plugin-meetings', () => {
13107
13207
  );
13108
13208
  assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13109
13209
  assert.calledOnce(meeting.stopTranscription);
13210
+ assert.isUndefined(meeting.transcription);
13211
+ assert.calledOnce(meeting.clearDataChannelToken);
13110
13212
  assert.calledOnce(meeting.annotation.deregisterEvents);
13111
13213
  });
13112
13214
  it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
@@ -13127,8 +13229,19 @@ describe('plugin-meetings', () => {
13127
13229
  );
13128
13230
  assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13129
13231
  assert.calledOnce(meeting.stopTranscription);
13232
+ assert.isUndefined(meeting.transcription);
13233
+ assert.calledOnce(meeting.clearDataChannelToken);
13130
13234
  assert.calledOnce(meeting.annotation.deregisterEvents);
13131
13235
  });
13236
+ it('always calls stopTranscription even when transcription is undefined', async () => {
13237
+ meeting.transcription = undefined;
13238
+
13239
+ await meeting.clearMeetingData();
13240
+
13241
+ assert.calledOnce(meeting.stopTranscription);
13242
+ assert.isUndefined(meeting.transcription);
13243
+ assert.calledOnce(meeting.clearDataChannelToken);
13244
+ });
13132
13245
  });
13133
13246
  });
13134
13247
 
@@ -210,6 +210,26 @@ describe('plugin-meetings', () => {
210
210
  meeting.processRelayEvent
211
211
  );
212
212
  });
213
+
214
+ it('removes a pending online listener if one exists', async () => {
215
+ const listener = sinon.stub();
216
+ webinar._pendingOnlineListener = listener;
217
+
218
+ await webinar.cleanupPSDataChannel();
219
+
220
+ assert.calledWith(webex.internal.llm.off, 'online', listener);
221
+ assert.isNull(webinar._pendingOnlineListener);
222
+ });
223
+
224
+ it('skips online listener removal when none is pending', async () => {
225
+ webinar._pendingOnlineListener = null;
226
+
227
+ await webinar.cleanupPSDataChannel();
228
+
229
+ // 'off' should only be called for the relay event, not for 'online'
230
+ const onlineOffCalls = webex.internal.llm.off.args.filter(([event]) => event === 'online');
231
+ assert.equal(onlineOffCalls.length, 0);
232
+ });
213
233
  });
214
234
 
215
235
  describe('#updatePSDataChannel', () => {
@@ -224,12 +244,22 @@ describe('plugin-meetings', () => {
224
244
  locusInfo: {
225
245
  url: 'locus-url',
226
246
  info: {practiceSessionDatachannelUrl: 'dc-url'},
227
- self: {practiceSessionDatachannelToken: 'ps-token'},
228
247
  },
229
248
  };
230
249
 
231
250
  webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
232
251
 
252
+ // Default session is connected by default; practice session is not
253
+ webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
254
+ return sessionId !== LLM_PRACTICE_SESSION;
255
+ });
256
+
257
+ // Token is pre-saved into LLM by saveDataChannelToken
258
+ webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
259
+ if (tokenType === DataChannelTokenType.PracticeSession) return 'ps-token';
260
+ return undefined;
261
+ });
262
+
233
263
  // Ensure connect path is eligible
234
264
  webinar.selfIsPanelist = true;
235
265
  webinar.practiceSessionEnabled = true;
@@ -284,11 +314,6 @@ describe('plugin-meetings', () => {
284
314
  it('connects when eligible', async () => {
285
315
  const result = await webinar.updatePSDataChannel();
286
316
 
287
- assert.calledOnceWithExactly(
288
- webex.internal.llm.setDatachannelToken,
289
- 'ps-token',
290
- DataChannelTokenType.PracticeSession
291
- );
292
317
  assert.calledOnce(webex.internal.llm.registerAndConnect);
293
318
  assert.calledWith(
294
319
  webex.internal.llm.registerAndConnect,
@@ -301,8 +326,11 @@ describe('plugin-meetings', () => {
301
326
  assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
302
327
  });
303
328
 
304
- it('uses cached token when available', async () => {
305
- webex.internal.llm.getDatachannelToken.returns('cached-token');
329
+ it('uses token from LLM', async () => {
330
+ webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
331
+ if (tokenType === DataChannelTokenType.PracticeSession) return 'cached-token';
332
+ return undefined;
333
+ });
306
334
 
307
335
  await webinar.updatePSDataChannel();
308
336
 
@@ -360,6 +388,101 @@ describe('plugin-meetings', () => {
360
388
 
361
389
  assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
362
390
  });
391
+
392
+ it('defers connect when default session is not yet connected', async () => {
393
+ // Default session is not connected initially
394
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
395
+
396
+ const result = await webinar.updatePSDataChannel();
397
+
398
+ // Should return undefined immediately (deferred)
399
+ assert.isUndefined(result);
400
+ // Should register an 'online' listener but NOT call registerAndConnect yet
401
+ assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
402
+ assert.notCalled(webex.internal.llm.registerAndConnect);
403
+ // Should store the pending listener
404
+ assert.isNotNull(webinar._pendingOnlineListener);
405
+ });
406
+
407
+ it('does not register duplicate online listeners on repeated calls', async () => {
408
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
409
+
410
+ await webinar.updatePSDataChannel();
411
+ await webinar.updatePSDataChannel();
412
+ await webinar.updatePSDataChannel();
413
+
414
+ // Only one 'online' listener should have been registered
415
+ const onlineCalls = webex.internal.llm.on.args.filter(([event]) => event === 'online');
416
+ assert.equal(onlineCalls.length, 1, 'should register exactly one online listener');
417
+ });
418
+
419
+ it('re-invokes updatePSDataChannel when default session comes online', async () => {
420
+ // Default session is not connected initially
421
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
422
+
423
+ const updatePSDataChannelSpy = sinon.spy(webinar, 'updatePSDataChannel');
424
+
425
+ // First call defers
426
+ await webinar.updatePSDataChannel();
427
+
428
+ // Capture the 'online' listener
429
+ const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
430
+ assert.isDefined(onlineCall, 'should have registered an online listener');
431
+
432
+ // Now simulate default session coming online
433
+ webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
434
+ return sessionId !== LLM_PRACTICE_SESSION;
435
+ });
436
+
437
+ // Fire the captured listener
438
+ onlineCall[1]();
439
+
440
+ // The listener should have cleared itself, removed itself, and re-called updatePSDataChannel
441
+ assert.isNull(webinar._pendingOnlineListener);
442
+ assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
443
+ assert.equal(updatePSDataChannelSpy.callCount, 2);
444
+ });
445
+
446
+ it('does not reconnect with stale data if demoted before default session comes online', async () => {
447
+ // Default session is not connected initially
448
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
449
+
450
+ await webinar.updatePSDataChannel();
451
+
452
+ // Capture the 'online' listener
453
+ const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
454
+ assert.isDefined(onlineCall);
455
+
456
+ // Simulate demotion while waiting
457
+ webinar.selfIsPanelist = false;
458
+
459
+ // Now default session comes online
460
+ webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
461
+ return sessionId !== LLM_PRACTICE_SESSION;
462
+ });
463
+
464
+ // Fire the listener — re-invokes updatePSDataChannel which will see isPracticeSession = false
465
+ onlineCall[1]();
466
+
467
+ // Should NOT have called registerAndConnect since the user is no longer eligible
468
+ assert.notCalled(webex.internal.llm.registerAndConnect);
469
+ });
470
+
471
+ it('proceeds immediately when default session is already connected', async () => {
472
+ // Default session already connected, practice session not
473
+ webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
474
+ return sessionId !== LLM_PRACTICE_SESSION;
475
+ });
476
+
477
+ const result = await webinar.updatePSDataChannel();
478
+
479
+ // The 'online' listener is registered then immediately removed since default session is already connected
480
+ assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
481
+ assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
482
+ assert.isNull(webinar._pendingOnlineListener);
483
+ assert.calledOnce(webex.internal.llm.registerAndConnect);
484
+ assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
485
+ });
363
486
  });
364
487
 
365
488
  describe('#updateStatusByRole', () => {
@@ -369,6 +492,7 @@ describe('plugin-meetings', () => {
369
492
  webinar.webex.meetings = {
370
493
  getMeetingByType: sinon.stub().returns({
371
494
  id: 'meeting-id',
495
+ isJoined: sinon.stub().returns(false),
372
496
  updateLLMConnection: sinon.stub(),
373
497
  shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
374
498
  locusInfo: {
@@ -423,6 +547,7 @@ describe('plugin-meetings', () => {
423
547
  webinar.webex.meetings = {
424
548
  getMeetingByType: sinon.stub().returns({
425
549
  id: 'meeting-id',
550
+ isJoined: sinon.stub().returns(false),
426
551
  updateLLMConnection: sinon.stub(),
427
552
  shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
428
553
  locusInfo: {