@webex/plugin-meetings 3.12.0-next.7 → 3.12.0-next.71
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
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
ONLINE,
|
|
35
35
|
OFFLINE,
|
|
36
36
|
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
|
|
37
|
+
LOCUS_LLM_EVENT,
|
|
38
|
+
LLM_PRACTICE_SESSION,
|
|
39
|
+
RECORDING_STATE,
|
|
37
40
|
} from '@webex/plugin-meetings/src/constants';
|
|
38
41
|
import {
|
|
39
42
|
ConnectionState,
|
|
@@ -266,6 +269,20 @@ describe('plugin-meetings', () => {
|
|
|
266
269
|
stopReachability: sinon.stub(),
|
|
267
270
|
isSubnetReachable: sinon.stub().returns(true),
|
|
268
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
|
+
});
|
|
269
286
|
webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
270
287
|
webex.internal.llm.on = sinon.stub();
|
|
271
288
|
webex.internal.voicea.announce = sinon.stub();
|
|
@@ -417,6 +434,160 @@ describe('plugin-meetings', () => {
|
|
|
417
434
|
assert.instanceOf(meeting.mediaRequestManagers.screenShareVideo, MediaRequestManager);
|
|
418
435
|
});
|
|
419
436
|
|
|
437
|
+
it('getIngressPayloadType on webrtcMediaConnection is invoked for H264 when sending multistream video requests', () => {
|
|
438
|
+
const getIngressPayloadType = sinon.stub().returns(97);
|
|
439
|
+
|
|
440
|
+
meeting.isMultistream = true;
|
|
441
|
+
meeting.mediaProperties.webrtcMediaConnection = {
|
|
442
|
+
getIngressPayloadType,
|
|
443
|
+
requestMedia: sinon.stub(),
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const fakeReceiveSlot = {
|
|
447
|
+
on: sinon.stub(),
|
|
448
|
+
off: sinon.stub(),
|
|
449
|
+
sourceState: 'live',
|
|
450
|
+
mediaType: MediaType.VideoMain,
|
|
451
|
+
wcmeReceiveSlot: {id: 'fake-wcme-slot'},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
meeting.mediaRequestManagers.video.addRequest(
|
|
455
|
+
{
|
|
456
|
+
policyInfo: {
|
|
457
|
+
policy: 'receiver-selected',
|
|
458
|
+
csi: 42,
|
|
459
|
+
},
|
|
460
|
+
receiveSlots: [fakeReceiveSlot],
|
|
461
|
+
codecInfo: {
|
|
462
|
+
codec: 'h264',
|
|
463
|
+
maxFs: 3600,
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
true
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
assert.calledOnceWithExactly(
|
|
470
|
+
getIngressPayloadType,
|
|
471
|
+
MediaType.VideoMain,
|
|
472
|
+
MediaCodecMimeType.H264
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('getIngressPayloadType on webrtcMediaConnection is invoked for H264 and AV1 for slides video when AV1 slides support is enabled', () => {
|
|
477
|
+
const localWebex = new MockWebex({
|
|
478
|
+
children: {
|
|
479
|
+
meetings: Meetings,
|
|
480
|
+
credentials: Credentials,
|
|
481
|
+
support: Support,
|
|
482
|
+
llm: LLM,
|
|
483
|
+
mercury: Mercury,
|
|
484
|
+
},
|
|
485
|
+
config: {
|
|
486
|
+
credentials: {
|
|
487
|
+
client_id: 'mock-client-id',
|
|
488
|
+
},
|
|
489
|
+
meetings: {
|
|
490
|
+
reconnection: {
|
|
491
|
+
enabled: false,
|
|
492
|
+
},
|
|
493
|
+
mediaSettings: {},
|
|
494
|
+
metrics: {},
|
|
495
|
+
stats: {},
|
|
496
|
+
experimental: {enableUnifiedMeetings: true},
|
|
497
|
+
degradationPreferences: {maxMacroblocksLimit: 8192},
|
|
498
|
+
enableAv1SlidesSupport: true,
|
|
499
|
+
},
|
|
500
|
+
metrics: {
|
|
501
|
+
type: ['behavioral'],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
localWebex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
|
|
507
|
+
localWebex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId =
|
|
508
|
+
sinon.stub();
|
|
509
|
+
localWebex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
|
|
510
|
+
localWebex.internal.services = {get: sinon.stub().returns('locus-url')};
|
|
511
|
+
localWebex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
|
|
512
|
+
localWebex.internal.metrics.submitClientMetrics = sinon.stub().returns(Promise.resolve());
|
|
513
|
+
localWebex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
|
|
514
|
+
localWebex.meetings.reachability = {
|
|
515
|
+
isAnyPublicClusterReachable: sinon.stub().resolves(true),
|
|
516
|
+
getReachabilityResults: sinon.stub().resolves(undefined),
|
|
517
|
+
getReachabilityMetrics: sinon.stub().resolves({}),
|
|
518
|
+
stopReachability: sinon.stub(),
|
|
519
|
+
isSubnetReachable: sinon.stub().returns(true),
|
|
520
|
+
};
|
|
521
|
+
localWebex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
522
|
+
localWebex.internal.llm.on = sinon.stub();
|
|
523
|
+
localWebex.internal.voicea.announce = sinon.stub();
|
|
524
|
+
localWebex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
|
|
525
|
+
{},
|
|
526
|
+
{parent: localWebex}
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
Metrics.initialSetup(localWebex);
|
|
530
|
+
|
|
531
|
+
const localMeeting = new Meeting(
|
|
532
|
+
{
|
|
533
|
+
userId: uuid1,
|
|
534
|
+
resource: uuid2,
|
|
535
|
+
deviceUrl: uuid3,
|
|
536
|
+
locus: {url: url1},
|
|
537
|
+
destination: testDestination,
|
|
538
|
+
destinationType: DESTINATION_TYPE.MEETING_ID,
|
|
539
|
+
correlationId,
|
|
540
|
+
selfId: uuid1,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
parent: localWebex,
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const getIngressPayloadType = sinon.stub().callsFake((_mediaType, codecMimeType) => {
|
|
548
|
+
if (codecMimeType === MediaCodecMimeType.H264) {
|
|
549
|
+
return 97;
|
|
550
|
+
}
|
|
551
|
+
if (codecMimeType === MediaCodecMimeType.AV1) {
|
|
552
|
+
return 98;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return undefined;
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
localMeeting.isMultistream = true;
|
|
559
|
+
localMeeting.mediaProperties.webrtcMediaConnection = {
|
|
560
|
+
getIngressPayloadType,
|
|
561
|
+
requestMedia: sinon.stub(),
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const fakeReceiveSlot = {
|
|
565
|
+
on: sinon.stub(),
|
|
566
|
+
off: sinon.stub(),
|
|
567
|
+
sourceState: 'live',
|
|
568
|
+
mediaType: MediaType.VideoSlides,
|
|
569
|
+
wcmeReceiveSlot: {id: 'fake-wcme-slides-slot'},
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
localMeeting.mediaRequestManagers.screenShareVideo.addRequest(
|
|
573
|
+
{
|
|
574
|
+
policyInfo: {
|
|
575
|
+
policy: 'receiver-selected',
|
|
576
|
+
csi: 42,
|
|
577
|
+
},
|
|
578
|
+
receiveSlots: [fakeReceiveSlot],
|
|
579
|
+
codecInfo: {
|
|
580
|
+
codec: 'h264',
|
|
581
|
+
maxFs: 3600,
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
true
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
assert.calledWith(getIngressPayloadType, MediaType.VideoSlides, MediaCodecMimeType.H264);
|
|
588
|
+
assert.calledWith(getIngressPayloadType, MediaType.VideoSlides, MediaCodecMimeType.AV1);
|
|
589
|
+
});
|
|
590
|
+
|
|
420
591
|
it('uses meeting id as correlation id if not provided in constructor', () => {
|
|
421
592
|
const newMeeting = new Meeting(
|
|
422
593
|
{
|
|
@@ -1977,6 +2148,113 @@ describe('plugin-meetings', () => {
|
|
|
1977
2148
|
fakeProcessedReaction
|
|
1978
2149
|
);
|
|
1979
2150
|
});
|
|
2151
|
+
|
|
2152
|
+
[
|
|
2153
|
+
{
|
|
2154
|
+
title: 'should skip a reaction when the default relay route does not match the LLM binding',
|
|
2155
|
+
isPracticeSessionConnected: false,
|
|
2156
|
+
route: 'wrong-default-route',
|
|
2157
|
+
defaultBinding: 'default-route',
|
|
2158
|
+
practiceBinding: 'practice-route',
|
|
2159
|
+
shouldProcess: false,
|
|
2160
|
+
expectedSessionLabel: 'default session',
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
title: 'should process a reaction when the default relay route matches the LLM binding',
|
|
2164
|
+
isPracticeSessionConnected: false,
|
|
2165
|
+
route: 'default-route',
|
|
2166
|
+
defaultBinding: 'default-route',
|
|
2167
|
+
practiceBinding: 'practice-route',
|
|
2168
|
+
shouldProcess: true,
|
|
2169
|
+
},
|
|
2170
|
+
{
|
|
2171
|
+
title:
|
|
2172
|
+
'should process a reaction when the practice-session relay route matches the practice-session LLM binding',
|
|
2173
|
+
isPracticeSessionConnected: true,
|
|
2174
|
+
route: 'practice-route',
|
|
2175
|
+
defaultBinding: 'default-route',
|
|
2176
|
+
practiceBinding: 'practice-route',
|
|
2177
|
+
shouldProcess: true,
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
title:
|
|
2181
|
+
'should skip a reaction when the practice-session relay route does not match the practice-session LLM binding',
|
|
2182
|
+
isPracticeSessionConnected: true,
|
|
2183
|
+
route: 'default-route',
|
|
2184
|
+
defaultBinding: 'default-route',
|
|
2185
|
+
practiceBinding: 'practice-route',
|
|
2186
|
+
shouldProcess: false,
|
|
2187
|
+
expectedSessionLabel: 'practice session',
|
|
2188
|
+
},
|
|
2189
|
+
].forEach(
|
|
2190
|
+
({
|
|
2191
|
+
title,
|
|
2192
|
+
isPracticeSessionConnected,
|
|
2193
|
+
route,
|
|
2194
|
+
defaultBinding,
|
|
2195
|
+
practiceBinding,
|
|
2196
|
+
shouldProcess,
|
|
2197
|
+
expectedSessionLabel,
|
|
2198
|
+
}) => {
|
|
2199
|
+
it(title, () => {
|
|
2200
|
+
meeting.isReactionsSupported = sinon.stub().returns(true);
|
|
2201
|
+
meeting.config.receiveReactions = true;
|
|
2202
|
+
const fakeSendersName = 'Fake reactors name';
|
|
2203
|
+
meeting.members.membersCollection.get = sinon.stub().returns({name: fakeSendersName});
|
|
2204
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((llmSessionId) => {
|
|
2205
|
+
return llmSessionId === LLM_PRACTICE_SESSION && isPracticeSessionConnected;
|
|
2206
|
+
});
|
|
2207
|
+
webex.internal.llm.getBinding = sinon.stub().callsFake((llmSessionId) => {
|
|
2208
|
+
if (llmSessionId === LLM_PRACTICE_SESSION) {
|
|
2209
|
+
return practiceBinding;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
return defaultBinding;
|
|
2213
|
+
});
|
|
2214
|
+
const fakeReactionPayload = {
|
|
2215
|
+
type: 'fake_type',
|
|
2216
|
+
codepoints: 'fake_codepoints',
|
|
2217
|
+
shortcodes: 'fake_shortcodes',
|
|
2218
|
+
};
|
|
2219
|
+
const fakeSenderPayload = {
|
|
2220
|
+
participantId: 'fake_participant_id',
|
|
2221
|
+
};
|
|
2222
|
+
const fakeRelayEvent = {
|
|
2223
|
+
headers: {route},
|
|
2224
|
+
data: {
|
|
2225
|
+
relayType: REACTION_RELAY_TYPES.REACTION,
|
|
2226
|
+
reaction: fakeReactionPayload,
|
|
2227
|
+
sender: fakeSenderPayload,
|
|
2228
|
+
},
|
|
2229
|
+
};
|
|
2230
|
+
const fakeProcessedReaction = {
|
|
2231
|
+
reaction: fakeReactionPayload,
|
|
2232
|
+
sender: {
|
|
2233
|
+
id: fakeSenderPayload.participantId,
|
|
2234
|
+
name: fakeSendersName,
|
|
2235
|
+
},
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
TriggerProxy.trigger.resetHistory();
|
|
2239
|
+
meeting.processRelayEvent(fakeRelayEvent);
|
|
2240
|
+
|
|
2241
|
+
if (shouldProcess) {
|
|
2242
|
+
assert.calledWith(
|
|
2243
|
+
TriggerProxy.trigger,
|
|
2244
|
+
sinon.match.instanceOf(Meeting),
|
|
2245
|
+
{
|
|
2246
|
+
file: 'meeting/index',
|
|
2247
|
+
function: 'join',
|
|
2248
|
+
},
|
|
2249
|
+
EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
|
|
2250
|
+
fakeProcessedReaction
|
|
2251
|
+
);
|
|
2252
|
+
} else {
|
|
2253
|
+
assert.notCalled(TriggerProxy.trigger);
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
);
|
|
1980
2258
|
});
|
|
1981
2259
|
|
|
1982
2260
|
describe('#handleLLMOnline', () => {
|
|
@@ -2016,6 +2294,14 @@ describe('plugin-meetings', () => {
|
|
|
2016
2294
|
|
|
2017
2295
|
assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
|
|
2018
2296
|
});
|
|
2297
|
+
|
|
2298
|
+
it('calls syncAllHashTreeDatasets on locusInfo', () => {
|
|
2299
|
+
sinon.stub(meeting.locusInfo, 'syncAllHashTreeDatasets').resolves();
|
|
2300
|
+
|
|
2301
|
+
meeting.handleLLMOnline();
|
|
2302
|
+
|
|
2303
|
+
assert.calledOnceWithExactly(meeting.locusInfo.syncAllHashTreeDatasets, {onlyLLM: true});
|
|
2304
|
+
});
|
|
2019
2305
|
});
|
|
2020
2306
|
|
|
2021
2307
|
describe('#join', () => {
|
|
@@ -4534,6 +4820,297 @@ describe('plugin-meetings', () => {
|
|
|
4534
4820
|
},
|
|
4535
4821
|
});
|
|
4536
4822
|
});
|
|
4823
|
+
|
|
4824
|
+
describe('handles STATS_UPDATE event for SRTP cipher detection', () => {
|
|
4825
|
+
it('emits MEETING_SRTP_CIPHER_UPDATED event when srtpCipher is found in transport stats', async () => {
|
|
4826
|
+
const fakeStats = new Map([
|
|
4827
|
+
[
|
|
4828
|
+
'transport-1',
|
|
4829
|
+
{
|
|
4830
|
+
type: 'transport',
|
|
4831
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4832
|
+
dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
|
4833
|
+
},
|
|
4834
|
+
],
|
|
4835
|
+
[
|
|
4836
|
+
'outbound-rtp-1',
|
|
4837
|
+
{
|
|
4838
|
+
type: 'outbound-rtp',
|
|
4839
|
+
ssrc: 12345,
|
|
4840
|
+
},
|
|
4841
|
+
],
|
|
4842
|
+
]);
|
|
4843
|
+
|
|
4844
|
+
statsAnalyzerStub.emit(
|
|
4845
|
+
{file: 'test', function: 'test'},
|
|
4846
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4847
|
+
{stats: fakeStats}
|
|
4848
|
+
);
|
|
4849
|
+
|
|
4850
|
+
assert.calledWith(
|
|
4851
|
+
TriggerProxy.trigger,
|
|
4852
|
+
sinon.match.instanceOf(Meeting),
|
|
4853
|
+
{
|
|
4854
|
+
file: 'meeting/index',
|
|
4855
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4856
|
+
},
|
|
4857
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4858
|
+
{srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
|
|
4859
|
+
);
|
|
4860
|
+
|
|
4861
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4862
|
+
});
|
|
4863
|
+
|
|
4864
|
+
it('updates meeting.mediaProperties.srtpCipher when cipher changes', async () => {
|
|
4865
|
+
const firstStats = new Map([
|
|
4866
|
+
[
|
|
4867
|
+
'transport-1',
|
|
4868
|
+
{
|
|
4869
|
+
type: 'transport',
|
|
4870
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4871
|
+
},
|
|
4872
|
+
],
|
|
4873
|
+
]);
|
|
4874
|
+
|
|
4875
|
+
statsAnalyzerStub.emit(
|
|
4876
|
+
{file: 'test', function: 'test'},
|
|
4877
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4878
|
+
{stats: firstStats}
|
|
4879
|
+
);
|
|
4880
|
+
|
|
4881
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4882
|
+
|
|
4883
|
+
const secondStats = new Map([
|
|
4884
|
+
[
|
|
4885
|
+
'transport-1',
|
|
4886
|
+
{
|
|
4887
|
+
type: 'transport',
|
|
4888
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
4889
|
+
},
|
|
4890
|
+
],
|
|
4891
|
+
]);
|
|
4892
|
+
|
|
4893
|
+
TriggerProxy.trigger.resetHistory();
|
|
4894
|
+
|
|
4895
|
+
statsAnalyzerStub.emit(
|
|
4896
|
+
{file: 'test', function: 'test'},
|
|
4897
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4898
|
+
{stats: secondStats}
|
|
4899
|
+
);
|
|
4900
|
+
|
|
4901
|
+
assert.calledWith(
|
|
4902
|
+
TriggerProxy.trigger,
|
|
4903
|
+
sinon.match.instanceOf(Meeting),
|
|
4904
|
+
{
|
|
4905
|
+
file: 'meeting/index',
|
|
4906
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4907
|
+
},
|
|
4908
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4909
|
+
{srtpCipher: 'AEAD_AES_256_GCM'}
|
|
4910
|
+
);
|
|
4911
|
+
|
|
4912
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AEAD_AES_256_GCM');
|
|
4913
|
+
});
|
|
4914
|
+
|
|
4915
|
+
it('does not emit event when srtpCipher has not changed', async () => {
|
|
4916
|
+
const firstStats = new Map([
|
|
4917
|
+
[
|
|
4918
|
+
'transport-1',
|
|
4919
|
+
{
|
|
4920
|
+
type: 'transport',
|
|
4921
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4922
|
+
},
|
|
4923
|
+
],
|
|
4924
|
+
]);
|
|
4925
|
+
|
|
4926
|
+
statsAnalyzerStub.emit(
|
|
4927
|
+
{file: 'test', function: 'test'},
|
|
4928
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4929
|
+
{stats: firstStats}
|
|
4930
|
+
);
|
|
4931
|
+
|
|
4932
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4933
|
+
|
|
4934
|
+
TriggerProxy.trigger.resetHistory();
|
|
4935
|
+
|
|
4936
|
+
// Emit same cipher again
|
|
4937
|
+
statsAnalyzerStub.emit(
|
|
4938
|
+
{file: 'test', function: 'test'},
|
|
4939
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4940
|
+
{stats: firstStats}
|
|
4941
|
+
);
|
|
4942
|
+
|
|
4943
|
+
// Should not trigger event again
|
|
4944
|
+
assert.neverCalledWith(
|
|
4945
|
+
TriggerProxy.trigger,
|
|
4946
|
+
sinon.match.instanceOf(Meeting),
|
|
4947
|
+
sinon.match.any,
|
|
4948
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4949
|
+
sinon.match.any
|
|
4950
|
+
);
|
|
4951
|
+
|
|
4952
|
+
// Cipher should remain the same
|
|
4953
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4954
|
+
});
|
|
4955
|
+
|
|
4956
|
+
it('does not emit event when stats contain no transport with srtpCipher', async () => {
|
|
4957
|
+
const fakeStats = new Map([
|
|
4958
|
+
[
|
|
4959
|
+
'outbound-rtp-1',
|
|
4960
|
+
{
|
|
4961
|
+
type: 'outbound-rtp',
|
|
4962
|
+
ssrc: 12345,
|
|
4963
|
+
},
|
|
4964
|
+
],
|
|
4965
|
+
[
|
|
4966
|
+
'inbound-rtp-1',
|
|
4967
|
+
{
|
|
4968
|
+
type: 'inbound-rtp',
|
|
4969
|
+
ssrc: 67890,
|
|
4970
|
+
},
|
|
4971
|
+
],
|
|
4972
|
+
]);
|
|
4973
|
+
|
|
4974
|
+
statsAnalyzerStub.emit(
|
|
4975
|
+
{file: 'test', function: 'test'},
|
|
4976
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4977
|
+
{stats: fakeStats}
|
|
4978
|
+
);
|
|
4979
|
+
|
|
4980
|
+
assert.neverCalledWith(
|
|
4981
|
+
TriggerProxy.trigger,
|
|
4982
|
+
sinon.match.instanceOf(Meeting),
|
|
4983
|
+
sinon.match.any,
|
|
4984
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4985
|
+
sinon.match.any
|
|
4986
|
+
);
|
|
4987
|
+
|
|
4988
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
4989
|
+
});
|
|
4990
|
+
|
|
4991
|
+
it('does not emit event when transport stat has no srtpCipher property', async () => {
|
|
4992
|
+
const fakeStats = new Map([
|
|
4993
|
+
[
|
|
4994
|
+
'transport-1',
|
|
4995
|
+
{
|
|
4996
|
+
type: 'transport',
|
|
4997
|
+
dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
|
4998
|
+
// no srtpCipher property
|
|
4999
|
+
},
|
|
5000
|
+
],
|
|
5001
|
+
]);
|
|
5002
|
+
|
|
5003
|
+
statsAnalyzerStub.emit(
|
|
5004
|
+
{file: 'test', function: 'test'},
|
|
5005
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
5006
|
+
{stats: fakeStats}
|
|
5007
|
+
);
|
|
5008
|
+
|
|
5009
|
+
assert.neverCalledWith(
|
|
5010
|
+
TriggerProxy.trigger,
|
|
5011
|
+
sinon.match.instanceOf(Meeting),
|
|
5012
|
+
sinon.match.any,
|
|
5013
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
5014
|
+
sinon.match.any
|
|
5015
|
+
);
|
|
5016
|
+
|
|
5017
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
5018
|
+
});
|
|
5019
|
+
|
|
5020
|
+
it('uses first transport with srtpCipher when multiple transports exist', async () => {
|
|
5021
|
+
const fakeStats = new Map([
|
|
5022
|
+
[
|
|
5023
|
+
'transport-1',
|
|
5024
|
+
{
|
|
5025
|
+
type: 'transport',
|
|
5026
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
5027
|
+
},
|
|
5028
|
+
],
|
|
5029
|
+
[
|
|
5030
|
+
'transport-2',
|
|
5031
|
+
{
|
|
5032
|
+
type: 'transport',
|
|
5033
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
5034
|
+
},
|
|
5035
|
+
],
|
|
5036
|
+
[
|
|
5037
|
+
'outbound-rtp-1',
|
|
5038
|
+
{
|
|
5039
|
+
type: 'outbound-rtp',
|
|
5040
|
+
ssrc: 12345,
|
|
5041
|
+
},
|
|
5042
|
+
],
|
|
5043
|
+
]);
|
|
5044
|
+
|
|
5045
|
+
statsAnalyzerStub.emit(
|
|
5046
|
+
{file: 'test', function: 'test'},
|
|
5047
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
5048
|
+
{stats: fakeStats}
|
|
5049
|
+
);
|
|
5050
|
+
|
|
5051
|
+
assert.calledWith(
|
|
5052
|
+
TriggerProxy.trigger,
|
|
5053
|
+
sinon.match.instanceOf(Meeting),
|
|
5054
|
+
{
|
|
5055
|
+
file: 'meeting/index',
|
|
5056
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
5057
|
+
},
|
|
5058
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
5059
|
+
{srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
|
|
5060
|
+
);
|
|
5061
|
+
|
|
5062
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
5063
|
+
});
|
|
5064
|
+
|
|
5065
|
+
it('handles empty stats map without errors', async () => {
|
|
5066
|
+
const emptyStats = new Map();
|
|
5067
|
+
|
|
5068
|
+
statsAnalyzerStub.emit(
|
|
5069
|
+
{file: 'test', function: 'test'},
|
|
5070
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
5071
|
+
{stats: emptyStats}
|
|
5072
|
+
);
|
|
5073
|
+
|
|
5074
|
+
assert.neverCalledWith(
|
|
5075
|
+
TriggerProxy.trigger,
|
|
5076
|
+
sinon.match.instanceOf(Meeting),
|
|
5077
|
+
sinon.match.any,
|
|
5078
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
5079
|
+
sinon.match.any
|
|
5080
|
+
);
|
|
5081
|
+
|
|
5082
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
5083
|
+
});
|
|
5084
|
+
|
|
5085
|
+
it('logs cipher change when cipher is updated', async () => {
|
|
5086
|
+
const loggerSpy = sinon.spy(LoggerProxy.logger, 'info');
|
|
5087
|
+
|
|
5088
|
+
meeting.mediaProperties.srtpCipher = 'AES_CM_128_HMAC_SHA1_80';
|
|
5089
|
+
|
|
5090
|
+
const newStats = new Map([
|
|
5091
|
+
[
|
|
5092
|
+
'transport-1',
|
|
5093
|
+
{
|
|
5094
|
+
type: 'transport',
|
|
5095
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
5096
|
+
},
|
|
5097
|
+
],
|
|
5098
|
+
]);
|
|
5099
|
+
|
|
5100
|
+
statsAnalyzerStub.emit(
|
|
5101
|
+
{file: 'test', function: 'test'},
|
|
5102
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
5103
|
+
{stats: newStats}
|
|
5104
|
+
);
|
|
5105
|
+
|
|
5106
|
+
assert.calledWithMatch(
|
|
5107
|
+
loggerSpy,
|
|
5108
|
+
sinon.match(/SRTP cipher changed from AES_CM_128_HMAC_SHA1_80 to AEAD_AES_256_GCM/)
|
|
5109
|
+
);
|
|
5110
|
+
|
|
5111
|
+
loggerSpy.restore();
|
|
5112
|
+
});
|
|
5113
|
+
});
|
|
4537
5114
|
});
|
|
4538
5115
|
|
|
4539
5116
|
describe('handles StatsMonitor events', () => {
|
|
@@ -6429,6 +7006,9 @@ describe('plugin-meetings', () => {
|
|
|
6429
7006
|
|
|
6430
7007
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
6431
7008
|
webex.internal.llm.off = sinon.stub();
|
|
7009
|
+
webex.internal.mercury.off = sinon.stub();
|
|
7010
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
7011
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
6432
7012
|
|
|
6433
7013
|
// A meeting needs to be joined to leave
|
|
6434
7014
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -6452,6 +7032,67 @@ describe('plugin-meetings', () => {
|
|
|
6452
7032
|
assert.calledOnce(meeting.clearMeetingData);
|
|
6453
7033
|
});
|
|
6454
7034
|
|
|
7035
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /leave', async () => {
|
|
7036
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
7037
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
7038
|
+
|
|
7039
|
+
await meeting.leave();
|
|
7040
|
+
|
|
7041
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
7042
|
+
// annotation) must be detached before the /leave request so that
|
|
7043
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
7044
|
+
// (per Locus team recommendation).
|
|
7045
|
+
assert.callOrder(
|
|
7046
|
+
webex.internal.llm.off,
|
|
7047
|
+
webex.internal.mercury.off,
|
|
7048
|
+
meeting.stopTranscription,
|
|
7049
|
+
meeting.annotation.deregisterEvents,
|
|
7050
|
+
meeting.meetingRequest.leaveMeeting
|
|
7051
|
+
);
|
|
7052
|
+
assert.calledWithExactly(
|
|
7053
|
+
webex.internal.llm.off,
|
|
7054
|
+
'event:relay.event',
|
|
7055
|
+
meeting.processRelayEvent
|
|
7056
|
+
);
|
|
7057
|
+
assert.calledWithExactly(
|
|
7058
|
+
webex.internal.llm.off,
|
|
7059
|
+
LOCUS_LLM_EVENT,
|
|
7060
|
+
meeting.processLocusLLMEvent
|
|
7061
|
+
);
|
|
7062
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
7063
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
7064
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
7065
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
7066
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
7067
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
7068
|
+
assert.isUndefined(meeting.transcription);
|
|
7069
|
+
});
|
|
7070
|
+
|
|
7071
|
+
it('tears down llm/mercury/transcription/annotation even when /leave rejects', async () => {
|
|
7072
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
7073
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
7074
|
+
meeting.meetingRequest.leaveMeeting = sinon
|
|
7075
|
+
.stub()
|
|
7076
|
+
.returns(Promise.reject(new Error('leave failed')));
|
|
7077
|
+
|
|
7078
|
+
await meeting.leave().catch(() => {});
|
|
7079
|
+
|
|
7080
|
+
assert.calledWithExactly(
|
|
7081
|
+
webex.internal.llm.off,
|
|
7082
|
+
'event:relay.event',
|
|
7083
|
+
meeting.processRelayEvent
|
|
7084
|
+
);
|
|
7085
|
+
assert.calledWithExactly(
|
|
7086
|
+
webex.internal.llm.off,
|
|
7087
|
+
LOCUS_LLM_EVENT,
|
|
7088
|
+
meeting.processLocusLLMEvent
|
|
7089
|
+
);
|
|
7090
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
7091
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
7092
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
7093
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
7094
|
+
});
|
|
7095
|
+
|
|
6455
7096
|
it('should reset call diagnostic latencies correctly', async () => {
|
|
6456
7097
|
const leave = meeting.leave();
|
|
6457
7098
|
|
|
@@ -8459,6 +9100,9 @@ describe('plugin-meetings', () => {
|
|
|
8459
9100
|
|
|
8460
9101
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
8461
9102
|
webex.internal.llm.off = sinon.stub();
|
|
9103
|
+
webex.internal.mercury.off = sinon.stub();
|
|
9104
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
9105
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
8462
9106
|
|
|
8463
9107
|
// A meeting needs to be joined to end
|
|
8464
9108
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -8481,6 +9125,66 @@ describe('plugin-meetings', () => {
|
|
|
8481
9125
|
assert.calledOnce(meeting?.unsetPeerConnections);
|
|
8482
9126
|
assert.calledOnce(meeting?.clearMeetingData);
|
|
8483
9127
|
});
|
|
9128
|
+
|
|
9129
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /end', async () => {
|
|
9130
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
9131
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
9132
|
+
|
|
9133
|
+
await meeting.endMeetingForAll();
|
|
9134
|
+
|
|
9135
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
9136
|
+
// annotation) must be detached before the /end request so that
|
|
9137
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
9138
|
+
// (per Locus team recommendation).
|
|
9139
|
+
assert.callOrder(
|
|
9140
|
+
webex.internal.llm.off,
|
|
9141
|
+
webex.internal.mercury.off,
|
|
9142
|
+
meeting.stopTranscription,
|
|
9143
|
+
meeting.annotation.deregisterEvents,
|
|
9144
|
+
meeting.meetingRequest.endMeetingForAll
|
|
9145
|
+
);
|
|
9146
|
+
assert.calledWithExactly(
|
|
9147
|
+
webex.internal.llm.off,
|
|
9148
|
+
'event:relay.event',
|
|
9149
|
+
meeting.processRelayEvent
|
|
9150
|
+
);
|
|
9151
|
+
assert.calledWithExactly(
|
|
9152
|
+
webex.internal.llm.off,
|
|
9153
|
+
LOCUS_LLM_EVENT,
|
|
9154
|
+
meeting.processLocusLLMEvent
|
|
9155
|
+
);
|
|
9156
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
9157
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
9158
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
9159
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
9160
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
9161
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
9162
|
+
});
|
|
9163
|
+
|
|
9164
|
+
it('tears down llm/mercury/transcription/annotation even when /end rejects', async () => {
|
|
9165
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
9166
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
9167
|
+
meeting.meetingRequest.endMeetingForAll = sinon
|
|
9168
|
+
.stub()
|
|
9169
|
+
.returns(Promise.reject(new Error('end failed')));
|
|
9170
|
+
|
|
9171
|
+
await meeting.endMeetingForAll().catch(() => {});
|
|
9172
|
+
|
|
9173
|
+
assert.calledWithExactly(
|
|
9174
|
+
webex.internal.llm.off,
|
|
9175
|
+
'event:relay.event',
|
|
9176
|
+
meeting.processRelayEvent
|
|
9177
|
+
);
|
|
9178
|
+
assert.calledWithExactly(
|
|
9179
|
+
webex.internal.llm.off,
|
|
9180
|
+
LOCUS_LLM_EVENT,
|
|
9181
|
+
meeting.processLocusLLMEvent
|
|
9182
|
+
);
|
|
9183
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
9184
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
9185
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
9186
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
9187
|
+
});
|
|
8484
9188
|
});
|
|
8485
9189
|
|
|
8486
9190
|
describe('#moveTo', () => {
|
|
@@ -10417,7 +11121,7 @@ describe('plugin-meetings', () => {
|
|
|
10417
11121
|
);
|
|
10418
11122
|
done();
|
|
10419
11123
|
});
|
|
10420
|
-
it('listens to the self admitted guest event
|
|
11124
|
+
it('listens to the self admitted guest event and waits for token prefetch before reconnecting LLM', async () => {
|
|
10421
11125
|
meeting.stopKeepAlive = sinon.stub();
|
|
10422
11126
|
meeting.updateLLMConnection = sinon.stub();
|
|
10423
11127
|
let resolvePrefetch;
|
|
@@ -10443,7 +11147,7 @@ describe('plugin-meetings', () => {
|
|
|
10443
11147
|
'meeting:self:guestAdmitted',
|
|
10444
11148
|
{payload: test1}
|
|
10445
11149
|
);
|
|
10446
|
-
assert.
|
|
11150
|
+
assert.notCalled(meeting.updateLLMConnection);
|
|
10447
11151
|
assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics);
|
|
10448
11152
|
|
|
10449
11153
|
assert.calledOnceWithExactly(
|
|
@@ -10456,6 +11160,7 @@ describe('plugin-meetings', () => {
|
|
|
10456
11160
|
|
|
10457
11161
|
resolvePrefetch(false);
|
|
10458
11162
|
await Promise.resolve();
|
|
11163
|
+
await Promise.resolve();
|
|
10459
11164
|
|
|
10460
11165
|
assert.calledOnce(meeting.updateLLMConnection);
|
|
10461
11166
|
});
|
|
@@ -10971,6 +11676,92 @@ describe('plugin-meetings', () => {
|
|
|
10971
11676
|
);
|
|
10972
11677
|
});
|
|
10973
11678
|
|
|
11679
|
+
const recordingTestCases = [
|
|
11680
|
+
{
|
|
11681
|
+
description: 'triggers MEETING_STARTED_RECORDING when state is RECORDING',
|
|
11682
|
+
state: RECORDING_STATE.RECORDING,
|
|
11683
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_STARTED_RECORDING,
|
|
11684
|
+
expectedRecordingState: RECORDING_STATE.RECORDING,
|
|
11685
|
+
},
|
|
11686
|
+
{
|
|
11687
|
+
description: 'triggers MEETING_STOPPED_RECORDING when state is IDLE',
|
|
11688
|
+
state: RECORDING_STATE.IDLE,
|
|
11689
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_STOPPED_RECORDING,
|
|
11690
|
+
expectedRecordingState: RECORDING_STATE.IDLE,
|
|
11691
|
+
},
|
|
11692
|
+
{
|
|
11693
|
+
description: 'triggers MEETING_PAUSED_RECORDING when state is PAUSED',
|
|
11694
|
+
state: RECORDING_STATE.PAUSED,
|
|
11695
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_PAUSED_RECORDING,
|
|
11696
|
+
expectedRecordingState: RECORDING_STATE.PAUSED,
|
|
11697
|
+
},
|
|
11698
|
+
{
|
|
11699
|
+
description:
|
|
11700
|
+
'triggers MEETING_RESUMED_RECORDING and sets state to RECORDING when state is RESUMED',
|
|
11701
|
+
state: RECORDING_STATE.RESUMED,
|
|
11702
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_RESUMED_RECORDING,
|
|
11703
|
+
expectedRecordingState: RECORDING_STATE.RECORDING,
|
|
11704
|
+
},
|
|
11705
|
+
];
|
|
11706
|
+
|
|
11707
|
+
recordingTestCases.forEach(({description, state, expectedEvent, expectedRecordingState}) => {
|
|
11708
|
+
it(`listens to CONTROLS_RECORDING_UPDATED - ${description}`, async () => {
|
|
11709
|
+
const modifiedBy = 'user-id-123';
|
|
11710
|
+
const lastModified = '2026-01-01T00:00:00Z';
|
|
11711
|
+
|
|
11712
|
+
await meeting.locusInfo.emitScoped(
|
|
11713
|
+
{function: 'test', file: 'test'},
|
|
11714
|
+
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
11715
|
+
{state, modifiedBy, lastModified, modifiedByServiceAppName: undefined, modifiedByServiceAppId: undefined}
|
|
11716
|
+
);
|
|
11717
|
+
|
|
11718
|
+
assert.deepEqual(meeting.recording, {
|
|
11719
|
+
state: expectedRecordingState,
|
|
11720
|
+
modifiedBy,
|
|
11721
|
+
lastModified,
|
|
11722
|
+
modifiedByServiceAppName: undefined,
|
|
11723
|
+
modifiedByServiceAppId: undefined,
|
|
11724
|
+
});
|
|
11725
|
+
|
|
11726
|
+
assert.calledWith(
|
|
11727
|
+
TriggerProxy.trigger,
|
|
11728
|
+
meeting,
|
|
11729
|
+
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
11730
|
+
expectedEvent,
|
|
11731
|
+
meeting.recording
|
|
11732
|
+
);
|
|
11733
|
+
});
|
|
11734
|
+
});
|
|
11735
|
+
|
|
11736
|
+
it('listens to CONTROLS_RECORDING_UPDATED and includes modifiedByServiceAppName and modifiedByServiceAppId when present', async () => {
|
|
11737
|
+
const modifiedBy = 'user-id-123';
|
|
11738
|
+
const lastModified = '2026-01-01T00:00:00Z';
|
|
11739
|
+
const modifiedByServiceAppName = 'My Bot';
|
|
11740
|
+
const modifiedByServiceAppId = 'app-id-123';
|
|
11741
|
+
|
|
11742
|
+
await meeting.locusInfo.emitScoped(
|
|
11743
|
+
{function: 'test', file: 'test'},
|
|
11744
|
+
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
11745
|
+
{state: RECORDING_STATE.RECORDING, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}
|
|
11746
|
+
);
|
|
11747
|
+
|
|
11748
|
+
assert.deepEqual(meeting.recording, {
|
|
11749
|
+
state: RECORDING_STATE.RECORDING,
|
|
11750
|
+
modifiedBy,
|
|
11751
|
+
lastModified,
|
|
11752
|
+
modifiedByServiceAppName,
|
|
11753
|
+
modifiedByServiceAppId,
|
|
11754
|
+
});
|
|
11755
|
+
|
|
11756
|
+
assert.calledWith(
|
|
11757
|
+
TriggerProxy.trigger,
|
|
11758
|
+
meeting,
|
|
11759
|
+
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
11760
|
+
EVENT_TRIGGERS.MEETING_STARTED_RECORDING,
|
|
11761
|
+
meeting.recording
|
|
11762
|
+
);
|
|
11763
|
+
});
|
|
11764
|
+
|
|
10974
11765
|
it('listens to the locus interpretation update event', () => {
|
|
10975
11766
|
const interpretation = {
|
|
10976
11767
|
siLanguages: [{languageCode: 20, languageName: 'en'}],
|
|
@@ -11024,6 +11815,7 @@ describe('plugin-meetings', () => {
|
|
|
11024
11815
|
meeting.annotation.locusUrlUpdate = sinon.stub();
|
|
11025
11816
|
meeting.simultaneousInterpretation.locusUrlUpdate = sinon.stub();
|
|
11026
11817
|
meeting.webinar.locusUrlUpdate = sinon.stub();
|
|
11818
|
+
meeting.aiEnableRequest.locusUrlUpdate = sinon.stub();
|
|
11027
11819
|
|
|
11028
11820
|
meeting.locusInfo.emit(
|
|
11029
11821
|
{function: 'test', file: 'test'},
|
|
@@ -11038,6 +11830,7 @@ describe('plugin-meetings', () => {
|
|
|
11038
11830
|
assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, false);
|
|
11039
11831
|
assert.calledWith(meeting.simultaneousInterpretation.locusUrlUpdate, newLocusUrl);
|
|
11040
11832
|
assert.calledWith(meeting.webinar.locusUrlUpdate, newLocusUrl);
|
|
11833
|
+
assert.calledWith(meeting.aiEnableRequest.locusUrlUpdate, newLocusUrl);
|
|
11041
11834
|
assert.equal(meeting.locusUrl, newLocusUrl);
|
|
11042
11835
|
assert(meeting.locusId, '12345');
|
|
11043
11836
|
|
|
@@ -11353,6 +12146,109 @@ describe('plugin-meetings', () => {
|
|
|
11353
12146
|
});
|
|
11354
12147
|
});
|
|
11355
12148
|
|
|
12149
|
+
describe('#finalizeMeetingAfterInitialLocusSetup', () => {
|
|
12150
|
+
it('refreshes destination from synced locus when destination type is LOCUS_ID', () => {
|
|
12151
|
+
const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12152
|
+
|
|
12153
|
+
meeting.destinationType = DESTINATION_TYPE.LOCUS_ID;
|
|
12154
|
+
meeting.destination = {info: {topic: 'old'}};
|
|
12155
|
+
|
|
12156
|
+
meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
|
|
12157
|
+
|
|
12158
|
+
assert.equal(meeting.destination, syncedLocus);
|
|
12159
|
+
});
|
|
12160
|
+
|
|
12161
|
+
it('does not refresh destination when destination type is not LOCUS_ID', () => {
|
|
12162
|
+
const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12163
|
+
const originalDestination = {destination: 'original-destination'};
|
|
12164
|
+
|
|
12165
|
+
meeting.destinationType = DESTINATION_TYPE.CONVERSATION_URL;
|
|
12166
|
+
meeting.destination = originalDestination;
|
|
12167
|
+
|
|
12168
|
+
meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
|
|
12169
|
+
|
|
12170
|
+
assert.equal(meeting.destination, originalDestination);
|
|
12171
|
+
});
|
|
12172
|
+
|
|
12173
|
+
it('fetches meeting info when meetingInfo is empty and destination has info', () => {
|
|
12174
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12175
|
+
|
|
12176
|
+
meeting.meetingInfo = {};
|
|
12177
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12178
|
+
|
|
12179
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12180
|
+
|
|
12181
|
+
assert.calledOnceWithExactly(fetchMeetingInfoStub, {});
|
|
12182
|
+
});
|
|
12183
|
+
|
|
12184
|
+
it('does not fetch meeting info when destination has no info', () => {
|
|
12185
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12186
|
+
|
|
12187
|
+
meeting.meetingInfo = {};
|
|
12188
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123'};
|
|
12189
|
+
|
|
12190
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12191
|
+
|
|
12192
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
12193
|
+
});
|
|
12194
|
+
|
|
12195
|
+
it('does not fetch meeting info when meetingInfo is already populated', () => {
|
|
12196
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12197
|
+
|
|
12198
|
+
meeting.meetingInfo = {meetingJoinUrl: 'https://example.com/join/abc'};
|
|
12199
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12200
|
+
|
|
12201
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12202
|
+
|
|
12203
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
12204
|
+
});
|
|
12205
|
+
|
|
12206
|
+
it('does not fetch meeting info when delayed fetch timer is already scheduled', () => {
|
|
12207
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12208
|
+
|
|
12209
|
+
meeting.meetingInfo = {};
|
|
12210
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12211
|
+
meeting.fetchMeetingInfoTimeoutId = 42;
|
|
12212
|
+
|
|
12213
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12214
|
+
|
|
12215
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
12216
|
+
});
|
|
12217
|
+
|
|
12218
|
+
['CALL', 'SIP_BRIDGE', 'SPACE_SHARE'].forEach((fullStateType) => {
|
|
12219
|
+
it(`does not fetch meeting info when destination is a 1:1 call (fullState.type ${fullStateType})`, () => {
|
|
12220
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12221
|
+
|
|
12222
|
+
meeting.meetingInfo = {};
|
|
12223
|
+
meeting.destination = {
|
|
12224
|
+
url: 'https://locus.example.com/locus/123',
|
|
12225
|
+
info: {topic: 'x'},
|
|
12226
|
+
};
|
|
12227
|
+
|
|
12228
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({fullState: {type: fullStateType}});
|
|
12229
|
+
|
|
12230
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
12231
|
+
});
|
|
12232
|
+
});
|
|
12233
|
+
|
|
12234
|
+
it('swallows async fetchMeetingInfo errors and logs info', async () => {
|
|
12235
|
+
const error = new Error('fetch failed');
|
|
12236
|
+
|
|
12237
|
+
meeting.meetingInfo = {};
|
|
12238
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12239
|
+
sinon.stub(meeting, 'fetchMeetingInfo').returns(Promise.reject(error));
|
|
12240
|
+
const loggerInfoStub = sinon.stub(LoggerProxy.logger, 'info');
|
|
12241
|
+
|
|
12242
|
+
await meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12243
|
+
|
|
12244
|
+
assert.calledOnce(loggerInfoStub);
|
|
12245
|
+
assert.match(
|
|
12246
|
+
loggerInfoStub.firstCall.args[0],
|
|
12247
|
+
/Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: fetch failed/
|
|
12248
|
+
);
|
|
12249
|
+
});
|
|
12250
|
+
});
|
|
12251
|
+
|
|
11356
12252
|
describe('#emailInput', () => {
|
|
11357
12253
|
it('should set the email input', () => {
|
|
11358
12254
|
assert.notOk(meeting.emailInput);
|
|
@@ -11955,6 +12851,7 @@ describe('plugin-meetings', () => {
|
|
|
11955
12851
|
let showAutoEndMeetingWarningSpy;
|
|
11956
12852
|
let canAttendeeRequestAiAssistantEnabledSpy;
|
|
11957
12853
|
let attendeeRequestAiAssistantDeclinedAllSpy;
|
|
12854
|
+
let isAnonymizeDisplayNamesEnabledSpy;
|
|
11958
12855
|
// Due to import tree issues, hasHints must be stubed within the scope of the `it`.
|
|
11959
12856
|
|
|
11960
12857
|
beforeEach(() => {
|
|
@@ -12003,6 +12900,10 @@ describe('plugin-meetings', () => {
|
|
|
12003
12900
|
MeetingUtil,
|
|
12004
12901
|
'attendeeRequestAiAssistantDeclinedAll'
|
|
12005
12902
|
);
|
|
12903
|
+
isAnonymizeDisplayNamesEnabledSpy = sinon.spy(
|
|
12904
|
+
MeetingUtil,
|
|
12905
|
+
'isAnonymizeDisplayNamesEnabled'
|
|
12906
|
+
);
|
|
12006
12907
|
});
|
|
12007
12908
|
|
|
12008
12909
|
afterEach(() => {
|
|
@@ -12011,6 +12912,7 @@ describe('plugin-meetings', () => {
|
|
|
12011
12912
|
showAutoEndMeetingWarningSpy.restore();
|
|
12012
12913
|
canAttendeeRequestAiAssistantEnabledSpy.restore();
|
|
12013
12914
|
attendeeRequestAiAssistantDeclinedAllSpy.restore();
|
|
12915
|
+
isAnonymizeDisplayNamesEnabledSpy.restore();
|
|
12014
12916
|
});
|
|
12015
12917
|
|
|
12016
12918
|
forEach(
|
|
@@ -12568,6 +13470,7 @@ describe('plugin-meetings', () => {
|
|
|
12568
13470
|
meeting.roles
|
|
12569
13471
|
);
|
|
12570
13472
|
assert.calledWith(attendeeRequestAiAssistantDeclinedAllSpy, userDisplayHints);
|
|
13473
|
+
assert.calledWith(isAnonymizeDisplayNamesEnabledSpy, userDisplayHints);
|
|
12571
13474
|
|
|
12572
13475
|
assert.calledWith(ControlsOptionsUtil.hasHints, {
|
|
12573
13476
|
requiredHints: [DISPLAY_HINTS.MUTE_ALL],
|
|
@@ -12782,6 +13685,10 @@ describe('plugin-meetings', () => {
|
|
|
12782
13685
|
describe('#saveDataChannelToken', () => {
|
|
12783
13686
|
beforeEach(() => {
|
|
12784
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);
|
|
12785
13692
|
});
|
|
12786
13693
|
|
|
12787
13694
|
it('saves datachannelToken into LLM as Default', () => {
|
|
@@ -12794,7 +13701,8 @@ describe('plugin-meetings', () => {
|
|
|
12794
13701
|
assert.calledWithExactly(
|
|
12795
13702
|
webex.internal.llm.setDatachannelToken,
|
|
12796
13703
|
'default-token',
|
|
12797
|
-
'llm-default-session'
|
|
13704
|
+
'llm-default-session',
|
|
13705
|
+
meeting.id
|
|
12798
13706
|
);
|
|
12799
13707
|
});
|
|
12800
13708
|
|
|
@@ -12808,7 +13716,8 @@ describe('plugin-meetings', () => {
|
|
|
12808
13716
|
assert.calledWithExactly(
|
|
12809
13717
|
webex.internal.llm.setDatachannelToken,
|
|
12810
13718
|
'ps-token',
|
|
12811
|
-
'llm-practice-session'
|
|
13719
|
+
'llm-practice-session',
|
|
13720
|
+
meeting.id
|
|
12812
13721
|
);
|
|
12813
13722
|
});
|
|
12814
13723
|
|
|
@@ -12826,12 +13735,14 @@ describe('plugin-meetings', () => {
|
|
|
12826
13735
|
assert.calledWithExactly(
|
|
12827
13736
|
webex.internal.llm.setDatachannelToken,
|
|
12828
13737
|
'default-token',
|
|
12829
|
-
'llm-default-session'
|
|
13738
|
+
'llm-default-session',
|
|
13739
|
+
meeting.id
|
|
12830
13740
|
);
|
|
12831
13741
|
assert.calledWithExactly(
|
|
12832
13742
|
webex.internal.llm.setDatachannelToken,
|
|
12833
13743
|
'ps-token',
|
|
12834
|
-
'llm-practice-session'
|
|
13744
|
+
'llm-practice-session',
|
|
13745
|
+
meeting.id
|
|
12835
13746
|
);
|
|
12836
13747
|
});
|
|
12837
13748
|
|
|
@@ -12852,17 +13763,42 @@ describe('plugin-meetings', () => {
|
|
|
12852
13763
|
|
|
12853
13764
|
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
12854
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
|
+
});
|
|
12855
13781
|
});
|
|
12856
13782
|
|
|
12857
13783
|
describe('#clearDataChannelToken', () => {
|
|
12858
13784
|
beforeEach(() => {
|
|
12859
|
-
webex.internal.llm.
|
|
13785
|
+
webex.internal.llm.clearDatachannelToken = sinon.stub();
|
|
12860
13786
|
});
|
|
12861
13787
|
|
|
12862
|
-
it('
|
|
13788
|
+
it('delegates default and practice token clears to llm with meeting ownership id', () => {
|
|
12863
13789
|
meeting.clearDataChannelToken();
|
|
12864
13790
|
|
|
12865
|
-
assert.
|
|
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);
|
|
12866
13802
|
});
|
|
12867
13803
|
});
|
|
12868
13804
|
|
|
@@ -12872,6 +13808,7 @@ describe('plugin-meetings', () => {
|
|
|
12872
13808
|
webex.internal.llm.getLocusUrl = sinon.stub();
|
|
12873
13809
|
webex.internal.llm.getDatachannelUrl = sinon.stub();
|
|
12874
13810
|
webex.internal.llm.registerAndConnect = sinon.stub().resolves('something');
|
|
13811
|
+
webex.internal.llm.setRefreshHandler = sinon.stub();
|
|
12875
13812
|
webex.internal.llm.disconnectLLM = sinon.stub().resolves();
|
|
12876
13813
|
webex.internal.llm.on = sinon.stub();
|
|
12877
13814
|
webex.internal.llm.off = sinon.stub();
|
|
@@ -12889,7 +13826,7 @@ describe('plugin-meetings', () => {
|
|
|
12889
13826
|
meeting.joinedWith = {state: 'any other state'};
|
|
12890
13827
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
12891
13828
|
|
|
12892
|
-
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13829
|
+
meeting.locusInfo = {syncAllHashTreeDatasets: sinon.stub().resolves(), url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
12893
13830
|
|
|
12894
13831
|
const result = await meeting.updateLLMConnection();
|
|
12895
13832
|
|
|
@@ -12901,6 +13838,7 @@ describe('plugin-meetings', () => {
|
|
|
12901
13838
|
it('returns undefined if llm is already connected and the locus url is unchanged', async () => {
|
|
12902
13839
|
meeting.joinedWith = {state: 'JOINED'};
|
|
12903
13840
|
meeting.locusInfo = {
|
|
13841
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
12904
13842
|
url: 'a url',
|
|
12905
13843
|
info: {datachannelUrl: 'a datachannel url'},
|
|
12906
13844
|
};
|
|
@@ -12937,7 +13875,7 @@ describe('plugin-meetings', () => {
|
|
|
12937
13875
|
});
|
|
12938
13876
|
it('connects if not already connected', async () => {
|
|
12939
13877
|
meeting.joinedWith = {state: 'JOINED'};
|
|
12940
|
-
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13878
|
+
meeting.locusInfo = {syncAllHashTreeDatasets: sinon.stub().resolves(), url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
12941
13879
|
|
|
12942
13880
|
const result = await meeting.updateLLMConnection();
|
|
12943
13881
|
|
|
@@ -12948,7 +13886,14 @@ describe('plugin-meetings', () => {
|
|
|
12948
13886
|
'a datachannel url',
|
|
12949
13887
|
undefined
|
|
12950
13888
|
);
|
|
13889
|
+
assert.calledOnceWithExactly(
|
|
13890
|
+
webex.internal.llm.setRefreshHandler,
|
|
13891
|
+
sinon.match.func,
|
|
13892
|
+
'llm-default-session',
|
|
13893
|
+
meeting.id
|
|
13894
|
+
);
|
|
12951
13895
|
assert.equal(result, 'something');
|
|
13896
|
+
assert.calledOnceWithExactly(meeting.locusInfo.syncAllHashTreeDatasets, {onlyLLM: true});
|
|
12952
13897
|
});
|
|
12953
13898
|
it('disconnects if the locus url has changed', async () => {
|
|
12954
13899
|
meeting.joinedWith = {state: 'JOINED'};
|
|
@@ -12957,6 +13902,7 @@ describe('plugin-meetings', () => {
|
|
|
12957
13902
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
12958
13903
|
|
|
12959
13904
|
meeting.locusInfo = {
|
|
13905
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
12960
13906
|
url: 'a different url',
|
|
12961
13907
|
info: {datachannelUrl: 'a datachannel url'},
|
|
12962
13908
|
self: {},
|
|
@@ -12967,7 +13913,7 @@ describe('plugin-meetings', () => {
|
|
|
12967
13913
|
assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
|
|
12968
13914
|
code: 3050,
|
|
12969
13915
|
reason: 'done (permanent)',
|
|
12970
|
-
});
|
|
13916
|
+
}, 'llm-default-session', meeting.id);
|
|
12971
13917
|
|
|
12972
13918
|
assert.calledWithExactly(
|
|
12973
13919
|
webex.internal.llm.registerAndConnect,
|
|
@@ -13010,6 +13956,7 @@ describe('plugin-meetings', () => {
|
|
|
13010
13956
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13011
13957
|
|
|
13012
13958
|
meeting.locusInfo = {
|
|
13959
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
13013
13960
|
url: 'a url',
|
|
13014
13961
|
info: {datachannelUrl: 'a different datachannel url'},
|
|
13015
13962
|
self: {},
|
|
@@ -13020,7 +13967,7 @@ describe('plugin-meetings', () => {
|
|
|
13020
13967
|
assert.calledWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13021
13968
|
code: 3050,
|
|
13022
13969
|
reason: 'done (permanent)',
|
|
13023
|
-
});
|
|
13970
|
+
}, 'llm-default-session', meeting.id);
|
|
13024
13971
|
|
|
13025
13972
|
assert.calledWithExactly(
|
|
13026
13973
|
webex.internal.llm.registerAndConnect,
|
|
@@ -13061,14 +14008,14 @@ describe('plugin-meetings', () => {
|
|
|
13061
14008
|
webex.internal.llm.isConnected.returns(true);
|
|
13062
14009
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13063
14010
|
|
|
13064
|
-
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
14011
|
+
meeting.locusInfo = {syncAllHashTreeDatasets: sinon.stub().resolves(), url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13065
14012
|
|
|
13066
14013
|
const result = await meeting.updateLLMConnection();
|
|
13067
14014
|
|
|
13068
14015
|
assert.calledWith(webex.internal.llm.disconnectLLM, {
|
|
13069
14016
|
code: 3050,
|
|
13070
14017
|
reason: 'done (permanent)',
|
|
13071
|
-
});
|
|
14018
|
+
}, 'llm-default-session', meeting.id);
|
|
13072
14019
|
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
13073
14020
|
assert.equal(result, undefined);
|
|
13074
14021
|
assert.isFalse(
|
|
@@ -13084,6 +14031,7 @@ describe('plugin-meetings', () => {
|
|
|
13084
14031
|
webex.internal.llm.disconnectLLM.rejects(disconnectError);
|
|
13085
14032
|
|
|
13086
14033
|
meeting.locusInfo = {
|
|
14034
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
13087
14035
|
url: 'a different url',
|
|
13088
14036
|
info: {datachannelUrl: 'a datachannel url'},
|
|
13089
14037
|
self: {},
|
|
@@ -13115,6 +14063,7 @@ describe('plugin-meetings', () => {
|
|
|
13115
14063
|
it('still need connect main session data channel when PS started', async () => {
|
|
13116
14064
|
meeting.joinedWith = {state: 'JOINED'};
|
|
13117
14065
|
meeting.locusInfo = {
|
|
14066
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
13118
14067
|
url: 'a url',
|
|
13119
14068
|
info: {
|
|
13120
14069
|
datachannelUrl: 'a datachannel url',
|
|
@@ -13135,11 +14084,14 @@ describe('plugin-meetings', () => {
|
|
|
13135
14084
|
it('passes dataChannelToken from LLM to registerAndConnect', async () => {
|
|
13136
14085
|
meeting.joinedWith = {state: 'JOINED'};
|
|
13137
14086
|
meeting.locusInfo = {
|
|
14087
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
13138
14088
|
url: 'a url',
|
|
13139
14089
|
info: {datachannelUrl: 'a datachannel url'},
|
|
13140
14090
|
};
|
|
13141
14091
|
|
|
13142
|
-
webex.internal.llm.getDatachannelToken
|
|
14092
|
+
webex.internal.llm.getDatachannelToken
|
|
14093
|
+
.withArgs('llm-default-session', meeting.id)
|
|
14094
|
+
.returns('token-123');
|
|
13143
14095
|
|
|
13144
14096
|
await meeting.updateLLMConnection();
|
|
13145
14097
|
|
|
@@ -13154,6 +14106,7 @@ describe('plugin-meetings', () => {
|
|
|
13154
14106
|
it('passes undefined token when LLM has no token stored', async () => {
|
|
13155
14107
|
meeting.joinedWith = {state: 'JOINED'};
|
|
13156
14108
|
meeting.locusInfo = {
|
|
14109
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
13157
14110
|
url: 'a url',
|
|
13158
14111
|
info: {datachannelUrl: 'a datachannel url'},
|
|
13159
14112
|
};
|
|
@@ -13175,6 +14128,7 @@ describe('plugin-meetings', () => {
|
|
|
13175
14128
|
it('does not pass token when data channel with jwt token is disabled', async () => {
|
|
13176
14129
|
meeting.joinedWith = {state: 'JOINED'};
|
|
13177
14130
|
meeting.locusInfo = {
|
|
14131
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
13178
14132
|
url: 'a url',
|
|
13179
14133
|
info: {datachannelUrl: 'a datachannel url'},
|
|
13180
14134
|
};
|
|
@@ -13193,6 +14147,197 @@ describe('plugin-meetings', () => {
|
|
|
13193
14147
|
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
13194
14148
|
});
|
|
13195
14149
|
|
|
14150
|
+
describe('ownership tag', () => {
|
|
14151
|
+
beforeEach(() => {
|
|
14152
|
+
// Make the owner stub dynamic so setOwnerMeetingId() writes
|
|
14153
|
+
// propagate back to getOwnerMeetingId() reads. This mirrors the
|
|
14154
|
+
// real LLM singleton behavior so the finally-block release in
|
|
14155
|
+
// cleanupLLMConneciton is reflected in subsequent reads.
|
|
14156
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub().returns(undefined);
|
|
14157
|
+
webex.internal.llm.setOwnerMeetingId = sinon.stub().callsFake((id) => {
|
|
14158
|
+
webex.internal.llm.getOwnerMeetingId.returns(id);
|
|
14159
|
+
});
|
|
14160
|
+
});
|
|
14161
|
+
|
|
14162
|
+
it('skips disconnect and reconnect when LLM is connected and owned by another meeting (regardless of URL)', async () => {
|
|
14163
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
14164
|
+
webex.internal.llm.isConnected.returns(true);
|
|
14165
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
14166
|
+
// Locus/datachannel URL mismatch is the *normal* case when
|
|
14167
|
+
// another meeting owns the live socket -- each meeting has its
|
|
14168
|
+
// own locus URL. URL mismatch must NOT trigger a reclaim,
|
|
14169
|
+
// because doing so would tear down the owning meeting's healthy
|
|
14170
|
+
// LLM socket and break its data channel.
|
|
14171
|
+
webex.internal.llm.getLocusUrl.returns('owner-locus-url');
|
|
14172
|
+
webex.internal.llm.getDatachannelUrl.returns('owner-dc-url');
|
|
14173
|
+
meeting.locusInfo = {
|
|
14174
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
14175
|
+
url: 'a different url',
|
|
14176
|
+
info: {datachannelUrl: 'a different datachannel url'},
|
|
14177
|
+
self: {},
|
|
14178
|
+
};
|
|
14179
|
+
|
|
14180
|
+
const result = await meeting.updateLLMConnection();
|
|
14181
|
+
|
|
14182
|
+
assert.equal(result, undefined);
|
|
14183
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
14184
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
14185
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
14186
|
+
assert.notCalled(meeting.startLLMHealthCheckTimer);
|
|
14187
|
+
});
|
|
14188
|
+
|
|
14189
|
+
|
|
14190
|
+
it('clears stale owner tag in cleanup finally block even when disconnectLLM rejects', async () => {
|
|
14191
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
14192
|
+
webex.internal.llm.isConnected.returns(true);
|
|
14193
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
14194
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
14195
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
14196
|
+
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
14197
|
+
meeting.locusInfo = {
|
|
14198
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
14199
|
+
url: 'a different url',
|
|
14200
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
14201
|
+
self: {},
|
|
14202
|
+
};
|
|
14203
|
+
|
|
14204
|
+
try {
|
|
14205
|
+
await meeting.updateLLMConnection();
|
|
14206
|
+
} catch (e) {
|
|
14207
|
+
/* updateLLMConnection may reject when cleanup throws */
|
|
14208
|
+
}
|
|
14209
|
+
|
|
14210
|
+
// The owner-eligible finally branch must release the tag so a
|
|
14211
|
+
// subsequent reconnect attempt from any meeting is not blocked.
|
|
14212
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId, undefined);
|
|
14213
|
+
});
|
|
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
|
+
|
|
14242
|
+
it('proceeds normally when LLM is connected and owned by this meeting with URL change', async () => {
|
|
14243
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
14244
|
+
webex.internal.llm.isConnected.returns(true);
|
|
14245
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
14246
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
14247
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
14248
|
+
meeting.locusInfo = {
|
|
14249
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
14250
|
+
url: 'a different url',
|
|
14251
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
14252
|
+
self: {},
|
|
14253
|
+
};
|
|
14254
|
+
|
|
14255
|
+
await meeting.updateLLMConnection();
|
|
14256
|
+
|
|
14257
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
14258
|
+
code: 3050,
|
|
14259
|
+
reason: 'done (permanent)',
|
|
14260
|
+
}, 'llm-default-session', meeting.id);
|
|
14261
|
+
assert.calledWithExactly(
|
|
14262
|
+
webex.internal.llm.registerAndConnect,
|
|
14263
|
+
'a different url',
|
|
14264
|
+
'a datachannel url',
|
|
14265
|
+
undefined
|
|
14266
|
+
);
|
|
14267
|
+
// setOwnerMeetingId is called twice: first with undefined in
|
|
14268
|
+
// cleanupLLMConneciton's finally block (so a failed disconnect
|
|
14269
|
+
// cannot leave a stale owner), then with this meeting's id
|
|
14270
|
+
// after registerAndConnect resolves.
|
|
14271
|
+
assert.calledTwice(webex.internal.llm.setOwnerMeetingId);
|
|
14272
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.firstCall, undefined);
|
|
14273
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.lastCall, meeting.id);
|
|
14274
|
+
});
|
|
14275
|
+
|
|
14276
|
+
it('claims ownership after successful registerAndConnect on initial connect', async () => {
|
|
14277
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
14278
|
+
webex.internal.llm.isConnected.returns(false);
|
|
14279
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
14280
|
+
meeting.locusInfo = {syncAllHashTreeDatasets: sinon.stub().resolves(), url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
14281
|
+
|
|
14282
|
+
await meeting.updateLLMConnection();
|
|
14283
|
+
|
|
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
|
+
);
|
|
14291
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
14292
|
+
});
|
|
14293
|
+
|
|
14294
|
+
it('proceeds to connect when LLM is not connected even if another ownerId lingers', async () => {
|
|
14295
|
+
// Defensive path: if the LLM reports not-connected but an old
|
|
14296
|
+
// ownerId is still present (e.g. race before a successful
|
|
14297
|
+
// connections.delete), this meeting can still claim a fresh
|
|
14298
|
+
// connection.
|
|
14299
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
14300
|
+
webex.internal.llm.isConnected.returns(false);
|
|
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');
|
|
14304
|
+
meeting.locusInfo = {syncAllHashTreeDatasets: sinon.stub().resolves(), url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
14305
|
+
|
|
14306
|
+
await meeting.updateLLMConnection();
|
|
14307
|
+
|
|
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
|
+
);
|
|
14337
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
14338
|
+
});
|
|
14339
|
+
});
|
|
14340
|
+
|
|
13196
14341
|
describe('#clearMeetingData', () => {
|
|
13197
14342
|
beforeEach(() => {
|
|
13198
14343
|
webex.internal.llm.isConnected = sinon.stub().returns(true);
|
|
@@ -13211,7 +14356,7 @@ describe('plugin-meetings', () => {
|
|
|
13211
14356
|
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13212
14357
|
code: 3050,
|
|
13213
14358
|
reason: 'done (permanent)',
|
|
13214
|
-
});
|
|
14359
|
+
}, 'llm-default-session', meeting.id);
|
|
13215
14360
|
assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
13216
14361
|
assert.calledWithExactly(
|
|
13217
14362
|
webex.internal.llm.off,
|
|
@@ -13224,10 +14369,13 @@ describe('plugin-meetings', () => {
|
|
|
13224
14369
|
meeting.processLocusLLMEvent
|
|
13225
14370
|
);
|
|
13226
14371
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13227
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13228
|
-
assert.isUndefined(meeting.transcription);
|
|
13229
14372
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13230
|
-
|
|
14373
|
+
// stopTranscription and annotation.deregisterEvents are not
|
|
14374
|
+
// called here: they run in stopListeningForMeetingEvents()
|
|
14375
|
+
// before /leave to avoid double-emitting
|
|
14376
|
+
// MEETING_STOPPED_RECEIVING_TRANSCRIPTION.
|
|
14377
|
+
assert.notCalled(meeting.stopTranscription);
|
|
14378
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13231
14379
|
});
|
|
13232
14380
|
it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
|
|
13233
14381
|
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
@@ -13246,19 +14394,65 @@ describe('plugin-meetings', () => {
|
|
|
13246
14394
|
meeting.processLocusLLMEvent
|
|
13247
14395
|
);
|
|
13248
14396
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13249
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13250
|
-
assert.isUndefined(meeting.transcription);
|
|
13251
14397
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13252
|
-
assert.
|
|
14398
|
+
assert.notCalled(meeting.stopTranscription);
|
|
14399
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13253
14400
|
});
|
|
13254
|
-
it('always calls stopTranscription even when transcription is undefined', async () => {
|
|
13255
|
-
meeting.transcription = undefined;
|
|
13256
14401
|
|
|
13257
|
-
|
|
14402
|
+
describe('ownership tag', () => {
|
|
14403
|
+
beforeEach(() => {
|
|
14404
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub();
|
|
14405
|
+
});
|
|
13258
14406
|
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
14407
|
+
it('skips disconnectLLM but still removes this meeting listeners when another meeting owns the LLM', async () => {
|
|
14408
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
14409
|
+
|
|
14410
|
+
await meeting.clearMeetingData();
|
|
14411
|
+
|
|
14412
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
14413
|
+
// clearDataChannelToken is always delegated; llm enforces
|
|
14414
|
+
// ownership and no-ops for non-owners internally.
|
|
14415
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
14416
|
+
// Listeners owned by *this* Meeting instance must still be
|
|
14417
|
+
// removed so a leaving subordinate meeting stops receiving
|
|
14418
|
+
// relay/locus events from the shared singleton.
|
|
14419
|
+
assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
14420
|
+
assert.calledWithExactly(
|
|
14421
|
+
webex.internal.llm.off,
|
|
14422
|
+
'event:relay.event',
|
|
14423
|
+
meeting.processRelayEvent
|
|
14424
|
+
);
|
|
14425
|
+
assert.calledWithExactly(
|
|
14426
|
+
webex.internal.llm.off,
|
|
14427
|
+
'event:locus.state_message',
|
|
14428
|
+
meeting.processLocusLLMEvent
|
|
14429
|
+
);
|
|
14430
|
+
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
14431
|
+
});
|
|
14432
|
+
|
|
14433
|
+
it('calls disconnectLLM and clears data channel token when this meeting is the owner', async () => {
|
|
14434
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
14435
|
+
|
|
14436
|
+
await meeting.clearMeetingData();
|
|
14437
|
+
|
|
14438
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
14439
|
+
code: 3050,
|
|
14440
|
+
reason: 'done (permanent)',
|
|
14441
|
+
}, 'llm-default-session', meeting.id);
|
|
14442
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
14443
|
+
});
|
|
14444
|
+
|
|
14445
|
+
it('calls disconnectLLM and clears data channel token when no owner is recorded (first-claim / legacy)', async () => {
|
|
14446
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
14447
|
+
|
|
14448
|
+
await meeting.clearMeetingData();
|
|
14449
|
+
|
|
14450
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
14451
|
+
code: 3050,
|
|
14452
|
+
reason: 'done (permanent)',
|
|
14453
|
+
}, 'llm-default-session', meeting.id);
|
|
14454
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
14455
|
+
});
|
|
13262
14456
|
});
|
|
13263
14457
|
});
|
|
13264
14458
|
});
|
|
@@ -15012,16 +16206,25 @@ describe('plugin-meetings', () => {
|
|
|
15012
16206
|
assert.notCalled(meeting.meetingRequest.sendReaction);
|
|
15013
16207
|
});
|
|
15014
16208
|
|
|
15015
|
-
it('should
|
|
16209
|
+
it('should send a custom reaction type not in the known list', async () => {
|
|
15016
16210
|
meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
|
|
15017
16211
|
|
|
15018
|
-
|
|
15019
|
-
meeting.sendReaction('invalid_reaction', 'light'),
|
|
15020
|
-
Error,
|
|
15021
|
-
'invalid_reaction is not a valid reaction.'
|
|
15022
|
-
);
|
|
16212
|
+
const reactionPromise = meeting.sendReaction('custom_reaction', 'light');
|
|
15023
16213
|
|
|
15024
|
-
assert.
|
|
16214
|
+
assert.exists(reactionPromise.then);
|
|
16215
|
+
await reactionPromise;
|
|
16216
|
+
assert.calledOnceWithExactly(meeting.meetingRequest.sendReaction, {
|
|
16217
|
+
reactionChannelUrl: 'Fake URL',
|
|
16218
|
+
reaction: {
|
|
16219
|
+
type: 'custom_reaction',
|
|
16220
|
+
tone: {
|
|
16221
|
+
type: 'light_skin_tone',
|
|
16222
|
+
codepoints: '1F3FB',
|
|
16223
|
+
shortcodes: ':skin-tone-2:',
|
|
16224
|
+
},
|
|
16225
|
+
},
|
|
16226
|
+
participantId: meeting.members.selfId,
|
|
16227
|
+
});
|
|
15025
16228
|
});
|
|
15026
16229
|
|
|
15027
16230
|
it('should send a reaction with default skin tone if provided skinToneType is invalid ', async () => {
|
|
@@ -16045,4 +17248,4 @@ describe('plugin-meetings', () => {
|
|
|
16045
17248
|
assert.calledOnceWithExactly(meeting.meetingRequest.cancelSipCallOut, participantId);
|
|
16046
17249
|
});
|
|
16047
17250
|
});
|
|
16048
|
-
});
|
|
17251
|
+
});
|