@webex/plugin-meetings 3.12.0-next.2 → 3.12.0-next.21

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 (67) 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/controls-options-manager/constants.js +11 -1
  5. package/dist/controls-options-manager/constants.js.map +1 -1
  6. package/dist/controls-options-manager/index.js +23 -21
  7. package/dist/controls-options-manager/index.js.map +1 -1
  8. package/dist/controls-options-manager/util.js +91 -0
  9. package/dist/controls-options-manager/util.js.map +1 -1
  10. package/dist/hashTree/constants.js +10 -1
  11. package/dist/hashTree/constants.js.map +1 -1
  12. package/dist/hashTree/hashTreeParser.js +56 -31
  13. package/dist/hashTree/hashTreeParser.js.map +1 -1
  14. package/dist/hashTree/utils.js +22 -0
  15. package/dist/hashTree/utils.js.map +1 -1
  16. package/dist/interpretation/index.js +1 -1
  17. package/dist/interpretation/siLanguage.js +1 -1
  18. package/dist/locus-info/index.js +51 -23
  19. package/dist/locus-info/index.js.map +1 -1
  20. package/dist/meeting/index.js +372 -292
  21. package/dist/meeting/index.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meetings/index.js +8 -9
  25. package/dist/meetings/index.js.map +1 -1
  26. package/dist/meetings/util.js +21 -2
  27. package/dist/meetings/util.js.map +1 -1
  28. package/dist/metrics/constants.js +5 -1
  29. package/dist/metrics/constants.js.map +1 -1
  30. package/dist/multistream/sendSlotManager.js +116 -2
  31. package/dist/multistream/sendSlotManager.js.map +1 -1
  32. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  33. package/dist/types/hashTree/constants.d.ts +1 -0
  34. package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
  35. package/dist/types/hashTree/utils.d.ts +11 -0
  36. package/dist/types/locus-info/index.d.ts +9 -5
  37. package/dist/types/meeting/index.d.ts +11 -0
  38. package/dist/types/metrics/constants.d.ts +4 -0
  39. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  40. package/dist/webinar/index.js +301 -226
  41. package/dist/webinar/index.js.map +1 -1
  42. package/package.json +16 -16
  43. package/src/controls-options-manager/constants.ts +14 -1
  44. package/src/controls-options-manager/index.ts +26 -19
  45. package/src/controls-options-manager/util.ts +81 -1
  46. package/src/hashTree/constants.ts +9 -0
  47. package/src/hashTree/hashTreeParser.ts +60 -36
  48. package/src/hashTree/utils.ts +17 -0
  49. package/src/locus-info/index.ts +56 -30
  50. package/src/meeting/index.ts +98 -11
  51. package/src/meeting/util.ts +1 -0
  52. package/src/meetings/index.ts +15 -16
  53. package/src/meetings/util.ts +26 -1
  54. package/src/metrics/constants.ts +5 -0
  55. package/src/multistream/sendSlotManager.ts +97 -3
  56. package/src/webinar/index.ts +75 -1
  57. package/test/unit/spec/controls-options-manager/index.js +114 -6
  58. package/test/unit/spec/controls-options-manager/util.js +165 -0
  59. package/test/unit/spec/hashTree/hashTreeParser.ts +441 -30
  60. package/test/unit/spec/hashTree/utils.ts +88 -1
  61. package/test/unit/spec/locus-info/index.js +75 -27
  62. package/test/unit/spec/meeting/index.js +54 -36
  63. package/test/unit/spec/meeting/utils.js +4 -0
  64. package/test/unit/spec/meetings/index.js +36 -3
  65. package/test/unit/spec/meetings/utils.js +108 -0
  66. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  67. package/test/unit/spec/webinar/index.ts +60 -0
@@ -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,
@@ -1285,10 +1285,10 @@ describe('plugin-meetings', () => {
1285
1285
  assert.exists(result.dispose);
1286
1286
  });
1287
1287
 
1288
- it('creates noise reduction effect with ST model', async () => {
1288
+ it('creates noise reduction effect with OFMV model', async () => {
1289
1289
  const result = await webex.meetings.createNoiseReductionEffect({
1290
1290
  audioContext: {},
1291
- model: 'st',
1291
+ model: 'ofmv',
1292
1292
  });
1293
1293
 
1294
1294
  assert.exists(result);
@@ -1300,7 +1300,7 @@ describe('plugin-meetings', () => {
1300
1300
  authToken: 'fake_token',
1301
1301
  mode: 'WORKLET',
1302
1302
  avoidSimd: false,
1303
- model: 'st',
1303
+ model: 'ofmv',
1304
1304
  });
1305
1305
  assert.exists(result.enable);
1306
1306
  assert.exists(result.disable);
@@ -2833,6 +2833,39 @@ describe('plugin-meetings', () => {
2833
2833
  checkCreateMeetingWithNoMeetingInfo(true, true);
2834
2834
  });
2835
2835
 
2836
+ it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
2837
+ // Make destroy actually remove the meeting from the collection
2838
+ // so that getMeetingByType returns null in the finally block
2839
+ webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
2840
+ webex.meetings.meetingCollection.delete(meeting.id);
2841
+ });
2842
+
2843
+ try {
2844
+ await webex.meetings.createMeeting(
2845
+ 'test destination',
2846
+ 'test type',
2847
+ undefined,
2848
+ undefined,
2849
+ undefined,
2850
+ true
2851
+ );
2852
+ assert.fail('should have thrown NoMeetingInfoError');
2853
+ } catch (err) {
2854
+ assert.instanceOf(err, NoMeetingInfoError);
2855
+ }
2856
+
2857
+ assert.calledOnce(webex.meetings.destroy);
2858
+
2859
+ // meeting:added should NOT have been triggered since the meeting was destroyed
2860
+ assert.neverCalledWith(
2861
+ TriggerProxy.trigger,
2862
+ sinon.match.any,
2863
+ sinon.match({function: 'createMeeting'}),
2864
+ 'meeting:added',
2865
+ sinon.match.any
2866
+ );
2867
+ });
2868
+
2836
2869
  it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
2837
2870
  const meeting = await webex.meetings.createMeeting(
2838
2871
  'test destination',
@@ -128,6 +128,114 @@ describe('plugin-meetings', () => {
128
128
  };
129
129
  assert.equal(MeetingsUtil.isBreakoutLocusDTO(newLocus), false);
130
130
  });
131
+
132
+ it('returns true if newLocus.info.isBreakout is true', () => {
133
+ const newLocus = {
134
+ info: {
135
+ isBreakout: true,
136
+ },
137
+ };
138
+ assert.equal(MeetingsUtil.isBreakoutLocusDTO(newLocus), true);
139
+ });
140
+
141
+ it('returns false if newLocus.info.isBreakout is false', () => {
142
+ const newLocus = {
143
+ info: {
144
+ isBreakout: false,
145
+ },
146
+ };
147
+ assert.equal(MeetingsUtil.isBreakoutLocusDTO(newLocus), false);
148
+ });
149
+
150
+ it('returns true if both sessionType is BREAKOUT and info.isBreakout is true', () => {
151
+ const newLocus = {
152
+ controls: {
153
+ breakout: {
154
+ sessionType: 'BREAKOUT',
155
+ },
156
+ },
157
+ info: {
158
+ isBreakout: true,
159
+ },
160
+ };
161
+ assert.equal(MeetingsUtil.isBreakoutLocusDTO(newLocus), true);
162
+ });
163
+ });
164
+
165
+ describe('#isMainAssociatedWithBreakout', () => {
166
+ it('returns true when breakout control url matches main locus breakout url', () => {
167
+ const mainLocus = {
168
+ url: 'main-locus-url',
169
+ controls: {
170
+ breakout: {
171
+ url: 'breakout-control-url',
172
+ },
173
+ },
174
+ };
175
+ const breakoutLocus = {
176
+ controls: {
177
+ breakout: {
178
+ url: 'breakout-control-url',
179
+ },
180
+ },
181
+ };
182
+
183
+ assert.equal(MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus), true);
184
+ });
185
+
186
+ it('returns true when breakout self device replaces the main locus url', () => {
187
+ const mainLocus = {
188
+ url: 'main-locus-url',
189
+ controls: {},
190
+ };
191
+ const breakoutLocus = {
192
+ controls: {
193
+ breakout: {
194
+ url: 'other-breakout-url',
195
+ },
196
+ },
197
+ self: {
198
+ deviceUrl: 'device-url-1',
199
+ devices: [
200
+ {
201
+ url: 'device-url-1',
202
+ replaces: [{locusUrl: 'main-locus-url'}],
203
+ },
204
+ ],
205
+ },
206
+ };
207
+
208
+ assert.equal(MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus), true);
209
+ });
210
+
211
+ it('returns false when breakout locus is not associated with the main locus', () => {
212
+ const mainLocus = {
213
+ url: 'main-locus-url',
214
+ controls: {
215
+ breakout: {
216
+ url: 'breakout-control-url',
217
+ },
218
+ },
219
+ };
220
+ const breakoutLocus = {
221
+ controls: {
222
+ breakout: {
223
+ url: 'different-breakout-url',
224
+ },
225
+ },
226
+ self: {
227
+ deviceUrl: 'device-url-1',
228
+ devices: [
229
+ {
230
+ url: 'device-url-1',
231
+ replaces: [{locusUrl: 'another-main-locus-url'}],
232
+ },
233
+ ],
234
+ },
235
+ };
236
+
237
+ assert.equal(MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus), false);
238
+ });
131
239
  });
132
240
 
133
241
  describe('#joinedOnThisDevice', () => {
@@ -1,19 +1,28 @@
1
1
  import 'jsdom-global/register';
2
2
  import SendSlotManager from '@webex/plugin-meetings/src/multistream/sendSlotManager';
3
- import { LocalStream, MediaType, MultistreamRoapMediaConnection } from "@webex/internal-media-core";
4
- import {expect} from '@webex/test-helper-chai';
3
+ import { LocalStream, MediaType, MultistreamRoapMediaConnection, MediaCodecMimeType } from "@webex/internal-media-core";
4
+ import {assert, expect} from '@webex/test-helper-chai';
5
5
  import sinon from 'sinon';
6
+ import Metrics from '@webex/plugin-meetings/src/metrics';
7
+ import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants';
6
8
 
7
9
  describe('SendSlotsManager', () => {
8
10
  let sendSlotsManager: SendSlotManager;
9
11
  const LoggerProxy = {
10
12
  logger: {
11
13
  info: sinon.stub(),
14
+ warn: sinon.stub(),
15
+ error: sinon.stub(),
12
16
  },
13
17
  };
14
18
 
15
19
  beforeEach(() => {
16
20
  sendSlotsManager = new SendSlotManager(LoggerProxy);
21
+ sinon.stub(Metrics, 'sendBehavioralMetric');
22
+ });
23
+
24
+ afterEach(() => {
25
+ sinon.restore();
17
26
  });
18
27
 
19
28
  describe('createSlot', () => {
@@ -29,13 +38,13 @@ describe('SendSlotsManager', () => {
29
38
  it('should create a slot for the given mediaType', () => {
30
39
  sendSlotsManager.createSlot(mediaConnection, mediaType);
31
40
 
32
- expect(mediaConnection.createSendSlot.calledWith(mediaType, true));
41
+ assert.calledWith(mediaConnection.createSendSlot, mediaType, true);
33
42
  });
34
43
 
35
44
  it('should create a slot for the given mediaType & active state', () => {
36
45
  sendSlotsManager.createSlot(mediaConnection, mediaType, false);
37
46
 
38
- expect(mediaConnection.createSendSlot.calledWith(mediaType, false));
47
+ assert.calledWith(mediaConnection.createSendSlot, mediaType, false);
39
48
  });
40
49
 
41
50
  it('should throw an error if a slot for the given mediaType already exists', () => {
@@ -86,14 +95,12 @@ describe('SendSlotsManager', () => {
86
95
 
87
96
  await sendSlotsManager.publishStream(mediaType, stream);
88
97
 
89
- expect(slot.publishStream.calledWith(stream));
98
+ assert.calledWith(slot.publishStream, stream);
90
99
  });
91
100
 
92
- it('should throw an error if a slot for the given mediaType does not exist', (done) => {
93
- sendSlotsManager.publishStream(mediaType, stream).catch((error) => {
94
- expect(error.message).to.equal(`Slot for ${mediaType} does not exist`);
95
- done();
96
- });
101
+ it('should throw an error if a slot for the given mediaType does not exist', async () => {
102
+ await expect(sendSlotsManager.publishStream(mediaType, stream))
103
+ .to.be.rejectedWith(`Slot for ${mediaType} does not exist`);
97
104
  });
98
105
  });
99
106
 
@@ -116,14 +123,12 @@ describe('SendSlotsManager', () => {
116
123
 
117
124
  await sendSlotsManager.unpublishStream(mediaType);
118
125
 
119
- expect(slot.unpublishStream.called);
126
+ assert.called(slot.unpublishStream);
120
127
  });
121
128
 
122
- it('should throw an error if a slot for the given mediaType does not exist',(done) => {
123
- sendSlotsManager.unpublishStream(mediaType).catch((error) => {
124
- expect(error.message).to.equal(`Slot for ${mediaType} does not exist`);
125
- done();
126
- });
129
+ it('should throw an error if a slot for the given mediaType does not exist', async () => {
130
+ await expect(sendSlotsManager.unpublishStream(mediaType))
131
+ .to.be.rejectedWith(`Slot for ${mediaType} does not exist`);
127
132
  });
128
133
  });
129
134
 
@@ -147,7 +152,7 @@ describe('SendSlotsManager', () => {
147
152
 
148
153
  await sendSlotsManager.setNamedMediaGroups(mediaType, groups);
149
154
 
150
- expect(slot.setNamedMediaGroups.calledWith(groups));
155
+ assert.calledWith(slot.setNamedMediaGroups, groups);
151
156
  });
152
157
 
153
158
  it('should throw an error if the given mediaType is not audio', () => {
@@ -169,16 +174,16 @@ describe('SendSlotsManager', () => {
169
174
  } as MultistreamRoapMediaConnection;
170
175
  });
171
176
 
172
- it('should set the active state of the sendSlot for the given mediaType', async () => {
177
+ it('should set the active state of the sendSlot for the given mediaType', () => {
173
178
  const slot = {
174
- setActive: sinon.stub().resolves(),
179
+ active: false,
175
180
  };
176
181
  mediaConnection.createSendSlot.returns(slot);
177
182
  sendSlotsManager.createSlot(mediaConnection, mediaType);
178
183
 
179
- await sendSlotsManager.setActive(mediaType,true);
184
+ sendSlotsManager.setActive(mediaType, true);
180
185
 
181
- expect(slot.setActive.called);
186
+ expect(slot.active).to.be.true;
182
187
  });
183
188
 
184
189
  it('should throw an error if a slot for the given mediaType does not exist', () => {
@@ -197,7 +202,7 @@ describe('SendSlotsManager', () => {
197
202
  } as MultistreamRoapMediaConnection;
198
203
  });
199
204
 
200
- it('should set the codec parameters of the sendSlot for the given mediaType', async () => {
205
+ it('should delegate to slot.setCodecParameters, log deprecation warning and send deprecation metric', async () => {
201
206
  const slot = {
202
207
  setCodecParameters: sinon.stub().resolves(),
203
208
  };
@@ -206,14 +211,17 @@ describe('SendSlotsManager', () => {
206
211
 
207
212
  await sendSlotsManager.setCodecParameters(mediaType, codecParameters);
208
213
 
209
- expect(slot.setCodecParameters.calledWith(codecParameters));
214
+ assert.calledWith(slot.setCodecParameters, codecParameters);
215
+ assert.called(LoggerProxy.logger.warn);
216
+ assert.calledWith(Metrics.sendBehavioralMetric as sinon.SinonStub,
217
+ BEHAVIORAL_METRICS.DEPRECATED_SET_CODEC_PARAMETERS_USED,
218
+ { mediaType, codecParameters }
219
+ );
210
220
  });
211
221
 
212
- it('should throw an error if a slot for the given mediaType does not exist', (done) => {
213
- sendSlotsManager.setCodecParameters(mediaType, codecParameters).catch((error) => {
214
- expect(error.message).to.equal(`Slot for ${mediaType} does not exist`);
215
- done();
216
- });
222
+ it('should throw an error if a slot for the given mediaType does not exist', async () => {
223
+ await expect(sendSlotsManager.setCodecParameters(mediaType, codecParameters))
224
+ .to.be.rejectedWith(`Slot for ${mediaType} does not exist`);
217
225
  });
218
226
  });
219
227
 
@@ -227,23 +235,114 @@ describe('SendSlotsManager', () => {
227
235
  } as MultistreamRoapMediaConnection;
228
236
  });
229
237
 
230
- it('should delete the codec parameters of the sendSlot for the given mediaType', async () => {
238
+ it('should delegate to slot.deleteCodecParameters, log deprecation warning and send deprecation metric', async () => {
231
239
  const slot = {
232
240
  deleteCodecParameters: sinon.stub().resolves(),
233
241
  };
234
242
  mediaConnection.createSendSlot.returns(slot);
235
243
  sendSlotsManager.createSlot(mediaConnection, mediaType);
236
244
 
237
- await sendSlotsManager.deleteCodecParameters(mediaType,[]);
245
+ await sendSlotsManager.deleteCodecParameters(mediaType, []);
246
+
247
+ assert.calledWith(slot.deleteCodecParameters, []);
248
+ assert.called(LoggerProxy.logger.warn);
249
+ assert.calledWith(Metrics.sendBehavioralMetric as sinon.SinonStub,
250
+ BEHAVIORAL_METRICS.DEPRECATED_DELETE_CODEC_PARAMETERS_USED,
251
+ { mediaType, parameters: [] }
252
+ );
253
+ });
254
+
255
+ it('should throw an error if a slot for the given mediaType does not exist', async () => {
256
+ await expect(sendSlotsManager.deleteCodecParameters(mediaType, []))
257
+ .to.be.rejectedWith(`Slot for ${mediaType} does not exist`);
258
+ });
259
+ });
260
+
261
+ describe('setCustomCodecParameters', () => {
262
+ let mediaConnection;
263
+ const mediaType = MediaType.AudioMain;
264
+ const codecMimeType = MediaCodecMimeType.OPUS;
265
+ const parameters = { maxaveragebitrate: '64000' };
266
+
267
+ beforeEach(() => {
268
+ mediaConnection = {
269
+ createSendSlot: sinon.stub(),
270
+ } as MultistreamRoapMediaConnection;
271
+ });
272
+
273
+ it('should set custom codec parameters on the sendSlot for the given mediaType and codec, log info and send metric', async () => {
274
+ const slot = {
275
+ setCustomCodecParameters: sinon.stub().resolves(),
276
+ };
277
+ mediaConnection.createSendSlot.returns(slot);
278
+ sendSlotsManager.createSlot(mediaConnection, mediaType);
279
+
280
+ await sendSlotsManager.setCustomCodecParameters(mediaType, codecMimeType, parameters);
281
+
282
+ assert.calledWith(slot.setCustomCodecParameters, codecMimeType, parameters);
283
+ assert.called(LoggerProxy.logger.info);
284
+ assert.calledWith(Metrics.sendBehavioralMetric as sinon.SinonStub,
285
+ BEHAVIORAL_METRICS.SET_CUSTOM_CODEC_PARAMETERS_USED,
286
+ { mediaType, codecMimeType, parameters }
287
+ );
288
+ });
289
+
290
+ it('should throw an error if a slot for the given mediaType does not exist', async () => {
291
+ await expect(sendSlotsManager.setCustomCodecParameters(mediaType, codecMimeType, parameters))
292
+ .to.be.rejectedWith(`Slot for ${mediaType} does not exist`);
293
+ });
294
+
295
+ it('should throw and log error when setCustomCodecParameters fails', async () => {
296
+ const error = new Error('codec parameter failure');
297
+ const slot = {
298
+ setCustomCodecParameters: sinon.stub().rejects(error),
299
+ };
300
+ mediaConnection.createSendSlot.returns(slot);
301
+ sendSlotsManager.createSlot(mediaConnection, mediaType);
302
+
303
+ await expect(sendSlotsManager.setCustomCodecParameters(mediaType, codecMimeType, parameters))
304
+ .to.be.rejectedWith('codec parameter failure');
305
+
306
+ assert.called(LoggerProxy.logger.error);
307
+ assert.calledWith(Metrics.sendBehavioralMetric as sinon.SinonStub,
308
+ BEHAVIORAL_METRICS.SET_CUSTOM_CODEC_PARAMETERS_USED,
309
+ { mediaType, codecMimeType, parameters }
310
+ );
311
+ });
312
+ });
313
+
314
+ describe('markCustomCodecParametersForDeletion', () => {
315
+ let mediaConnection;
316
+ const mediaType = MediaType.AudioMain;
317
+ const codecMimeType = MediaCodecMimeType.OPUS;
318
+ const parameters = ['maxaveragebitrate', 'maxplaybackrate'];
319
+
320
+ beforeEach(() => {
321
+ mediaConnection = {
322
+ createSendSlot: sinon.stub(),
323
+ } as MultistreamRoapMediaConnection;
324
+ });
325
+
326
+ it('should mark custom codec parameters for deletion on the sendSlot for the given mediaType and codec, log info and send metric', async () => {
327
+ const slot = {
328
+ markCustomCodecParametersForDeletion: sinon.stub().resolves(),
329
+ };
330
+ mediaConnection.createSendSlot.returns(slot);
331
+ sendSlotsManager.createSlot(mediaConnection, mediaType);
332
+
333
+ await sendSlotsManager.markCustomCodecParametersForDeletion(mediaType, codecMimeType, parameters);
238
334
 
239
- expect(slot.deleteCodecParameters.called);
335
+ assert.calledWith(slot.markCustomCodecParametersForDeletion, codecMimeType, parameters);
336
+ assert.called(LoggerProxy.logger.info);
337
+ assert.calledWith(Metrics.sendBehavioralMetric as sinon.SinonStub,
338
+ BEHAVIORAL_METRICS.MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED,
339
+ { mediaType, codecMimeType, parameters }
340
+ );
240
341
  });
241
342
 
242
- it('should throw an error if a slot for the given mediaType does not exist', (done) => {
243
- sendSlotsManager.deleteCodecParameters(mediaType,[]).catch((error) => {
244
- expect(error.message).to.equal(`Slot for ${mediaType} does not exist`);
245
- done();
246
- });
343
+ it('should throw an error if a slot for the given mediaType does not exist', async () => {
344
+ await expect(sendSlotsManager.markCustomCodecParametersForDeletion(mediaType, codecMimeType, parameters))
345
+ .to.be.rejectedWith(`Slot for ${mediaType} does not exist`);
247
346
  });
248
347
  });
249
348
 
@@ -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();