@webex/plugin-meetings 3.12.0-next.7 → 3.12.0-next.70
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/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +8 -3
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +30 -7
- package/dist/constants.js.map +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +38 -24
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +13 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +880 -382
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +42 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/dataChannelAuthToken.js +75 -15
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/interpretation.types.js +7 -0
- package/dist/interpretation/interpretation.types.js.map +1 -0
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +298 -87
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +19 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +1046 -689
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +10 -1
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.js +5 -2
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +20 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +2 -2
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +231 -78
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +79 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/constants.d.ts +9 -1
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +2 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
- package/dist/types/hashTree/utils.d.ts +18 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/interpretation/interpretation.types.d.ts +10 -0
- package/dist/types/locus-info/index.d.ts +50 -6
- package/dist/types/locus-info/types.d.ts +21 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +78 -5
- package/dist/types/meeting/request.d.ts +1 -0
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +30 -2
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +3 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +305 -159
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +3 -1
- package/src/breakouts/index.ts +31 -0
- package/src/config.ts +2 -0
- package/src/constants.ts +13 -2
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +16 -0
- package/src/hashTree/hashTreeParser.ts +580 -196
- package/src/hashTree/utils.ts +36 -0
- package/src/index.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +88 -12
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +27 -9
- package/src/interpretation/interpretation.types.ts +11 -0
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +293 -97
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +386 -48
- package/src/meeting/muteState.ts +10 -1
- package/src/meeting/request.ts +11 -0
- package/src/meeting/util.ts +21 -2
- package/src/meeting-info/meeting-info-v2.ts +4 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +3 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/src/multistream/receiveSlot.ts +18 -0
- package/src/reactions/reactions.type.ts +3 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +214 -36
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +9 -3
- package/test/unit/spec/breakouts/index.ts +49 -0
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
- package/test/unit/spec/hashTree/utils.ts +125 -1
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +487 -81
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +1240 -37
- package/test/unit/spec/meeting/muteState.js +81 -0
- package/test/unit/spec/meeting/request.js +12 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
- package/test/unit/spec/meetings/index.js +360 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +329 -28
|
@@ -35,6 +35,7 @@ describe('plugin-meetings', () => {
|
|
|
35
35
|
beforeEach(() => {
|
|
36
36
|
request = {
|
|
37
37
|
request: sinon.stub().returns(Promise.resolve()),
|
|
38
|
+
locusDeltaRequest: sinon.stub().returns(Promise.resolve()),
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
controller = new RecordingController(request);
|
|
@@ -69,13 +70,13 @@ describe('plugin-meetings', () => {
|
|
|
69
70
|
|
|
70
71
|
const result = controller.startRecording();
|
|
71
72
|
|
|
72
|
-
assert.calledWith(request.
|
|
73
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
73
74
|
uri: `${locusUrl}/controls`,
|
|
74
75
|
body: {record: {recording: true, paused: false}},
|
|
75
76
|
method: HTTP_VERBS.PATCH,
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
assert.deepEqual(result, request.
|
|
79
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
@@ -103,13 +104,13 @@ describe('plugin-meetings', () => {
|
|
|
103
104
|
|
|
104
105
|
const result = controller.stopRecording();
|
|
105
106
|
|
|
106
|
-
assert.calledWith(request.
|
|
107
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
107
108
|
uri: `${locusUrl}/controls`,
|
|
108
109
|
body: {record: {recording: false, paused: false}},
|
|
109
110
|
method: HTTP_VERBS.PATCH,
|
|
110
111
|
});
|
|
111
112
|
|
|
112
|
-
assert.deepEqual(result, request.
|
|
113
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
113
114
|
});
|
|
114
115
|
});
|
|
115
116
|
|
|
@@ -139,13 +140,13 @@ describe('plugin-meetings', () => {
|
|
|
139
140
|
|
|
140
141
|
const result = controller.pauseRecording();
|
|
141
142
|
|
|
142
|
-
assert.calledWith(request.
|
|
143
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
143
144
|
uri: `${locusUrl}/controls`,
|
|
144
145
|
body: {record: {recording: true, paused: true}},
|
|
145
146
|
method: HTTP_VERBS.PATCH,
|
|
146
147
|
});
|
|
147
148
|
|
|
148
|
-
assert.deepEqual(result, request.
|
|
149
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
149
150
|
});
|
|
150
151
|
});
|
|
151
152
|
|
|
@@ -176,13 +177,13 @@ describe('plugin-meetings', () => {
|
|
|
176
177
|
|
|
177
178
|
const result = controller.resumeRecording();
|
|
178
179
|
|
|
179
|
-
assert.calledWith(request.
|
|
180
|
+
assert.calledWith(request.locusDeltaRequest, {
|
|
180
181
|
uri: `${locusUrl}/controls`,
|
|
181
182
|
body: {record: {recording: true, paused: false}},
|
|
182
183
|
method: HTTP_VERBS.PATCH,
|
|
183
184
|
});
|
|
184
185
|
|
|
185
|
-
assert.deepEqual(result, request.
|
|
186
|
+
assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
|
|
186
187
|
});
|
|
187
188
|
});
|
|
188
189
|
});
|
|
@@ -5,7 +5,9 @@ import MockWebex from '@webex/test-helper-mock-webex';
|
|
|
5
5
|
import uuid from 'uuid';
|
|
6
6
|
import sinon from 'sinon';
|
|
7
7
|
import {DataChannelTokenType} from '@webex/internal-plugin-llm';
|
|
8
|
-
import {LLM_PRACTICE_SESSION, SHARE_STATUS} from '@webex/plugin-meetings/src/constants';
|
|
8
|
+
import {LLM_PRACTICE_SESSION, LOCUS_LLM_EVENT, SHARE_STATUS} from '@webex/plugin-meetings/src/constants';
|
|
9
|
+
|
|
10
|
+
const PRACTICE_SESSION_KEY = LLM_PRACTICE_SESSION || DataChannelTokenType.PracticeSession;
|
|
9
11
|
|
|
10
12
|
describe('plugin-meetings', () => {
|
|
11
13
|
describe('Webinar', () => {
|
|
@@ -33,6 +35,17 @@ describe('plugin-meetings', () => {
|
|
|
33
35
|
webex.internal.llm = {
|
|
34
36
|
getDatachannelToken: sinon.stub().returns(undefined),
|
|
35
37
|
setDatachannelToken: sinon.stub(),
|
|
38
|
+
setRefreshHandler: sinon.stub(),
|
|
39
|
+
getOwnerMeetingId: sinon.stub().returns(undefined),
|
|
40
|
+
resolveSessionOwnership: sinon.stub().callsFake((ownerMeetingId, sessionId) => {
|
|
41
|
+
const currentOwner = webex.internal.llm.getOwnerMeetingId(sessionId);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
currentOwner,
|
|
45
|
+
isOwner: !currentOwner || !ownerMeetingId || currentOwner === ownerMeetingId,
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
setOwnerMeetingId: sinon.stub(),
|
|
36
49
|
isDataChannelTokenEnabled: sinon.stub().resolves(false),
|
|
37
50
|
isConnected: sinon.stub().returns(false),
|
|
38
51
|
disconnectLLM: sinon.stub().resolves(),
|
|
@@ -176,6 +189,42 @@ describe('plugin-meetings', () => {
|
|
|
176
189
|
});
|
|
177
190
|
});
|
|
178
191
|
|
|
192
|
+
describe('#getValidatedWebinarMeeting', () => {
|
|
193
|
+
it('returns the meeting when its locusUrl matches the webinar locusUrl', () => {
|
|
194
|
+
const meeting = {locusUrl: 'locusUrl'};
|
|
195
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
196
|
+
webinar.locusUrl = 'locusUrl';
|
|
197
|
+
|
|
198
|
+
assert.equal(webinar.getValidatedWebinarMeeting(), meeting);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('returns undefined and warns when the resolved meeting locusUrl does not match', () => {
|
|
202
|
+
const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
|
|
203
|
+
const meeting = {locusUrl: 'other-locus-url'};
|
|
204
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
205
|
+
webinar.locusUrl = 'locusUrl';
|
|
206
|
+
|
|
207
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
208
|
+
assert.calledOnce(warnStub);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('returns undefined when no meeting is resolved', () => {
|
|
212
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(undefined);
|
|
213
|
+
|
|
214
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('returns undefined and warns when webinar locusUrl is not yet initialized', () => {
|
|
218
|
+
const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
|
|
219
|
+
const meeting = {locusUrl: 'some-url'};
|
|
220
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
221
|
+
webinar.locusUrl = undefined;
|
|
222
|
+
|
|
223
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
224
|
+
assert.calledOnce(warnStub);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
179
228
|
describe('#cleanUp', () => {
|
|
180
229
|
it('delegates to cleanupPSDataChannel', () => {
|
|
181
230
|
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
@@ -187,29 +236,119 @@ describe('plugin-meetings', () => {
|
|
|
187
236
|
});
|
|
188
237
|
|
|
189
238
|
describe('#cleanupPSDataChannel', () => {
|
|
190
|
-
let
|
|
239
|
+
let relayListener;
|
|
191
240
|
|
|
192
241
|
beforeEach(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
242
|
+
webinar.meetingId = 'meeting-id';
|
|
243
|
+
relayListener = sinon.stub();
|
|
244
|
+
webinar.llmListeners = {relay: relayListener, locusLLM: null};
|
|
245
|
+
});
|
|
196
246
|
|
|
197
|
-
|
|
247
|
+
it('disconnects the practice session channel and removes the tracked relay listener', async () => {
|
|
248
|
+
await webinar.cleanupPSDataChannel();
|
|
249
|
+
|
|
250
|
+
assert.calledOnceWithExactly(
|
|
251
|
+
webex.internal.llm.disconnectLLM,
|
|
252
|
+
{code: 3050, reason: 'done (permanent)'},
|
|
253
|
+
PRACTICE_SESSION_KEY,
|
|
254
|
+
webinar.meetingId
|
|
255
|
+
);
|
|
256
|
+
assert.calledWithExactly(
|
|
257
|
+
webex.internal.llm.off,
|
|
258
|
+
`event:relay.event:${PRACTICE_SESSION_KEY}`,
|
|
259
|
+
relayListener
|
|
260
|
+
);
|
|
261
|
+
assert.isNull(webinar.llmListeners.relay);
|
|
198
262
|
});
|
|
199
263
|
|
|
200
|
-
it('
|
|
264
|
+
it('skips disconnect when practice-session owner is another meeting', async () => {
|
|
265
|
+
webex.internal.llm.getOwnerMeetingId.returns('other-meeting-id');
|
|
266
|
+
webex.internal.llm.disconnectLLM.resolves(false);
|
|
267
|
+
|
|
201
268
|
await webinar.cleanupPSDataChannel();
|
|
202
269
|
|
|
203
270
|
assert.calledOnceWithExactly(
|
|
204
271
|
webex.internal.llm.disconnectLLM,
|
|
205
272
|
{code: 3050, reason: 'done (permanent)'},
|
|
206
|
-
|
|
273
|
+
PRACTICE_SESSION_KEY,
|
|
274
|
+
webinar.meetingId
|
|
207
275
|
);
|
|
276
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
208
277
|
assert.calledOnceWithExactly(
|
|
209
278
|
webex.internal.llm.off,
|
|
210
|
-
`event:relay.event:${
|
|
211
|
-
|
|
279
|
+
`event:relay.event:${PRACTICE_SESSION_KEY}`,
|
|
280
|
+
relayListener
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('skips relay listener removal when no listener has been tracked', async () => {
|
|
285
|
+
webinar.llmListeners.relay = null;
|
|
286
|
+
|
|
287
|
+
await webinar.cleanupPSDataChannel();
|
|
288
|
+
|
|
289
|
+
const relayOffCalls = webex.internal.llm.off.args.filter(
|
|
290
|
+
([event]) => event === `event:relay.event:${PRACTICE_SESSION_KEY}`
|
|
212
291
|
);
|
|
292
|
+
assert.equal(relayOffCalls.length, 0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('removes tracked relay listener even when disconnect throws', async () => {
|
|
296
|
+
const disconnectError = new Error('disconnect failed');
|
|
297
|
+
webex.internal.llm.disconnectLLM.rejects(disconnectError);
|
|
298
|
+
|
|
299
|
+
let caughtError;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await webinar.cleanupPSDataChannel();
|
|
303
|
+
} catch (error) {
|
|
304
|
+
caughtError = error;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
assert.equal(caughtError, disconnectError);
|
|
308
|
+
assert.calledOnceWithExactly(
|
|
309
|
+
webex.internal.llm.setOwnerMeetingId,
|
|
310
|
+
undefined,
|
|
311
|
+
PRACTICE_SESSION_KEY
|
|
312
|
+
);
|
|
313
|
+
assert.calledOnceWithExactly(
|
|
314
|
+
webex.internal.llm.off,
|
|
315
|
+
`event:relay.event:${PRACTICE_SESSION_KEY}`,
|
|
316
|
+
relayListener
|
|
317
|
+
);
|
|
318
|
+
assert.notOk(webinar._practiceSessionRelayListener);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('disconnects and removes the tracked locusLLM listener', async () => {
|
|
322
|
+
const locusLLMListener = sinon.stub();
|
|
323
|
+
webinar.llmListeners.locusLLM = locusLLMListener;
|
|
324
|
+
|
|
325
|
+
await webinar.cleanupPSDataChannel();
|
|
326
|
+
|
|
327
|
+
assert.calledWithExactly(
|
|
328
|
+
webex.internal.llm.off,
|
|
329
|
+
`${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`,
|
|
330
|
+
locusLLMListener
|
|
331
|
+
);
|
|
332
|
+
assert.isNull(webinar.llmListeners.locusLLM);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('skips locusLLM listener removal when no listener has been tracked', async () => {
|
|
336
|
+
webinar.llmListeners.locusLLM = null;
|
|
337
|
+
|
|
338
|
+
await webinar.cleanupPSDataChannel();
|
|
339
|
+
|
|
340
|
+
const locusLLMOffCalls = webex.internal.llm.off.args.filter(
|
|
341
|
+
([event]) => event === `${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`
|
|
342
|
+
);
|
|
343
|
+
assert.equal(locusLLMOffCalls.length, 0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('does not consult the meeting collection during cleanup', async () => {
|
|
347
|
+
webex.meetings.getMeetingByType = sinon.stub();
|
|
348
|
+
|
|
349
|
+
await webinar.cleanupPSDataChannel();
|
|
350
|
+
|
|
351
|
+
assert.notCalled(webex.meetings.getMeetingByType);
|
|
213
352
|
});
|
|
214
353
|
|
|
215
354
|
it('removes a pending online listener if one exists', async () => {
|
|
@@ -236,12 +375,18 @@ describe('plugin-meetings', () => {
|
|
|
236
375
|
describe('#updatePSDataChannel', () => {
|
|
237
376
|
let meeting;
|
|
238
377
|
let processRelayEvent;
|
|
378
|
+
let processLocusLLMEvent;
|
|
239
379
|
|
|
240
380
|
beforeEach(() => {
|
|
381
|
+
webinar.meetingId = 'meeting-id';
|
|
241
382
|
processRelayEvent = sinon.stub();
|
|
383
|
+
processLocusLLMEvent = sinon.stub();
|
|
242
384
|
meeting = {
|
|
385
|
+
id: 'meeting-id',
|
|
386
|
+
locusUrl: 'locusUrl',
|
|
243
387
|
isJoined: sinon.stub().returns(true),
|
|
244
388
|
processRelayEvent,
|
|
389
|
+
processLocusLLMEvent,
|
|
245
390
|
locusInfo: {
|
|
246
391
|
url: 'locus-url',
|
|
247
392
|
info: {practiceSessionDatachannelUrl: 'dc-url'},
|
|
@@ -252,7 +397,7 @@ describe('plugin-meetings', () => {
|
|
|
252
397
|
|
|
253
398
|
// Default session is connected by default; practice session is not
|
|
254
399
|
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
255
|
-
return sessionId !==
|
|
400
|
+
return sessionId !== PRACTICE_SESSION_KEY;
|
|
256
401
|
});
|
|
257
402
|
|
|
258
403
|
// Token is pre-saved into LLM by saveDataChannelToken
|
|
@@ -288,14 +433,15 @@ describe('plugin-meetings', () => {
|
|
|
288
433
|
assert.calledWithExactly(
|
|
289
434
|
webex.internal.llm.setDatachannelToken,
|
|
290
435
|
'ps-token-from-refresh',
|
|
291
|
-
DataChannelTokenType.PracticeSession
|
|
436
|
+
DataChannelTokenType.PracticeSession,
|
|
437
|
+
'meeting-id'
|
|
292
438
|
);
|
|
293
439
|
assert.calledWith(
|
|
294
440
|
webex.internal.llm.registerAndConnect,
|
|
295
441
|
'locus-url',
|
|
296
442
|
'dc-url',
|
|
297
443
|
'ps-token-from-refresh',
|
|
298
|
-
|
|
444
|
+
PRACTICE_SESSION_KEY
|
|
299
445
|
);
|
|
300
446
|
});
|
|
301
447
|
|
|
@@ -355,6 +501,8 @@ describe('plugin-meetings', () => {
|
|
|
355
501
|
const result = await webinar.updatePSDataChannel();
|
|
356
502
|
|
|
357
503
|
assert.isUndefined(result);
|
|
504
|
+
assert.notCalled(webex.internal.llm.setRefreshHandler);
|
|
505
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
358
506
|
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
359
507
|
});
|
|
360
508
|
|
|
@@ -375,12 +523,17 @@ describe('plugin-meetings', () => {
|
|
|
375
523
|
const result = await webinar.updatePSDataChannel();
|
|
376
524
|
|
|
377
525
|
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
526
|
+
assert.calledWithExactly(
|
|
527
|
+
webex.internal.llm.setOwnerMeetingId,
|
|
528
|
+
'meeting-id',
|
|
529
|
+
PRACTICE_SESSION_KEY
|
|
530
|
+
);
|
|
378
531
|
assert.calledWith(
|
|
379
532
|
webex.internal.llm.registerAndConnect,
|
|
380
533
|
'locus-url',
|
|
381
534
|
'dc-url',
|
|
382
535
|
'ps-token',
|
|
383
|
-
|
|
536
|
+
PRACTICE_SESSION_KEY
|
|
384
537
|
);
|
|
385
538
|
assert.calledOnceWithExactly(webex.internal.voicea.announce);
|
|
386
539
|
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
@@ -396,7 +549,8 @@ describe('plugin-meetings', () => {
|
|
|
396
549
|
|
|
397
550
|
assert.calledWithExactly(
|
|
398
551
|
webex.internal.llm.getDatachannelToken,
|
|
399
|
-
DataChannelTokenType.PracticeSession
|
|
552
|
+
DataChannelTokenType.PracticeSession,
|
|
553
|
+
webinar.meetingId
|
|
400
554
|
);
|
|
401
555
|
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
402
556
|
assert.calledWith(
|
|
@@ -404,7 +558,7 @@ describe('plugin-meetings', () => {
|
|
|
404
558
|
'locus-url',
|
|
405
559
|
'dc-url',
|
|
406
560
|
'cached-token',
|
|
407
|
-
|
|
561
|
+
PRACTICE_SESSION_KEY
|
|
408
562
|
);
|
|
409
563
|
});
|
|
410
564
|
|
|
@@ -418,19 +572,55 @@ describe('plugin-meetings', () => {
|
|
|
418
572
|
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
419
573
|
});
|
|
420
574
|
|
|
421
|
-
it('
|
|
575
|
+
it('tracks and binds the relay listener after successful connect', async () => {
|
|
422
576
|
await webinar.updatePSDataChannel();
|
|
423
577
|
|
|
578
|
+
// Stores the exact listener reference for deterministic cleanup
|
|
579
|
+
assert.equal(webinar.llmListeners.relay, processRelayEvent);
|
|
424
580
|
assert.calledWith(
|
|
425
|
-
webex.internal.llm.
|
|
426
|
-
`event:relay.event:${
|
|
581
|
+
webex.internal.llm.on,
|
|
582
|
+
`event:relay.event:${PRACTICE_SESSION_KEY}`,
|
|
427
583
|
processRelayEvent
|
|
428
584
|
);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('removes a previously tracked relay listener before re-binding on reconnect', async () => {
|
|
588
|
+
const previousListener = sinon.stub();
|
|
589
|
+
webinar.llmListeners = {relay: previousListener, locusLLM: null};
|
|
590
|
+
|
|
591
|
+
await webinar.updatePSDataChannel();
|
|
592
|
+
|
|
593
|
+
assert.calledWith(
|
|
594
|
+
webex.internal.llm.off,
|
|
595
|
+
`event:relay.event:${PRACTICE_SESSION_KEY}`,
|
|
596
|
+
previousListener
|
|
597
|
+
);
|
|
598
|
+
assert.equal(webinar.llmListeners.relay, processRelayEvent);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('tracks and binds the locusLLM listener after successful connect', async () => {
|
|
602
|
+
await webinar.updatePSDataChannel();
|
|
603
|
+
|
|
604
|
+
assert.equal(webinar.llmListeners.locusLLM, processLocusLLMEvent);
|
|
429
605
|
assert.calledWith(
|
|
430
606
|
webex.internal.llm.on,
|
|
431
|
-
|
|
432
|
-
|
|
607
|
+
`${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`,
|
|
608
|
+
processLocusLLMEvent
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('removes a previously tracked locusLLM listener before re-binding on reconnect', async () => {
|
|
613
|
+
const previousListener = sinon.stub();
|
|
614
|
+
webinar.llmListeners = {relay: null, locusLLM: previousListener};
|
|
615
|
+
|
|
616
|
+
await webinar.updatePSDataChannel();
|
|
617
|
+
|
|
618
|
+
assert.calledWith(
|
|
619
|
+
webex.internal.llm.off,
|
|
620
|
+
`${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`,
|
|
621
|
+
previousListener
|
|
433
622
|
);
|
|
623
|
+
assert.equal(webinar.llmListeners.locusLLM, processLocusLLMEvent);
|
|
434
624
|
});
|
|
435
625
|
|
|
436
626
|
it('subscribes to transcription when caption intent is enabled', async () => {
|
|
@@ -460,6 +650,8 @@ describe('plugin-meetings', () => {
|
|
|
460
650
|
// Should register an 'online' listener but NOT call registerAndConnect yet
|
|
461
651
|
assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
|
|
462
652
|
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
653
|
+
assert.notCalled(webex.internal.llm.setRefreshHandler);
|
|
654
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
463
655
|
// Should store the pending listener
|
|
464
656
|
assert.isNotNull(webinar._pendingOnlineListener);
|
|
465
657
|
});
|
|
@@ -491,7 +683,7 @@ describe('plugin-meetings', () => {
|
|
|
491
683
|
|
|
492
684
|
// Now simulate default session coming online
|
|
493
685
|
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
494
|
-
return sessionId !==
|
|
686
|
+
return sessionId !== PRACTICE_SESSION_KEY;
|
|
495
687
|
});
|
|
496
688
|
|
|
497
689
|
// Fire the captured listener
|
|
@@ -518,7 +710,7 @@ describe('plugin-meetings', () => {
|
|
|
518
710
|
|
|
519
711
|
// Now default session comes online
|
|
520
712
|
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
521
|
-
return sessionId !==
|
|
713
|
+
return sessionId !== PRACTICE_SESSION_KEY;
|
|
522
714
|
});
|
|
523
715
|
|
|
524
716
|
// Fire the listener — re-invokes updatePSDataChannel which will see isPracticeSession = false
|
|
@@ -531,7 +723,7 @@ describe('plugin-meetings', () => {
|
|
|
531
723
|
it('proceeds immediately when default session is already connected', async () => {
|
|
532
724
|
// Default session already connected, practice session not
|
|
533
725
|
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
534
|
-
return sessionId !==
|
|
726
|
+
return sessionId !== PRACTICE_SESSION_KEY;
|
|
535
727
|
});
|
|
536
728
|
|
|
537
729
|
const result = await webinar.updatePSDataChannel();
|
|
@@ -543,6 +735,115 @@ describe('plugin-meetings', () => {
|
|
|
543
735
|
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
544
736
|
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
545
737
|
});
|
|
738
|
+
|
|
739
|
+
it('does not override practice refresh handler or reconnect when owned by another meeting', async () => {
|
|
740
|
+
webex.internal.llm.getOwnerMeetingId.returns('other-meeting-id');
|
|
741
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
742
|
+
return sessionId !== undefined;
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
const result = await webinar.updatePSDataChannel();
|
|
746
|
+
|
|
747
|
+
assert.isUndefined(result);
|
|
748
|
+
assert.notCalled(webex.internal.llm.setRefreshHandler);
|
|
749
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('does not reconnect when practice session is disconnected but owned by another meeting', async () => {
|
|
753
|
+
webex.internal.llm.getOwnerMeetingId.returns('other-meeting-id');
|
|
754
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
755
|
+
|
|
756
|
+
const result = await webinar.updatePSDataChannel();
|
|
757
|
+
|
|
758
|
+
assert.isUndefined(result);
|
|
759
|
+
assert.notCalled(webex.internal.llm.setRefreshHandler);
|
|
760
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
761
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('does not write owner or connect if ownership changes before pre-connect owner write', async () => {
|
|
765
|
+
let ownerMeetingId = 'meeting-id';
|
|
766
|
+
|
|
767
|
+
webex.internal.llm.getOwnerMeetingId.callsFake(() => ownerMeetingId);
|
|
768
|
+
webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
|
|
769
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().returns(undefined);
|
|
770
|
+
|
|
771
|
+
let resolveRefresh;
|
|
772
|
+
meeting.refreshDataChannelToken = sinon.stub().returns(
|
|
773
|
+
new Promise((resolve) => {
|
|
774
|
+
resolveRefresh = resolve;
|
|
775
|
+
})
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
const updatePromise = webinar.updatePSDataChannel();
|
|
779
|
+
|
|
780
|
+
ownerMeetingId = 'other-meeting-id';
|
|
781
|
+
resolveRefresh({
|
|
782
|
+
body: {
|
|
783
|
+
datachannelToken: 'ps-token-from-refresh',
|
|
784
|
+
dataChannelTokenType: DataChannelTokenType.PracticeSession,
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
const result = await updatePromise;
|
|
789
|
+
|
|
790
|
+
assert.isUndefined(result);
|
|
791
|
+
assert.notCalled(webex.internal.llm.setRefreshHandler);
|
|
792
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
793
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('does not overwrite owner after connect when ownership changed during registerAndConnect', async () => {
|
|
797
|
+
let ownerMeetingId = 'meeting-id';
|
|
798
|
+
|
|
799
|
+
webex.internal.llm.getOwnerMeetingId.callsFake(() => ownerMeetingId);
|
|
800
|
+
webex.internal.llm.registerAndConnect = sinon.stub().callsFake(async () => {
|
|
801
|
+
ownerMeetingId = 'other-meeting-id';
|
|
802
|
+
|
|
803
|
+
return 'REGISTER_AND_CONNECT_RESULT';
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const result = await webinar.updatePSDataChannel();
|
|
807
|
+
|
|
808
|
+
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
809
|
+
assert.calledOnce(webex.internal.llm.setOwnerMeetingId);
|
|
810
|
+
assert.calledWithExactly(
|
|
811
|
+
webex.internal.llm.setOwnerMeetingId,
|
|
812
|
+
'meeting-id',
|
|
813
|
+
PRACTICE_SESSION_KEY
|
|
814
|
+
);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('clears pre-claimed owner when registerAndConnect rejects', async () => {
|
|
818
|
+
const registerError = new Error('register failed');
|
|
819
|
+
let ownerMeetingId = 'meeting-id';
|
|
820
|
+
|
|
821
|
+
webex.internal.llm.getOwnerMeetingId.callsFake(() => ownerMeetingId);
|
|
822
|
+
webex.internal.llm.setOwnerMeetingId.callsFake((id) => {
|
|
823
|
+
ownerMeetingId = id;
|
|
824
|
+
});
|
|
825
|
+
webex.internal.llm.registerAndConnect = sinon.stub().rejects(registerError);
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
await webinar.updatePSDataChannel();
|
|
829
|
+
assert.fail('Expected updatePSDataChannel to reject when registerAndConnect fails');
|
|
830
|
+
} catch (error) {
|
|
831
|
+
assert.equal(error, registerError);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
assert.calledTwice(webex.internal.llm.setOwnerMeetingId);
|
|
835
|
+
assert.calledWithExactly(
|
|
836
|
+
webex.internal.llm.setOwnerMeetingId.firstCall,
|
|
837
|
+
'meeting-id',
|
|
838
|
+
PRACTICE_SESSION_KEY
|
|
839
|
+
);
|
|
840
|
+
assert.calledWithExactly(
|
|
841
|
+
webex.internal.llm.setOwnerMeetingId.secondCall,
|
|
842
|
+
undefined,
|
|
843
|
+
PRACTICE_SESSION_KEY
|
|
844
|
+
);
|
|
845
|
+
assert.isUndefined(ownerMeetingId);
|
|
846
|
+
});
|
|
546
847
|
});
|
|
547
848
|
|
|
548
849
|
describe('#updateStatusByRole', () => {
|
|
@@ -551,7 +852,7 @@ describe('plugin-meetings', () => {
|
|
|
551
852
|
updateMediaShares = sinon.stub()
|
|
552
853
|
webinar.webex.meetings = {
|
|
553
854
|
getMeetingByType: sinon.stub().returns({
|
|
554
|
-
id: 'meeting-id',
|
|
855
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
555
856
|
isJoined: sinon.stub().returns(false),
|
|
556
857
|
updateLLMConnection: sinon.stub(),
|
|
557
858
|
shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
|
|
@@ -606,7 +907,7 @@ describe('plugin-meetings', () => {
|
|
|
606
907
|
|
|
607
908
|
webinar.webex.meetings = {
|
|
608
909
|
getMeetingByType: sinon.stub().returns({
|
|
609
|
-
id: 'meeting-id',
|
|
910
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
610
911
|
isJoined: sinon.stub().returns(false),
|
|
611
912
|
updateLLMConnection: sinon.stub(),
|
|
612
913
|
shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
|
|
@@ -1034,7 +1335,7 @@ describe('plugin-meetings', () => {
|
|
|
1034
1335
|
// @ts-ignore
|
|
1035
1336
|
webinar.webex.meetings = {
|
|
1036
1337
|
getMeetingByType: sinon.stub().returns({
|
|
1037
|
-
id: 'meeting-id',
|
|
1338
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
1038
1339
|
locusInfo: {
|
|
1039
1340
|
links:{
|
|
1040
1341
|
resources: {
|
|
@@ -1051,7 +1352,7 @@ describe('plugin-meetings', () => {
|
|
|
1051
1352
|
it('throws an error if attendeeSearchUrl is not available', async () => {
|
|
1052
1353
|
webinar.webex.meetings = {
|
|
1053
1354
|
getMeetingByType: sinon.stub().returns({
|
|
1054
|
-
id: 'meeting-id',
|
|
1355
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
1055
1356
|
locusInfo: {
|
|
1056
1357
|
links:{
|
|
1057
1358
|
resources: {
|