@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.7

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.
@@ -1982,11 +1982,12 @@ describe('plugin-meetings', () => {
1982
1982
  describe('#handleLLMOnline', () => {
1983
1983
  beforeEach(() => {
1984
1984
  webex.internal.llm.off = sinon.stub();
1985
+ webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
1986
+ webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
1985
1987
  });
1986
1988
 
1987
- it('turns off llm online, emits transcription connected events', () => {
1989
+ it('emits transcription connected events', () => {
1988
1990
  meeting.handleLLMOnline();
1989
- assert.calledOnceWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
1990
1991
  assert.calledWith(
1991
1992
  TriggerProxy.trigger,
1992
1993
  sinon.match.instanceOf(Meeting),
@@ -1997,6 +1998,24 @@ describe('plugin-meetings', () => {
1997
1998
  EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1998
1999
  );
1999
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
+ });
2000
2019
  });
2001
2020
 
2002
2021
  describe('#join', () => {
@@ -2016,6 +2035,7 @@ describe('plugin-meetings', () => {
2016
2035
  it('should have #join', () => {
2017
2036
  assert.exists(meeting.join);
2018
2037
  });
2038
+
2019
2039
  beforeEach(() => {
2020
2040
  setCorrelationIdSpy = sinon.spy(meeting, 'setCorrelationId');
2021
2041
  meeting.setLocus = sinon.stub().returns(true);
@@ -2169,7 +2189,6 @@ describe('plugin-meetings', () => {
2169
2189
  await meeting.join().catch(() => {
2170
2190
  assert.calledOnce(MeetingUtil.joinMeeting);
2171
2191
 
2172
- // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2173
2192
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2174
2193
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2175
2194
  name: 'client.call.initiated',
@@ -2201,6 +2220,7 @@ describe('plugin-meetings', () => {
2201
2220
  });
2202
2221
  });
2203
2222
  });
2223
+
2204
2224
  describe('lmm, transcription & permissionTokenRefresh decoupling', () => {
2205
2225
  beforeEach(() => {
2206
2226
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
@@ -2271,7 +2291,6 @@ describe('plugin-meetings', () => {
2271
2291
  const locusInfoParseStub = sinon.stub(meeting.locusInfo, 'parse');
2272
2292
  sinon.stub(meeting, 'isJoined').returns(true);
2273
2293
 
2274
- // Set up llm.on stub to capture the registered listener when updateLLMConnection is called
2275
2294
  let locusLLMEventListener;
2276
2295
  meeting.webex.internal.llm.on = sinon.stub().callsFake((eventName, callback) => {
2277
2296
  if (eventName === 'event:locus.state_message') {
@@ -2280,16 +2299,12 @@ describe('plugin-meetings', () => {
2280
2299
  });
2281
2300
  meeting.webex.internal.llm.off = sinon.stub();
2282
2301
 
2283
- // we need the real meeting.updateLLMConnection not the mock
2284
2302
  meeting.updateLLMConnection.restore();
2285
2303
 
2286
- // Call updateLLMConnection to register the listener
2287
2304
  await meeting.updateLLMConnection();
2288
2305
 
2289
- // Verify the listener was registered and we captured it
2290
2306
  assert.isDefined(locusLLMEventListener, 'LLM event listener should be registered');
2291
2307
 
2292
- // Now trigger the event
2293
2308
  const eventData = {
2294
2309
  eventType: 'locus.state_message',
2295
2310
  stateElementsMessage: {
@@ -2309,13 +2324,10 @@ describe('plugin-meetings', () => {
2309
2324
  sinon.stub(meeting.webex.internal.llm, 'hasEverConnected').value(true);
2310
2325
  sinon.stub(meeting.webex.internal.llm, 'registerAndConnect').resolves({});
2311
2326
 
2312
- // Restore the real updateLLMConnection
2313
2327
  meeting.updateLLMConnection.restore();
2314
2328
 
2315
- // Call updateLLMConnection to start the timer
2316
2329
  await meeting.updateLLMConnection();
2317
2330
 
2318
- // Fast forward time by 3 minutes
2319
2331
  fakeClock.tick(3 * 60 * 1000);
2320
2332
 
2321
2333
  assert.calledWith(
@@ -2340,18 +2352,14 @@ describe('plugin-meetings', () => {
2340
2352
  .stub(meeting.webex.internal.llm, 'getDatachannelUrl')
2341
2353
  .returns('https://datachannel1.example.com');
2342
2354
 
2343
- // Restore the real updateLLMConnection
2344
2355
  meeting.updateLLMConnection.restore();
2345
2356
 
2346
- // First, connect LLM and start the timer
2347
2357
  isJoinedStub.returns(true);
2348
2358
  meeting.webex.internal.llm.isConnected.returns(false);
2349
2359
  await meeting.updateLLMConnection();
2350
2360
 
2351
- // Verify timer was started
2352
2361
  assert.exists(meeting.llmHealthCheckTimer);
2353
2362
 
2354
- // Now simulate that we're no longer joined
2355
2363
  isJoinedStub.returns(false);
2356
2364
  meeting.webex.internal.llm.isConnected.returns(true);
2357
2365
 
@@ -2359,10 +2367,8 @@ describe('plugin-meetings', () => {
2359
2367
 
2360
2368
  assert.calledOnce(meeting.webex.internal.llm.disconnectLLM);
2361
2369
 
2362
- // Verify the timer was cleared (should be undefined)
2363
2370
  assert.isUndefined(meeting.llmHealthCheckTimer);
2364
2371
 
2365
- // Fast forward time to ensure no metric is sent
2366
2372
  Metrics.sendBehavioralMetric.resetHistory();
2367
2373
  fakeClock.tick(3 * 60 * 1000);
2368
2374
 
@@ -2397,7 +2403,6 @@ describe('plugin-meetings', () => {
2397
2403
  .stub()
2398
2404
  .rejects(new CaptchaError('bad captcha'));
2399
2405
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2400
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil, 'joinMeetingOptions');
2401
2406
 
2402
2407
  try {
2403
2408
  await meeting.join();
@@ -2411,8 +2416,7 @@ describe('plugin-meetings', () => {
2411
2416
  );
2412
2417
  assert.instanceOf(error, CaptchaError);
2413
2418
  assert.equal(error.message, 'bad captcha');
2414
- // should not get to the end promise chain, which does do the join
2415
- assert.notCalled(joinMeetingOptionsSpy);
2419
+ assert.notCalled(MeetingUtil.joinMeeting);
2416
2420
  }
2417
2421
  });
2418
2422
 
@@ -2421,7 +2425,6 @@ describe('plugin-meetings', () => {
2421
2425
  .stub()
2422
2426
  .rejects(new PasswordError('bad password'));
2423
2427
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2424
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
2425
2428
 
2426
2429
  try {
2427
2430
  await meeting.join();
@@ -2435,8 +2438,7 @@ describe('plugin-meetings', () => {
2435
2438
  );
2436
2439
  assert.instanceOf(error, PasswordError);
2437
2440
  assert.equal(error.message, 'bad password');
2438
- // should not get to the end promise chain, which does do the join
2439
- assert.notCalled(joinMeetingOptionsSpy);
2441
+ assert.notCalled(MeetingUtil.joinMeeting);
2440
2442
  }
2441
2443
  });
2442
2444
 
@@ -2445,7 +2447,6 @@ describe('plugin-meetings', () => {
2445
2447
  .stub()
2446
2448
  .rejects(new PermissionError('bad permission'));
2447
2449
  const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
2448
- const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
2449
2450
 
2450
2451
  try {
2451
2452
  await meeting.join();
@@ -2459,14 +2460,14 @@ describe('plugin-meetings', () => {
2459
2460
  );
2460
2461
  assert.instanceOf(error, PermissionError);
2461
2462
  assert.equal(error.message, 'bad permission');
2462
- // should not get to the end promise chain, which does do the join
2463
- assert.notCalled(joinMeetingOptionsSpy);
2463
+ assert.notCalled(MeetingUtil.joinMeeting);
2464
2464
  }
2465
2465
  });
2466
2466
  });
2467
2467
  });
2468
2468
  });
2469
2469
 
2470
+
2470
2471
  describe('#addMedia', () => {
2471
2472
  const muteStateStub = {
2472
2473
  handleClientRequest: sinon.stub().returns(Promise.resolve(true)),
@@ -10416,14 +10417,24 @@ describe('plugin-meetings', () => {
10416
10417
  );
10417
10418
  done();
10418
10419
  });
10419
- it('listens to the self admitted guest event', (done) => {
10420
+ it('listens to the self admitted guest event without blocking on token prefetch', async () => {
10420
10421
  meeting.stopKeepAlive = sinon.stub();
10421
10422
  meeting.updateLLMConnection = sinon.stub();
10423
+ let resolvePrefetch;
10424
+
10425
+ meeting.ensureDefaultDatachannelTokenAfterAdmit = sinon
10426
+ .stub()
10427
+ .returns(new Promise((resolve) => {
10428
+ resolvePrefetch = resolve;
10429
+ }));
10422
10430
  meeting.rtcMetrics = {
10423
10431
  sendNextMetrics: sinon.stub(),
10424
10432
  };
10433
+
10425
10434
  meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
10435
+
10426
10436
  assert.calledOnceWithExactly(meeting.stopKeepAlive);
10437
+ assert.calledOnceWithExactly(meeting.ensureDefaultDatachannelTokenAfterAdmit);
10427
10438
  assert.calledThrice(TriggerProxy.trigger);
10428
10439
  assert.calledWith(
10429
10440
  TriggerProxy.trigger,
@@ -10442,7 +10453,11 @@ describe('plugin-meetings', () => {
10442
10453
  correlation_id: meeting.correlationId,
10443
10454
  }
10444
10455
  );
10445
- done();
10456
+
10457
+ resolvePrefetch(false);
10458
+ await Promise.resolve();
10459
+
10460
+ assert.calledOnce(meeting.updateLLMConnection);
10446
10461
  });
10447
10462
 
10448
10463
  it('listens to the breakouts changed event', () => {
@@ -33,6 +33,7 @@ describe('plugin-meetings', () => {
33
33
  webex.internal.llm = {
34
34
  getDatachannelToken: sinon.stub().returns(undefined),
35
35
  setDatachannelToken: sinon.stub(),
36
+ isDataChannelTokenEnabled: sinon.stub().resolves(false),
36
37
  isConnected: sinon.stub().returns(false),
37
38
  disconnectLLM: sinon.stub().resolves(),
38
39
  off: sinon.stub(),
@@ -267,6 +268,65 @@ describe('plugin-meetings', () => {
267
268
  webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
268
269
  });
269
270
 
271
+ it('refreshes practice-session token before register when cached token is missing', async () => {
272
+ webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
273
+ webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
274
+ if (tokenType === DataChannelTokenType.PracticeSession) return undefined;
275
+
276
+ return undefined;
277
+ });
278
+ meeting.refreshDataChannelToken = sinon.stub().resolves({
279
+ body: {
280
+ datachannelToken: 'ps-token-from-refresh',
281
+ dataChannelTokenType: DataChannelTokenType.PracticeSession,
282
+ },
283
+ });
284
+
285
+ await webinar.updatePSDataChannel();
286
+
287
+ assert.calledOnceWithExactly(meeting.refreshDataChannelToken);
288
+ assert.calledWithExactly(
289
+ webex.internal.llm.setDatachannelToken,
290
+ 'ps-token-from-refresh',
291
+ DataChannelTokenType.PracticeSession
292
+ );
293
+ assert.calledWith(
294
+ webex.internal.llm.registerAndConnect,
295
+ 'locus-url',
296
+ 'dc-url',
297
+ 'ps-token-from-refresh',
298
+ LLM_PRACTICE_SESSION
299
+ );
300
+ });
301
+
302
+ it('does not reconnect if practice-session eligibility changes during async token refresh', async () => {
303
+ webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
304
+ webex.internal.llm.getDatachannelToken = sinon.stub().returns(undefined);
305
+
306
+ let resolveRefresh;
307
+ meeting.refreshDataChannelToken = sinon.stub().returns(
308
+ new Promise((resolve) => {
309
+ resolveRefresh = resolve;
310
+ })
311
+ );
312
+
313
+ const updatePromise = webinar.updatePSDataChannel();
314
+
315
+ webinar.practiceSessionEnabled = false;
316
+
317
+ resolveRefresh({
318
+ body: {
319
+ datachannelToken: 'stale-ps-token',
320
+ dataChannelTokenType: DataChannelTokenType.PracticeSession,
321
+ },
322
+ });
323
+
324
+ const result = await updatePromise;
325
+
326
+ assert.isUndefined(result);
327
+ assert.notCalled(webex.internal.llm.registerAndConnect);
328
+ });
329
+
270
330
  it('no-ops when practice session join eligibility is false', async () => {
271
331
  webinar.practiceSessionEnabled = false;
272
332
  const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();