@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,7 @@ import DataChannelAuthTokenInterceptor from '@webex/plugin-meetings/src/intercep
7
7
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
8
8
  import * as utils from '@webex/plugin-meetings/src/interceptors/utils';
9
9
  import {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY} from '@webex/plugin-meetings/src/interceptors/constant';
10
+ import {LOCUS_URL} from '@webex/plugin-meetings/src/constants';
10
11
 
11
12
  describe('plugin-meetings', () => {
12
13
  describe('Interceptors', () => {
@@ -178,6 +179,28 @@ describe('plugin-meetings', () => {
178
179
  expect(result).to.equal('mock-response');
179
180
  });
180
181
 
182
+ it('passes request URL to _refreshDataChannelToken', async () => {
183
+ const psOptions = {
184
+ headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'},
185
+ method: 'POST',
186
+ uri: 'https://locus.example.com/practiceSession/datachannel',
187
+ };
188
+
189
+ interceptor._refreshDataChannelToken.resolves('new-token');
190
+ webex.request.resolves('mock-response');
191
+
192
+ const promise = interceptor.refreshTokenAndRetryWithDelay(psOptions);
193
+
194
+ clock.tick(2000);
195
+
196
+ await promise;
197
+
198
+ sinon.assert.calledOnceWithExactly(
199
+ interceptor._refreshDataChannelToken,
200
+ psOptions.uri
201
+ );
202
+ });
203
+
181
204
  it('rejects when refreshDataChannelToken fails', async () => {
182
205
  interceptor._refreshDataChannelToken.rejects(new Error('refresh failed'));
183
206
 
@@ -205,6 +228,179 @@ describe('plugin-meetings', () => {
205
228
  );
206
229
  });
207
230
  });
231
+
232
+ describe('refreshDataChannelToken routing (factory dispatcher)', () => {
233
+ let llmMock;
234
+ let meetingA;
235
+ let meetingsMock;
236
+ let dispatcherInterceptor;
237
+
238
+ const PS_DATACHANNEL_URL = 'https://board-a.wbx2.com/datachannel/api/v1/locus/cHJhY3RpY2Vfc2Vzc2lvbl9sb2N1cw==/registrations';
239
+ const DEFAULT_DATACHANNEL_URL = 'https://board-a.wbx2.com/datachannel/api/v1/locus/aHR0cHM6Ly9sb2N1cy1hLndieDIuY29t/registrations';
240
+
241
+ beforeEach(() => {
242
+ meetingA = {
243
+ id: 'meeting-a',
244
+ refreshDataChannelToken: sinon.stub().resolves({
245
+ body: {
246
+ datachannelToken: 'token-from-meeting-a',
247
+ dataChannelTokenType: 'llm-practice-session',
248
+ },
249
+ }),
250
+ };
251
+
252
+ llmMock = {
253
+ isDataChannelTokenEnabled: sinon.stub().resolves(true),
254
+ getSessionIdByDatachannelUrl: sinon.stub(),
255
+ getLocusUrlByDatachannelUrl: sinon.stub(),
256
+ getOwnerMeetingId: sinon.stub().returns(undefined),
257
+ refreshDataChannelToken: sinon.stub().resolves({
258
+ body: {
259
+ datachannelToken: 'token-from-llm-fallback',
260
+ dataChannelTokenType: 'llm-default-session',
261
+ },
262
+ }),
263
+ setDatachannelToken: sinon.stub(),
264
+ };
265
+
266
+ meetingsMock = {
267
+ getMeetingByType: sinon.stub(),
268
+ };
269
+
270
+ const context = {
271
+ internal: {llm: llmMock},
272
+ meetings: meetingsMock,
273
+ };
274
+
275
+ dispatcherInterceptor = Reflect.apply(DataChannelAuthTokenInterceptor.create, context, []);
276
+ });
277
+
278
+ it('routes PS request URL to PS session handler', async () => {
279
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns('llm-practice-session');
280
+ llmMock.refreshDataChannelToken
281
+ .withArgs('llm-practice-session')
282
+ .resolves({
283
+ body: {
284
+ datachannelToken: 'token-from-ps-session',
285
+ dataChannelTokenType: 'llm-practice-session',
286
+ },
287
+ });
288
+
289
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
290
+
291
+ expect(token).to.equal('token-from-ps-session');
292
+ sinon.assert.calledOnceWithExactly(llmMock.refreshDataChannelToken, 'llm-practice-session');
293
+ sinon.assert.calledOnceWithExactly(
294
+ llmMock.setDatachannelToken,
295
+ 'token-from-ps-session',
296
+ 'llm-practice-session',
297
+ undefined
298
+ );
299
+ });
300
+
301
+ it('routes non-PS URL to default session handler', async () => {
302
+ llmMock.getSessionIdByDatachannelUrl.withArgs(DEFAULT_DATACHANNEL_URL).returns('llm-default-session');
303
+ llmMock.refreshDataChannelToken
304
+ .withArgs('llm-default-session')
305
+ .resolves({
306
+ body: {
307
+ datachannelToken: 'token-from-default-session',
308
+ dataChannelTokenType: 'llm-default-session',
309
+ },
310
+ });
311
+
312
+ const token = await dispatcherInterceptor._refreshDataChannelToken(DEFAULT_DATACHANNEL_URL);
313
+
314
+ expect(token).to.equal('token-from-default-session');
315
+ sinon.assert.calledOnceWithExactly(llmMock.refreshDataChannelToken, 'llm-default-session');
316
+ sinon.assert.calledOnceWithExactly(
317
+ llmMock.setDatachannelToken,
318
+ 'token-from-default-session',
319
+ 'llm-default-session',
320
+ undefined
321
+ );
322
+ });
323
+
324
+ it('falls back to default refresh when URL does not match any session or meeting route', async () => {
325
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
326
+ llmMock.getLocusUrlByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
327
+ llmMock.refreshDataChannelToken.withArgs(undefined).resolves({
328
+ body: {
329
+ datachannelToken: 'token-from-default-fallback',
330
+ dataChannelTokenType: 'llm-default-session',
331
+ },
332
+ });
333
+
334
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
335
+
336
+ expect(token).to.equal('token-from-default-fallback');
337
+ sinon.assert.calledOnceWithExactly(llmMock.refreshDataChannelToken, undefined);
338
+ sinon.assert.calledOnceWithExactly(
339
+ llmMock.setDatachannelToken,
340
+ 'token-from-default-fallback',
341
+ 'llm-default-session',
342
+ undefined
343
+ );
344
+ });
345
+
346
+ it('falls back to meeting lookup by locusUrl when session cannot be resolved', async () => {
347
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
348
+ llmMock.getLocusUrlByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns('https://locus-a.example.com');
349
+ meetingsMock.getMeetingByType.withArgs(LOCUS_URL, 'https://locus-a.example.com').returns(meetingA);
350
+
351
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
352
+
353
+ expect(token).to.equal('token-from-meeting-a');
354
+ sinon.assert.calledOnceWithExactly(meetingA.refreshDataChannelToken);
355
+ sinon.assert.notCalled(llmMock.refreshDataChannelToken);
356
+ sinon.assert.calledOnceWithExactly(
357
+ llmMock.setDatachannelToken,
358
+ 'token-from-meeting-a',
359
+ 'llm-practice-session',
360
+ 'meeting-a'
361
+ );
362
+ });
363
+
364
+ it('falls back to active meeting datachannel URL lookup when session/locus routing is unavailable', async () => {
365
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
366
+ llmMock.getLocusUrlByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
367
+ meetingsMock.getAllMeetings = sinon.stub().returns({
368
+ 'meeting-a': {
369
+ ...meetingA,
370
+ locusInfo: {
371
+ info: {
372
+ practiceSessionDatachannelUrl: PS_DATACHANNEL_URL,
373
+ datachannelUrl: DEFAULT_DATACHANNEL_URL,
374
+ },
375
+ },
376
+ },
377
+ });
378
+
379
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
380
+
381
+ expect(token).to.equal('token-from-meeting-a');
382
+ sinon.assert.calledOnceWithExactly(meetingA.refreshDataChannelToken);
383
+ sinon.assert.notCalled(llmMock.refreshDataChannelToken);
384
+ sinon.assert.calledOnceWithExactly(
385
+ llmMock.setDatachannelToken,
386
+ 'token-from-meeting-a',
387
+ 'llm-practice-session',
388
+ 'meeting-a'
389
+ );
390
+ });
391
+
392
+ it('throws when refresh returns no payload', async () => {
393
+ llmMock.getSessionIdByDatachannelUrl.returns('llm-default-session');
394
+ llmMock.refreshDataChannelToken.withArgs('llm-default-session').resolves(null);
395
+
396
+ await assert.isRejected(
397
+ dispatcherInterceptor._refreshDataChannelToken(
398
+ 'https://unknown-datachannel.example.com/registrations'
399
+ ),
400
+ /DataChannel token refresh returned no payload/
401
+ );
402
+ });
403
+ });
208
404
  });
209
405
  });
210
406
  });
@@ -269,6 +269,20 @@ describe('plugin-meetings', () => {
269
269
  stopReachability: sinon.stub(),
270
270
  isSubnetReachable: sinon.stub().returns(true),
271
271
  };
272
+ webex.internal.llm.resolveSessionOwnership = sinon
273
+ .stub()
274
+ .callsFake((ownerMeetingId, sessionId) => {
275
+ const currentOwner = webex.internal.llm.getOwnerMeetingId
276
+ ? webex.internal.llm.getOwnerMeetingId(sessionId)
277
+ : undefined;
278
+ const canAssertOwnership = !!ownerMeetingId;
279
+
280
+ return {
281
+ currentOwner,
282
+ canAssertOwnership,
283
+ isOwner: !currentOwner || !canAssertOwnership || currentOwner === ownerMeetingId,
284
+ };
285
+ });
272
286
  webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
273
287
  webex.internal.llm.on = sinon.stub();
274
288
  webex.internal.voicea.announce = sinon.stub();
@@ -11107,7 +11121,7 @@ describe('plugin-meetings', () => {
11107
11121
  );
11108
11122
  done();
11109
11123
  });
11110
- it('listens to the self admitted guest event without blocking on token prefetch', async () => {
11124
+ it('listens to the self admitted guest event and waits for token prefetch before reconnecting LLM', async () => {
11111
11125
  meeting.stopKeepAlive = sinon.stub();
11112
11126
  meeting.updateLLMConnection = sinon.stub();
11113
11127
  let resolvePrefetch;
@@ -11133,7 +11147,7 @@ describe('plugin-meetings', () => {
11133
11147
  'meeting:self:guestAdmitted',
11134
11148
  {payload: test1}
11135
11149
  );
11136
- assert.calledOnce(meeting.updateLLMConnection);
11150
+ assert.notCalled(meeting.updateLLMConnection);
11137
11151
  assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics);
11138
11152
 
11139
11153
  assert.calledOnceWithExactly(
@@ -11146,6 +11160,7 @@ describe('plugin-meetings', () => {
11146
11160
 
11147
11161
  resolvePrefetch(false);
11148
11162
  await Promise.resolve();
11163
+ await Promise.resolve();
11149
11164
 
11150
11165
  assert.calledOnce(meeting.updateLLMConnection);
11151
11166
  });
@@ -13670,6 +13685,10 @@ describe('plugin-meetings', () => {
13670
13685
  describe('#saveDataChannelToken', () => {
13671
13686
  beforeEach(() => {
13672
13687
  webex.internal.llm.setDatachannelToken = sinon.stub();
13688
+ webex.internal.llm.resolveSessionOwnership = sinon
13689
+ .stub()
13690
+ .returns({currentOwner: undefined, isOwner: true});
13691
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
13673
13692
  });
13674
13693
 
13675
13694
  it('saves datachannelToken into LLM as Default', () => {
@@ -13682,7 +13701,8 @@ describe('plugin-meetings', () => {
13682
13701
  assert.calledWithExactly(
13683
13702
  webex.internal.llm.setDatachannelToken,
13684
13703
  'default-token',
13685
- 'llm-default-session'
13704
+ 'llm-default-session',
13705
+ meeting.id
13686
13706
  );
13687
13707
  });
13688
13708
 
@@ -13696,7 +13716,8 @@ describe('plugin-meetings', () => {
13696
13716
  assert.calledWithExactly(
13697
13717
  webex.internal.llm.setDatachannelToken,
13698
13718
  'ps-token',
13699
- 'llm-practice-session'
13719
+ 'llm-practice-session',
13720
+ meeting.id
13700
13721
  );
13701
13722
  });
13702
13723
 
@@ -13714,12 +13735,14 @@ describe('plugin-meetings', () => {
13714
13735
  assert.calledWithExactly(
13715
13736
  webex.internal.llm.setDatachannelToken,
13716
13737
  'default-token',
13717
- 'llm-default-session'
13738
+ 'llm-default-session',
13739
+ meeting.id
13718
13740
  );
13719
13741
  assert.calledWithExactly(
13720
13742
  webex.internal.llm.setDatachannelToken,
13721
13743
  'ps-token',
13722
- 'llm-practice-session'
13744
+ 'llm-practice-session',
13745
+ meeting.id
13723
13746
  );
13724
13747
  });
13725
13748
 
@@ -13740,17 +13763,42 @@ describe('plugin-meetings', () => {
13740
13763
 
13741
13764
  assert.notCalled(webex.internal.llm.setDatachannelToken);
13742
13765
  });
13766
+
13767
+ it('writes token with meeting id as owner', () => {
13768
+ meeting.saveDataChannelToken({
13769
+ locus: {
13770
+ self: {datachannelToken: 'default-token'},
13771
+ },
13772
+ });
13773
+
13774
+ assert.calledOnceWithExactly(
13775
+ webex.internal.llm.setDatachannelToken,
13776
+ 'default-token',
13777
+ 'llm-default-session',
13778
+ meeting.id
13779
+ );
13780
+ });
13743
13781
  });
13744
13782
 
13745
13783
  describe('#clearDataChannelToken', () => {
13746
13784
  beforeEach(() => {
13747
- webex.internal.llm.resetDatachannelTokens = sinon.stub();
13785
+ webex.internal.llm.clearDatachannelToken = sinon.stub();
13748
13786
  });
13749
13787
 
13750
- it('calls resetDatachannelTokens on LLM', () => {
13788
+ it('delegates default and practice token clears to llm with meeting ownership id', () => {
13751
13789
  meeting.clearDataChannelToken();
13752
13790
 
13753
- assert.calledOnce(webex.internal.llm.resetDatachannelTokens);
13791
+ assert.calledWithExactly(
13792
+ webex.internal.llm.clearDatachannelToken,
13793
+ 'llm-default-session',
13794
+ meeting.id
13795
+ );
13796
+ assert.calledWithExactly(
13797
+ webex.internal.llm.clearDatachannelToken,
13798
+ 'llm-practice-session',
13799
+ meeting.id
13800
+ );
13801
+ assert.callCount(webex.internal.llm.clearDatachannelToken, 2);
13754
13802
  });
13755
13803
  });
13756
13804
 
@@ -13760,6 +13808,7 @@ describe('plugin-meetings', () => {
13760
13808
  webex.internal.llm.getLocusUrl = sinon.stub();
13761
13809
  webex.internal.llm.getDatachannelUrl = sinon.stub();
13762
13810
  webex.internal.llm.registerAndConnect = sinon.stub().resolves('something');
13811
+ webex.internal.llm.setRefreshHandler = sinon.stub();
13763
13812
  webex.internal.llm.disconnectLLM = sinon.stub().resolves();
13764
13813
  webex.internal.llm.on = sinon.stub();
13765
13814
  webex.internal.llm.off = sinon.stub();
@@ -13837,6 +13886,12 @@ describe('plugin-meetings', () => {
13837
13886
  'a datachannel url',
13838
13887
  undefined
13839
13888
  );
13889
+ assert.calledOnceWithExactly(
13890
+ webex.internal.llm.setRefreshHandler,
13891
+ sinon.match.func,
13892
+ 'llm-default-session',
13893
+ meeting.id
13894
+ );
13840
13895
  assert.equal(result, 'something');
13841
13896
  assert.calledOnceWithExactly(meeting.locusInfo.syncAllHashTreeDatasets, {onlyLLM: true});
13842
13897
  });
@@ -13858,7 +13913,7 @@ describe('plugin-meetings', () => {
13858
13913
  assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
13859
13914
  code: 3050,
13860
13915
  reason: 'done (permanent)',
13861
- });
13916
+ }, 'llm-default-session', meeting.id);
13862
13917
 
13863
13918
  assert.calledWithExactly(
13864
13919
  webex.internal.llm.registerAndConnect,
@@ -13912,7 +13967,7 @@ describe('plugin-meetings', () => {
13912
13967
  assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
13913
13968
  code: 3050,
13914
13969
  reason: 'done (permanent)',
13915
- });
13970
+ }, 'llm-default-session', meeting.id);
13916
13971
 
13917
13972
  assert.calledWithExactly(
13918
13973
  webex.internal.llm.registerAndConnect,
@@ -13960,7 +14015,7 @@ describe('plugin-meetings', () => {
13960
14015
  assert.calledWith(webex.internal.llm.disconnectLLM, {
13961
14016
  code: 3050,
13962
14017
  reason: 'done (permanent)',
13963
- });
14018
+ }, 'llm-default-session', meeting.id);
13964
14019
  assert.notCalled(webex.internal.llm.registerAndConnect);
13965
14020
  assert.equal(result, undefined);
13966
14021
  assert.isFalse(
@@ -14035,7 +14090,7 @@ describe('plugin-meetings', () => {
14035
14090
  };
14036
14091
 
14037
14092
  webex.internal.llm.getDatachannelToken
14038
- .withArgs('llm-default-session')
14093
+ .withArgs('llm-default-session', meeting.id)
14039
14094
  .returns('token-123');
14040
14095
 
14041
14096
  await meeting.updateLLMConnection();
@@ -14157,6 +14212,33 @@ describe('plugin-meetings', () => {
14157
14212
  assert.calledWith(webex.internal.llm.setOwnerMeetingId, undefined);
14158
14213
  });
14159
14214
 
14215
+ it('does not clear owner tag when ownership changes during cleanup disconnect await', async () => {
14216
+ meeting.joinedWith = {state: 'JOINED'};
14217
+ webex.internal.llm.isConnected.returns(true);
14218
+ webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
14219
+ webex.internal.llm.getLocusUrl.returns('a url');
14220
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
14221
+ webex.internal.llm.disconnectLLM.callsFake(async () => {
14222
+ webex.internal.llm.getOwnerMeetingId.returns('new-owner-id');
14223
+ throw new Error('disconnect failed');
14224
+ });
14225
+ meeting.locusInfo = {
14226
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
14227
+ url: 'a different url',
14228
+ info: {datachannelUrl: 'a datachannel url'},
14229
+ self: {},
14230
+ };
14231
+
14232
+ try {
14233
+ await meeting.updateLLMConnection();
14234
+ } catch (e) {
14235
+ /* updateLLMConnection may reject when cleanup throws */
14236
+ }
14237
+
14238
+ assert.notCalled(webex.internal.llm.setOwnerMeetingId);
14239
+ assert.equal(webex.internal.llm.getOwnerMeetingId(), 'new-owner-id');
14240
+ });
14241
+
14160
14242
  it('proceeds normally when LLM is connected and owned by this meeting with URL change', async () => {
14161
14243
  meeting.joinedWith = {state: 'JOINED'};
14162
14244
  webex.internal.llm.isConnected.returns(true);
@@ -14175,7 +14257,7 @@ describe('plugin-meetings', () => {
14175
14257
  assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
14176
14258
  code: 3050,
14177
14259
  reason: 'done (permanent)',
14178
- });
14260
+ }, 'llm-default-session', meeting.id);
14179
14261
  assert.calledWithExactly(
14180
14262
  webex.internal.llm.registerAndConnect,
14181
14263
  'a different url',
@@ -14200,6 +14282,12 @@ describe('plugin-meetings', () => {
14200
14282
  await meeting.updateLLMConnection();
14201
14283
 
14202
14284
  assert.calledOnce(webex.internal.llm.registerAndConnect);
14285
+ assert.calledOnceWithExactly(
14286
+ webex.internal.llm.setRefreshHandler,
14287
+ sinon.match.func,
14288
+ 'llm-default-session',
14289
+ meeting.id
14290
+ );
14203
14291
  assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
14204
14292
  });
14205
14293
 
@@ -14211,11 +14299,41 @@ describe('plugin-meetings', () => {
14211
14299
  meeting.joinedWith = {state: 'JOINED'};
14212
14300
  webex.internal.llm.isConnected.returns(false);
14213
14301
  webex.internal.llm.getOwnerMeetingId.returns('stale-owner-id');
14302
+ webex.internal.llm.getDatachannelToken.onFirstCall().returns(undefined);
14303
+ webex.internal.llm.getDatachannelToken.onSecondCall().returns('recovered-token');
14214
14304
  meeting.locusInfo = {syncAllHashTreeDatasets: sinon.stub().resolves(), url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
14215
14305
 
14216
14306
  await meeting.updateLLMConnection();
14217
14307
 
14218
- assert.calledOnce(webex.internal.llm.registerAndConnect);
14308
+ assert.calledTwice(webex.internal.llm.getDatachannelToken);
14309
+ assert.calledWithExactly(
14310
+ webex.internal.llm.getDatachannelToken.firstCall,
14311
+ 'llm-default-session',
14312
+ meeting.id
14313
+ );
14314
+ assert.calledWithExactly(
14315
+ webex.internal.llm.getDatachannelToken.secondCall,
14316
+ 'llm-default-session'
14317
+ );
14318
+ assert.calledOnceWithExactly(
14319
+ webex.internal.llm.registerAndConnect,
14320
+ 'a url',
14321
+ 'a datachannel url',
14322
+ 'recovered-token'
14323
+ );
14324
+ assert.calledWithExactly(
14325
+ webex.internal.llm.setRefreshHandler.firstCall,
14326
+ sinon.match.func,
14327
+ 'llm-default-session',
14328
+ undefined
14329
+ );
14330
+ assert.calledTwice(webex.internal.llm.setRefreshHandler);
14331
+ assert.calledWithExactly(
14332
+ webex.internal.llm.setRefreshHandler.secondCall,
14333
+ sinon.match.func,
14334
+ 'llm-default-session',
14335
+ meeting.id
14336
+ );
14219
14337
  assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
14220
14338
  });
14221
14339
  });
@@ -14238,7 +14356,7 @@ describe('plugin-meetings', () => {
14238
14356
  assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
14239
14357
  code: 3050,
14240
14358
  reason: 'done (permanent)',
14241
- });
14359
+ }, 'llm-default-session', meeting.id);
14242
14360
  assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
14243
14361
  assert.calledWithExactly(
14244
14362
  webex.internal.llm.off,
@@ -14292,11 +14410,9 @@ describe('plugin-meetings', () => {
14292
14410
  await meeting.clearMeetingData();
14293
14411
 
14294
14412
  assert.notCalled(webex.internal.llm.disconnectLLM);
14295
- // Shared data-channel auth tokens belong to the owner meeting's
14296
- // live LLM session and must not be wiped by a non-owner
14297
- // teardown, otherwise the owner's next reconnect would lose
14298
- // its Data-Channel-Auth-Token.
14299
- assert.notCalled(meeting.clearDataChannelToken);
14413
+ // clearDataChannelToken is always delegated; llm enforces
14414
+ // ownership and no-ops for non-owners internally.
14415
+ assert.calledOnce(meeting.clearDataChannelToken);
14300
14416
  // Listeners owned by *this* Meeting instance must still be
14301
14417
  // removed so a leaving subordinate meeting stops receiving
14302
14418
  // relay/locus events from the shared singleton.
@@ -14322,7 +14438,7 @@ describe('plugin-meetings', () => {
14322
14438
  assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
14323
14439
  code: 3050,
14324
14440
  reason: 'done (permanent)',
14325
- });
14441
+ }, 'llm-default-session', meeting.id);
14326
14442
  assert.calledOnce(meeting.clearDataChannelToken);
14327
14443
  });
14328
14444
 
@@ -14334,7 +14450,7 @@ describe('plugin-meetings', () => {
14334
14450
  assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
14335
14451
  code: 3050,
14336
14452
  reason: 'done (permanent)',
14337
- });
14453
+ }, 'llm-default-session', meeting.id);
14338
14454
  assert.calledOnce(meeting.clearDataChannelToken);
14339
14455
  });
14340
14456
  });
@@ -347,6 +347,18 @@ describe('plugin-meetings', () => {
347
347
  'ANNOTATION_ON_SHARE_SUPPORTED',
348
348
  ]);
349
349
  });
350
+ it('adds deviceCapabilities to request when simultaneous interpretation is enabled', async () => {
351
+ await meetingsRequest.joinMeeting({
352
+ enableSimultaneousInterpretation: true,
353
+ });
354
+ const requestParams = meetingsRequest.request.getCall(0).args[0];
355
+ assert.deepEqual(requestParams.body.deviceCapabilities, [
356
+ 'HOST_CONTROL_SI_SUPPORTED',
357
+ 'INTERPRETER_CONTROL_SI_SUPPORTED',
358
+ 'SI_HANDOVER_SUPPORTED',
359
+ 'SIGN_INTERPRETER_SUPPORTED',
360
+ ]);
361
+ });
350
362
  it('does not add deviceCapabilities to request when breakouts and live annotation are not supported', async () => {
351
363
  await meetingsRequest.joinMeeting({});
352
364