@webex/plugin-meetings 3.12.0-next.1 → 3.12.0-next.11
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.
- package/dist/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +56 -31
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +38 -14
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meeting/index.js +427 -323
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +1 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/metrics/constants.js +5 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +116 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/locus-info/index.d.ts +8 -3
- package/dist/types/meeting/index.d.ts +24 -1
- package/dist/types/metrics/constants.d.ts +4 -0
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/webinar/index.js +325 -220
- package/dist/webinar/index.js.map +1 -1
- package/package.json +15 -15
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +60 -36
- package/src/hashTree/utils.ts +17 -0
- package/src/locus-info/index.ts +48 -24
- package/src/meeting/index.ts +165 -57
- package/src/meeting/util.ts +1 -0
- package/src/metrics/constants.ts +5 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- package/src/webinar/index.ts +120 -18
- package/test/unit/spec/hashTree/hashTreeParser.ts +295 -30
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/locus-info/index.js +47 -22
- package/test/unit/spec/meeting/index.js +179 -48
- package/test/unit/spec/meeting/utils.js +4 -0
- package/test/unit/spec/meetings/index.js +3 -3
- package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
- package/test/unit/spec/webinar/index.ts +193 -8
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', (
|
|
93
|
-
sendSlotsManager.publishStream(mediaType, stream)
|
|
94
|
-
|
|
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
|
-
|
|
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',(
|
|
123
|
-
sendSlotsManager.unpublishStream(mediaType)
|
|
124
|
-
|
|
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
|
-
|
|
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',
|
|
177
|
+
it('should set the active state of the sendSlot for the given mediaType', () => {
|
|
173
178
|
const slot = {
|
|
174
|
-
|
|
179
|
+
active: false,
|
|
175
180
|
};
|
|
176
181
|
mediaConnection.createSendSlot.returns(slot);
|
|
177
182
|
sendSlotsManager.createSlot(mediaConnection, mediaType);
|
|
178
183
|
|
|
179
|
-
|
|
184
|
+
sendSlotsManager.setActive(mediaType, true);
|
|
180
185
|
|
|
181
|
-
expect(slot.
|
|
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
|
|
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
|
-
|
|
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', (
|
|
213
|
-
sendSlotsManager.setCodecParameters(mediaType, codecParameters)
|
|
214
|
-
|
|
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
|
|
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
|
-
|
|
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', (
|
|
243
|
-
sendSlotsManager.
|
|
244
|
-
|
|
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(),
|
|
@@ -210,6 +211,26 @@ describe('plugin-meetings', () => {
|
|
|
210
211
|
meeting.processRelayEvent
|
|
211
212
|
);
|
|
212
213
|
});
|
|
214
|
+
|
|
215
|
+
it('removes a pending online listener if one exists', async () => {
|
|
216
|
+
const listener = sinon.stub();
|
|
217
|
+
webinar._pendingOnlineListener = listener;
|
|
218
|
+
|
|
219
|
+
await webinar.cleanupPSDataChannel();
|
|
220
|
+
|
|
221
|
+
assert.calledWith(webex.internal.llm.off, 'online', listener);
|
|
222
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('skips online listener removal when none is pending', async () => {
|
|
226
|
+
webinar._pendingOnlineListener = null;
|
|
227
|
+
|
|
228
|
+
await webinar.cleanupPSDataChannel();
|
|
229
|
+
|
|
230
|
+
// 'off' should only be called for the relay event, not for 'online'
|
|
231
|
+
const onlineOffCalls = webex.internal.llm.off.args.filter(([event]) => event === 'online');
|
|
232
|
+
assert.equal(onlineOffCalls.length, 0);
|
|
233
|
+
});
|
|
213
234
|
});
|
|
214
235
|
|
|
215
236
|
describe('#updatePSDataChannel', () => {
|
|
@@ -224,12 +245,22 @@ describe('plugin-meetings', () => {
|
|
|
224
245
|
locusInfo: {
|
|
225
246
|
url: 'locus-url',
|
|
226
247
|
info: {practiceSessionDatachannelUrl: 'dc-url'},
|
|
227
|
-
self: {practiceSessionDatachannelToken: 'ps-token'},
|
|
228
248
|
},
|
|
229
249
|
};
|
|
230
250
|
|
|
231
251
|
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
232
252
|
|
|
253
|
+
// Default session is connected by default; practice session is not
|
|
254
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
255
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Token is pre-saved into LLM by saveDataChannelToken
|
|
259
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
|
|
260
|
+
if (tokenType === DataChannelTokenType.PracticeSession) return 'ps-token';
|
|
261
|
+
return undefined;
|
|
262
|
+
});
|
|
263
|
+
|
|
233
264
|
// Ensure connect path is eligible
|
|
234
265
|
webinar.selfIsPanelist = true;
|
|
235
266
|
webinar.practiceSessionEnabled = true;
|
|
@@ -237,6 +268,65 @@ describe('plugin-meetings', () => {
|
|
|
237
268
|
webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
|
|
238
269
|
});
|
|
239
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
|
+
|
|
240
330
|
it('no-ops when practice session join eligibility is false', async () => {
|
|
241
331
|
webinar.practiceSessionEnabled = false;
|
|
242
332
|
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
@@ -284,11 +374,6 @@ describe('plugin-meetings', () => {
|
|
|
284
374
|
it('connects when eligible', async () => {
|
|
285
375
|
const result = await webinar.updatePSDataChannel();
|
|
286
376
|
|
|
287
|
-
assert.calledOnceWithExactly(
|
|
288
|
-
webex.internal.llm.setDatachannelToken,
|
|
289
|
-
'ps-token',
|
|
290
|
-
DataChannelTokenType.PracticeSession
|
|
291
|
-
);
|
|
292
377
|
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
293
378
|
assert.calledWith(
|
|
294
379
|
webex.internal.llm.registerAndConnect,
|
|
@@ -301,8 +386,11 @@ describe('plugin-meetings', () => {
|
|
|
301
386
|
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
302
387
|
});
|
|
303
388
|
|
|
304
|
-
it('uses
|
|
305
|
-
webex.internal.llm.getDatachannelToken.
|
|
389
|
+
it('uses token from LLM', async () => {
|
|
390
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
|
|
391
|
+
if (tokenType === DataChannelTokenType.PracticeSession) return 'cached-token';
|
|
392
|
+
return undefined;
|
|
393
|
+
});
|
|
306
394
|
|
|
307
395
|
await webinar.updatePSDataChannel();
|
|
308
396
|
|
|
@@ -360,6 +448,101 @@ describe('plugin-meetings', () => {
|
|
|
360
448
|
|
|
361
449
|
assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
|
|
362
450
|
});
|
|
451
|
+
|
|
452
|
+
it('defers connect when default session is not yet connected', async () => {
|
|
453
|
+
// Default session is not connected initially
|
|
454
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
455
|
+
|
|
456
|
+
const result = await webinar.updatePSDataChannel();
|
|
457
|
+
|
|
458
|
+
// Should return undefined immediately (deferred)
|
|
459
|
+
assert.isUndefined(result);
|
|
460
|
+
// Should register an 'online' listener but NOT call registerAndConnect yet
|
|
461
|
+
assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
|
|
462
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
463
|
+
// Should store the pending listener
|
|
464
|
+
assert.isNotNull(webinar._pendingOnlineListener);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('does not register duplicate online listeners on repeated calls', async () => {
|
|
468
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
469
|
+
|
|
470
|
+
await webinar.updatePSDataChannel();
|
|
471
|
+
await webinar.updatePSDataChannel();
|
|
472
|
+
await webinar.updatePSDataChannel();
|
|
473
|
+
|
|
474
|
+
// Only one 'online' listener should have been registered
|
|
475
|
+
const onlineCalls = webex.internal.llm.on.args.filter(([event]) => event === 'online');
|
|
476
|
+
assert.equal(onlineCalls.length, 1, 'should register exactly one online listener');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('re-invokes updatePSDataChannel when default session comes online', async () => {
|
|
480
|
+
// Default session is not connected initially
|
|
481
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
482
|
+
|
|
483
|
+
const updatePSDataChannelSpy = sinon.spy(webinar, 'updatePSDataChannel');
|
|
484
|
+
|
|
485
|
+
// First call defers
|
|
486
|
+
await webinar.updatePSDataChannel();
|
|
487
|
+
|
|
488
|
+
// Capture the 'online' listener
|
|
489
|
+
const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
|
|
490
|
+
assert.isDefined(onlineCall, 'should have registered an online listener');
|
|
491
|
+
|
|
492
|
+
// Now simulate default session coming online
|
|
493
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
494
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Fire the captured listener
|
|
498
|
+
onlineCall[1]();
|
|
499
|
+
|
|
500
|
+
// The listener should have cleared itself, removed itself, and re-called updatePSDataChannel
|
|
501
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
502
|
+
assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
|
|
503
|
+
assert.equal(updatePSDataChannelSpy.callCount, 2);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('does not reconnect with stale data if demoted before default session comes online', async () => {
|
|
507
|
+
// Default session is not connected initially
|
|
508
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
509
|
+
|
|
510
|
+
await webinar.updatePSDataChannel();
|
|
511
|
+
|
|
512
|
+
// Capture the 'online' listener
|
|
513
|
+
const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
|
|
514
|
+
assert.isDefined(onlineCall);
|
|
515
|
+
|
|
516
|
+
// Simulate demotion while waiting
|
|
517
|
+
webinar.selfIsPanelist = false;
|
|
518
|
+
|
|
519
|
+
// Now default session comes online
|
|
520
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
521
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Fire the listener — re-invokes updatePSDataChannel which will see isPracticeSession = false
|
|
525
|
+
onlineCall[1]();
|
|
526
|
+
|
|
527
|
+
// Should NOT have called registerAndConnect since the user is no longer eligible
|
|
528
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('proceeds immediately when default session is already connected', async () => {
|
|
532
|
+
// Default session already connected, practice session not
|
|
533
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
534
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const result = await webinar.updatePSDataChannel();
|
|
538
|
+
|
|
539
|
+
// The 'online' listener is registered then immediately removed since default session is already connected
|
|
540
|
+
assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
|
|
541
|
+
assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
|
|
542
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
543
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
544
|
+
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
545
|
+
});
|
|
363
546
|
});
|
|
364
547
|
|
|
365
548
|
describe('#updateStatusByRole', () => {
|
|
@@ -369,6 +552,7 @@ describe('plugin-meetings', () => {
|
|
|
369
552
|
webinar.webex.meetings = {
|
|
370
553
|
getMeetingByType: sinon.stub().returns({
|
|
371
554
|
id: 'meeting-id',
|
|
555
|
+
isJoined: sinon.stub().returns(false),
|
|
372
556
|
updateLLMConnection: sinon.stub(),
|
|
373
557
|
shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
|
|
374
558
|
locusInfo: {
|
|
@@ -423,6 +607,7 @@ describe('plugin-meetings', () => {
|
|
|
423
607
|
webinar.webex.meetings = {
|
|
424
608
|
getMeetingByType: sinon.stub().returns({
|
|
425
609
|
id: 'meeting-id',
|
|
610
|
+
isJoined: sinon.stub().returns(false),
|
|
426
611
|
updateLLMConnection: sinon.stub(),
|
|
427
612
|
shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
|
|
428
613
|
locusInfo: {
|