@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.51
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 +6 -2
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +6 -3
- 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 +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +646 -371
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.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/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 +289 -86
- 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/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 +842 -521
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +19 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +205 -77
- 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 +67 -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 +2 -1
- package/dist/metrics/constants.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 +1 -0
- package/dist/types/constants.d.ts +2 -0
- 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 +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +83 -16
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +46 -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 +70 -1
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +20 -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 +1 -0
- package/dist/webinar/index.js +361 -235
- 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 +2 -1
- package/src/config.ts +1 -0
- package/src/constants.ts +5 -1
- 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 +9 -0
- package/src/hashTree/hashTreeParser.ts +362 -174
- package/src/hashTree/utils.ts +17 -0
- package/src/index.ts +5 -0
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +25 -8
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +291 -93
- package/src/locus-info/types.ts +25 -1
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +315 -26
- package/src/meeting/util.ts +20 -2
- package/src/meetings/index.ts +109 -43
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +80 -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 +1 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +162 -21
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +7 -3
- 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 +1341 -140
- package/test/unit/spec/hashTree/utils.ts +88 -1
- 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 +475 -81
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +836 -41
- package/test/unit/spec/meeting/muteState.js +3 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meetings/index.js +309 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +161 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +141 -16
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
ONLINE,
|
|
35
35
|
OFFLINE,
|
|
36
36
|
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
|
|
37
|
+
LOCUS_LLM_EVENT,
|
|
38
|
+
RECORDING_STATE,
|
|
37
39
|
} from '@webex/plugin-meetings/src/constants';
|
|
38
40
|
import {
|
|
39
41
|
ConnectionState,
|
|
@@ -1982,11 +1984,12 @@ describe('plugin-meetings', () => {
|
|
|
1982
1984
|
describe('#handleLLMOnline', () => {
|
|
1983
1985
|
beforeEach(() => {
|
|
1984
1986
|
webex.internal.llm.off = sinon.stub();
|
|
1987
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
|
|
1988
|
+
webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
|
|
1985
1989
|
});
|
|
1986
1990
|
|
|
1987
|
-
it('
|
|
1991
|
+
it('emits transcription connected events', () => {
|
|
1988
1992
|
meeting.handleLLMOnline();
|
|
1989
|
-
assert.calledOnceWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
1990
1993
|
assert.calledWith(
|
|
1991
1994
|
TriggerProxy.trigger,
|
|
1992
1995
|
sinon.match.instanceOf(Meeting),
|
|
@@ -1997,6 +2000,24 @@ describe('plugin-meetings', () => {
|
|
|
1997
2000
|
EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
|
|
1998
2001
|
);
|
|
1999
2002
|
});
|
|
2003
|
+
|
|
2004
|
+
it('restores transcription subscription when caption intent is enabled', () => {
|
|
2005
|
+
webex.internal.voicea.getIsCaptionBoxOn.returns(true);
|
|
2006
|
+
|
|
2007
|
+
meeting.handleLLMOnline();
|
|
2008
|
+
|
|
2009
|
+
assert.calledOnceWithExactly(webex.internal.voicea.updateSubchannelSubscriptions, {
|
|
2010
|
+
subscribe: ['transcription'],
|
|
2011
|
+
});
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
it('does not restore transcription subscription when caption intent is disabled', () => {
|
|
2015
|
+
webex.internal.voicea.getIsCaptionBoxOn.returns(false);
|
|
2016
|
+
|
|
2017
|
+
meeting.handleLLMOnline();
|
|
2018
|
+
|
|
2019
|
+
assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
|
|
2020
|
+
});
|
|
2000
2021
|
});
|
|
2001
2022
|
|
|
2002
2023
|
describe('#join', () => {
|
|
@@ -2016,6 +2037,7 @@ describe('plugin-meetings', () => {
|
|
|
2016
2037
|
it('should have #join', () => {
|
|
2017
2038
|
assert.exists(meeting.join);
|
|
2018
2039
|
});
|
|
2040
|
+
|
|
2019
2041
|
beforeEach(() => {
|
|
2020
2042
|
setCorrelationIdSpy = sinon.spy(meeting, 'setCorrelationId');
|
|
2021
2043
|
meeting.setLocus = sinon.stub().returns(true);
|
|
@@ -2169,7 +2191,6 @@ describe('plugin-meetings', () => {
|
|
|
2169
2191
|
await meeting.join().catch(() => {
|
|
2170
2192
|
assert.calledOnce(MeetingUtil.joinMeeting);
|
|
2171
2193
|
|
|
2172
|
-
// Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
|
|
2173
2194
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
2174
2195
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
2175
2196
|
name: 'client.call.initiated',
|
|
@@ -2201,6 +2222,7 @@ describe('plugin-meetings', () => {
|
|
|
2201
2222
|
});
|
|
2202
2223
|
});
|
|
2203
2224
|
});
|
|
2225
|
+
|
|
2204
2226
|
describe('lmm, transcription & permissionTokenRefresh decoupling', () => {
|
|
2205
2227
|
beforeEach(() => {
|
|
2206
2228
|
sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
|
|
@@ -2271,7 +2293,6 @@ describe('plugin-meetings', () => {
|
|
|
2271
2293
|
const locusInfoParseStub = sinon.stub(meeting.locusInfo, 'parse');
|
|
2272
2294
|
sinon.stub(meeting, 'isJoined').returns(true);
|
|
2273
2295
|
|
|
2274
|
-
// Set up llm.on stub to capture the registered listener when updateLLMConnection is called
|
|
2275
2296
|
let locusLLMEventListener;
|
|
2276
2297
|
meeting.webex.internal.llm.on = sinon.stub().callsFake((eventName, callback) => {
|
|
2277
2298
|
if (eventName === 'event:locus.state_message') {
|
|
@@ -2280,16 +2301,12 @@ describe('plugin-meetings', () => {
|
|
|
2280
2301
|
});
|
|
2281
2302
|
meeting.webex.internal.llm.off = sinon.stub();
|
|
2282
2303
|
|
|
2283
|
-
// we need the real meeting.updateLLMConnection not the mock
|
|
2284
2304
|
meeting.updateLLMConnection.restore();
|
|
2285
2305
|
|
|
2286
|
-
// Call updateLLMConnection to register the listener
|
|
2287
2306
|
await meeting.updateLLMConnection();
|
|
2288
2307
|
|
|
2289
|
-
// Verify the listener was registered and we captured it
|
|
2290
2308
|
assert.isDefined(locusLLMEventListener, 'LLM event listener should be registered');
|
|
2291
2309
|
|
|
2292
|
-
// Now trigger the event
|
|
2293
2310
|
const eventData = {
|
|
2294
2311
|
eventType: 'locus.state_message',
|
|
2295
2312
|
stateElementsMessage: {
|
|
@@ -2309,13 +2326,10 @@ describe('plugin-meetings', () => {
|
|
|
2309
2326
|
sinon.stub(meeting.webex.internal.llm, 'hasEverConnected').value(true);
|
|
2310
2327
|
sinon.stub(meeting.webex.internal.llm, 'registerAndConnect').resolves({});
|
|
2311
2328
|
|
|
2312
|
-
// Restore the real updateLLMConnection
|
|
2313
2329
|
meeting.updateLLMConnection.restore();
|
|
2314
2330
|
|
|
2315
|
-
// Call updateLLMConnection to start the timer
|
|
2316
2331
|
await meeting.updateLLMConnection();
|
|
2317
2332
|
|
|
2318
|
-
// Fast forward time by 3 minutes
|
|
2319
2333
|
fakeClock.tick(3 * 60 * 1000);
|
|
2320
2334
|
|
|
2321
2335
|
assert.calledWith(
|
|
@@ -2340,18 +2354,14 @@ describe('plugin-meetings', () => {
|
|
|
2340
2354
|
.stub(meeting.webex.internal.llm, 'getDatachannelUrl')
|
|
2341
2355
|
.returns('https://datachannel1.example.com');
|
|
2342
2356
|
|
|
2343
|
-
// Restore the real updateLLMConnection
|
|
2344
2357
|
meeting.updateLLMConnection.restore();
|
|
2345
2358
|
|
|
2346
|
-
// First, connect LLM and start the timer
|
|
2347
2359
|
isJoinedStub.returns(true);
|
|
2348
2360
|
meeting.webex.internal.llm.isConnected.returns(false);
|
|
2349
2361
|
await meeting.updateLLMConnection();
|
|
2350
2362
|
|
|
2351
|
-
// Verify timer was started
|
|
2352
2363
|
assert.exists(meeting.llmHealthCheckTimer);
|
|
2353
2364
|
|
|
2354
|
-
// Now simulate that we're no longer joined
|
|
2355
2365
|
isJoinedStub.returns(false);
|
|
2356
2366
|
meeting.webex.internal.llm.isConnected.returns(true);
|
|
2357
2367
|
|
|
@@ -2359,10 +2369,8 @@ describe('plugin-meetings', () => {
|
|
|
2359
2369
|
|
|
2360
2370
|
assert.calledOnce(meeting.webex.internal.llm.disconnectLLM);
|
|
2361
2371
|
|
|
2362
|
-
// Verify the timer was cleared (should be undefined)
|
|
2363
2372
|
assert.isUndefined(meeting.llmHealthCheckTimer);
|
|
2364
2373
|
|
|
2365
|
-
// Fast forward time to ensure no metric is sent
|
|
2366
2374
|
Metrics.sendBehavioralMetric.resetHistory();
|
|
2367
2375
|
fakeClock.tick(3 * 60 * 1000);
|
|
2368
2376
|
|
|
@@ -2397,7 +2405,6 @@ describe('plugin-meetings', () => {
|
|
|
2397
2405
|
.stub()
|
|
2398
2406
|
.rejects(new CaptchaError('bad captcha'));
|
|
2399
2407
|
const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
|
|
2400
|
-
const joinMeetingOptionsSpy = sinon.spy(MeetingUtil, 'joinMeetingOptions');
|
|
2401
2408
|
|
|
2402
2409
|
try {
|
|
2403
2410
|
await meeting.join();
|
|
@@ -2411,8 +2418,7 @@ describe('plugin-meetings', () => {
|
|
|
2411
2418
|
);
|
|
2412
2419
|
assert.instanceOf(error, CaptchaError);
|
|
2413
2420
|
assert.equal(error.message, 'bad captcha');
|
|
2414
|
-
|
|
2415
|
-
assert.notCalled(joinMeetingOptionsSpy);
|
|
2421
|
+
assert.notCalled(MeetingUtil.joinMeeting);
|
|
2416
2422
|
}
|
|
2417
2423
|
});
|
|
2418
2424
|
|
|
@@ -2421,7 +2427,6 @@ describe('plugin-meetings', () => {
|
|
|
2421
2427
|
.stub()
|
|
2422
2428
|
.rejects(new PasswordError('bad password'));
|
|
2423
2429
|
const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
|
|
2424
|
-
const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
|
|
2425
2430
|
|
|
2426
2431
|
try {
|
|
2427
2432
|
await meeting.join();
|
|
@@ -2435,8 +2440,7 @@ describe('plugin-meetings', () => {
|
|
|
2435
2440
|
);
|
|
2436
2441
|
assert.instanceOf(error, PasswordError);
|
|
2437
2442
|
assert.equal(error.message, 'bad password');
|
|
2438
|
-
|
|
2439
|
-
assert.notCalled(joinMeetingOptionsSpy);
|
|
2443
|
+
assert.notCalled(MeetingUtil.joinMeeting);
|
|
2440
2444
|
}
|
|
2441
2445
|
});
|
|
2442
2446
|
|
|
@@ -2445,7 +2449,6 @@ describe('plugin-meetings', () => {
|
|
|
2445
2449
|
.stub()
|
|
2446
2450
|
.rejects(new PermissionError('bad permission'));
|
|
2447
2451
|
const stateMachineFailSpy = sinon.spy(meeting.meetingFiniteStateMachine, 'fail');
|
|
2448
|
-
const joinMeetingOptionsSpy = sinon.spy(MeetingUtil.joinMeetingOptions);
|
|
2449
2452
|
|
|
2450
2453
|
try {
|
|
2451
2454
|
await meeting.join();
|
|
@@ -2459,14 +2462,14 @@ describe('plugin-meetings', () => {
|
|
|
2459
2462
|
);
|
|
2460
2463
|
assert.instanceOf(error, PermissionError);
|
|
2461
2464
|
assert.equal(error.message, 'bad permission');
|
|
2462
|
-
|
|
2463
|
-
assert.notCalled(joinMeetingOptionsSpy);
|
|
2465
|
+
assert.notCalled(MeetingUtil.joinMeeting);
|
|
2464
2466
|
}
|
|
2465
2467
|
});
|
|
2466
2468
|
});
|
|
2467
2469
|
});
|
|
2468
2470
|
});
|
|
2469
2471
|
|
|
2472
|
+
|
|
2470
2473
|
describe('#addMedia', () => {
|
|
2471
2474
|
const muteStateStub = {
|
|
2472
2475
|
handleClientRequest: sinon.stub().returns(Promise.resolve(true)),
|
|
@@ -4533,6 +4536,297 @@ describe('plugin-meetings', () => {
|
|
|
4533
4536
|
},
|
|
4534
4537
|
});
|
|
4535
4538
|
});
|
|
4539
|
+
|
|
4540
|
+
describe('handles STATS_UPDATE event for SRTP cipher detection', () => {
|
|
4541
|
+
it('emits MEETING_SRTP_CIPHER_UPDATED event when srtpCipher is found in transport stats', async () => {
|
|
4542
|
+
const fakeStats = new Map([
|
|
4543
|
+
[
|
|
4544
|
+
'transport-1',
|
|
4545
|
+
{
|
|
4546
|
+
type: 'transport',
|
|
4547
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4548
|
+
dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
|
4549
|
+
},
|
|
4550
|
+
],
|
|
4551
|
+
[
|
|
4552
|
+
'outbound-rtp-1',
|
|
4553
|
+
{
|
|
4554
|
+
type: 'outbound-rtp',
|
|
4555
|
+
ssrc: 12345,
|
|
4556
|
+
},
|
|
4557
|
+
],
|
|
4558
|
+
]);
|
|
4559
|
+
|
|
4560
|
+
statsAnalyzerStub.emit(
|
|
4561
|
+
{file: 'test', function: 'test'},
|
|
4562
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4563
|
+
{stats: fakeStats}
|
|
4564
|
+
);
|
|
4565
|
+
|
|
4566
|
+
assert.calledWith(
|
|
4567
|
+
TriggerProxy.trigger,
|
|
4568
|
+
sinon.match.instanceOf(Meeting),
|
|
4569
|
+
{
|
|
4570
|
+
file: 'meeting/index',
|
|
4571
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4572
|
+
},
|
|
4573
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4574
|
+
{srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
|
|
4575
|
+
);
|
|
4576
|
+
|
|
4577
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4578
|
+
});
|
|
4579
|
+
|
|
4580
|
+
it('updates meeting.mediaProperties.srtpCipher when cipher changes', async () => {
|
|
4581
|
+
const firstStats = new Map([
|
|
4582
|
+
[
|
|
4583
|
+
'transport-1',
|
|
4584
|
+
{
|
|
4585
|
+
type: 'transport',
|
|
4586
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4587
|
+
},
|
|
4588
|
+
],
|
|
4589
|
+
]);
|
|
4590
|
+
|
|
4591
|
+
statsAnalyzerStub.emit(
|
|
4592
|
+
{file: 'test', function: 'test'},
|
|
4593
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4594
|
+
{stats: firstStats}
|
|
4595
|
+
);
|
|
4596
|
+
|
|
4597
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4598
|
+
|
|
4599
|
+
const secondStats = new Map([
|
|
4600
|
+
[
|
|
4601
|
+
'transport-1',
|
|
4602
|
+
{
|
|
4603
|
+
type: 'transport',
|
|
4604
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
4605
|
+
},
|
|
4606
|
+
],
|
|
4607
|
+
]);
|
|
4608
|
+
|
|
4609
|
+
TriggerProxy.trigger.resetHistory();
|
|
4610
|
+
|
|
4611
|
+
statsAnalyzerStub.emit(
|
|
4612
|
+
{file: 'test', function: 'test'},
|
|
4613
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4614
|
+
{stats: secondStats}
|
|
4615
|
+
);
|
|
4616
|
+
|
|
4617
|
+
assert.calledWith(
|
|
4618
|
+
TriggerProxy.trigger,
|
|
4619
|
+
sinon.match.instanceOf(Meeting),
|
|
4620
|
+
{
|
|
4621
|
+
file: 'meeting/index',
|
|
4622
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4623
|
+
},
|
|
4624
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4625
|
+
{srtpCipher: 'AEAD_AES_256_GCM'}
|
|
4626
|
+
);
|
|
4627
|
+
|
|
4628
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AEAD_AES_256_GCM');
|
|
4629
|
+
});
|
|
4630
|
+
|
|
4631
|
+
it('does not emit event when srtpCipher has not changed', async () => {
|
|
4632
|
+
const firstStats = new Map([
|
|
4633
|
+
[
|
|
4634
|
+
'transport-1',
|
|
4635
|
+
{
|
|
4636
|
+
type: 'transport',
|
|
4637
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4638
|
+
},
|
|
4639
|
+
],
|
|
4640
|
+
]);
|
|
4641
|
+
|
|
4642
|
+
statsAnalyzerStub.emit(
|
|
4643
|
+
{file: 'test', function: 'test'},
|
|
4644
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4645
|
+
{stats: firstStats}
|
|
4646
|
+
);
|
|
4647
|
+
|
|
4648
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4649
|
+
|
|
4650
|
+
TriggerProxy.trigger.resetHistory();
|
|
4651
|
+
|
|
4652
|
+
// Emit same cipher again
|
|
4653
|
+
statsAnalyzerStub.emit(
|
|
4654
|
+
{file: 'test', function: 'test'},
|
|
4655
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4656
|
+
{stats: firstStats}
|
|
4657
|
+
);
|
|
4658
|
+
|
|
4659
|
+
// Should not trigger event again
|
|
4660
|
+
assert.neverCalledWith(
|
|
4661
|
+
TriggerProxy.trigger,
|
|
4662
|
+
sinon.match.instanceOf(Meeting),
|
|
4663
|
+
sinon.match.any,
|
|
4664
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4665
|
+
sinon.match.any
|
|
4666
|
+
);
|
|
4667
|
+
|
|
4668
|
+
// Cipher should remain the same
|
|
4669
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4670
|
+
});
|
|
4671
|
+
|
|
4672
|
+
it('does not emit event when stats contain no transport with srtpCipher', async () => {
|
|
4673
|
+
const fakeStats = new Map([
|
|
4674
|
+
[
|
|
4675
|
+
'outbound-rtp-1',
|
|
4676
|
+
{
|
|
4677
|
+
type: 'outbound-rtp',
|
|
4678
|
+
ssrc: 12345,
|
|
4679
|
+
},
|
|
4680
|
+
],
|
|
4681
|
+
[
|
|
4682
|
+
'inbound-rtp-1',
|
|
4683
|
+
{
|
|
4684
|
+
type: 'inbound-rtp',
|
|
4685
|
+
ssrc: 67890,
|
|
4686
|
+
},
|
|
4687
|
+
],
|
|
4688
|
+
]);
|
|
4689
|
+
|
|
4690
|
+
statsAnalyzerStub.emit(
|
|
4691
|
+
{file: 'test', function: 'test'},
|
|
4692
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4693
|
+
{stats: fakeStats}
|
|
4694
|
+
);
|
|
4695
|
+
|
|
4696
|
+
assert.neverCalledWith(
|
|
4697
|
+
TriggerProxy.trigger,
|
|
4698
|
+
sinon.match.instanceOf(Meeting),
|
|
4699
|
+
sinon.match.any,
|
|
4700
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4701
|
+
sinon.match.any
|
|
4702
|
+
);
|
|
4703
|
+
|
|
4704
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
4705
|
+
});
|
|
4706
|
+
|
|
4707
|
+
it('does not emit event when transport stat has no srtpCipher property', async () => {
|
|
4708
|
+
const fakeStats = new Map([
|
|
4709
|
+
[
|
|
4710
|
+
'transport-1',
|
|
4711
|
+
{
|
|
4712
|
+
type: 'transport',
|
|
4713
|
+
dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
|
4714
|
+
// no srtpCipher property
|
|
4715
|
+
},
|
|
4716
|
+
],
|
|
4717
|
+
]);
|
|
4718
|
+
|
|
4719
|
+
statsAnalyzerStub.emit(
|
|
4720
|
+
{file: 'test', function: 'test'},
|
|
4721
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4722
|
+
{stats: fakeStats}
|
|
4723
|
+
);
|
|
4724
|
+
|
|
4725
|
+
assert.neverCalledWith(
|
|
4726
|
+
TriggerProxy.trigger,
|
|
4727
|
+
sinon.match.instanceOf(Meeting),
|
|
4728
|
+
sinon.match.any,
|
|
4729
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4730
|
+
sinon.match.any
|
|
4731
|
+
);
|
|
4732
|
+
|
|
4733
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
4734
|
+
});
|
|
4735
|
+
|
|
4736
|
+
it('uses first transport with srtpCipher when multiple transports exist', async () => {
|
|
4737
|
+
const fakeStats = new Map([
|
|
4738
|
+
[
|
|
4739
|
+
'transport-1',
|
|
4740
|
+
{
|
|
4741
|
+
type: 'transport',
|
|
4742
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4743
|
+
},
|
|
4744
|
+
],
|
|
4745
|
+
[
|
|
4746
|
+
'transport-2',
|
|
4747
|
+
{
|
|
4748
|
+
type: 'transport',
|
|
4749
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
4750
|
+
},
|
|
4751
|
+
],
|
|
4752
|
+
[
|
|
4753
|
+
'outbound-rtp-1',
|
|
4754
|
+
{
|
|
4755
|
+
type: 'outbound-rtp',
|
|
4756
|
+
ssrc: 12345,
|
|
4757
|
+
},
|
|
4758
|
+
],
|
|
4759
|
+
]);
|
|
4760
|
+
|
|
4761
|
+
statsAnalyzerStub.emit(
|
|
4762
|
+
{file: 'test', function: 'test'},
|
|
4763
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4764
|
+
{stats: fakeStats}
|
|
4765
|
+
);
|
|
4766
|
+
|
|
4767
|
+
assert.calledWith(
|
|
4768
|
+
TriggerProxy.trigger,
|
|
4769
|
+
sinon.match.instanceOf(Meeting),
|
|
4770
|
+
{
|
|
4771
|
+
file: 'meeting/index',
|
|
4772
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4773
|
+
},
|
|
4774
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4775
|
+
{srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
|
|
4776
|
+
);
|
|
4777
|
+
|
|
4778
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4779
|
+
});
|
|
4780
|
+
|
|
4781
|
+
it('handles empty stats map without errors', async () => {
|
|
4782
|
+
const emptyStats = new Map();
|
|
4783
|
+
|
|
4784
|
+
statsAnalyzerStub.emit(
|
|
4785
|
+
{file: 'test', function: 'test'},
|
|
4786
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4787
|
+
{stats: emptyStats}
|
|
4788
|
+
);
|
|
4789
|
+
|
|
4790
|
+
assert.neverCalledWith(
|
|
4791
|
+
TriggerProxy.trigger,
|
|
4792
|
+
sinon.match.instanceOf(Meeting),
|
|
4793
|
+
sinon.match.any,
|
|
4794
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4795
|
+
sinon.match.any
|
|
4796
|
+
);
|
|
4797
|
+
|
|
4798
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
4799
|
+
});
|
|
4800
|
+
|
|
4801
|
+
it('logs cipher change when cipher is updated', async () => {
|
|
4802
|
+
const loggerSpy = sinon.spy(LoggerProxy.logger, 'info');
|
|
4803
|
+
|
|
4804
|
+
meeting.mediaProperties.srtpCipher = 'AES_CM_128_HMAC_SHA1_80';
|
|
4805
|
+
|
|
4806
|
+
const newStats = new Map([
|
|
4807
|
+
[
|
|
4808
|
+
'transport-1',
|
|
4809
|
+
{
|
|
4810
|
+
type: 'transport',
|
|
4811
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
4812
|
+
},
|
|
4813
|
+
],
|
|
4814
|
+
]);
|
|
4815
|
+
|
|
4816
|
+
statsAnalyzerStub.emit(
|
|
4817
|
+
{file: 'test', function: 'test'},
|
|
4818
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4819
|
+
{stats: newStats}
|
|
4820
|
+
);
|
|
4821
|
+
|
|
4822
|
+
assert.calledWithMatch(
|
|
4823
|
+
loggerSpy,
|
|
4824
|
+
sinon.match(/SRTP cipher changed from AES_CM_128_HMAC_SHA1_80 to AEAD_AES_256_GCM/)
|
|
4825
|
+
);
|
|
4826
|
+
|
|
4827
|
+
loggerSpy.restore();
|
|
4828
|
+
});
|
|
4829
|
+
});
|
|
4536
4830
|
});
|
|
4537
4831
|
|
|
4538
4832
|
describe('handles StatsMonitor events', () => {
|
|
@@ -6428,6 +6722,9 @@ describe('plugin-meetings', () => {
|
|
|
6428
6722
|
|
|
6429
6723
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
6430
6724
|
webex.internal.llm.off = sinon.stub();
|
|
6725
|
+
webex.internal.mercury.off = sinon.stub();
|
|
6726
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
6727
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
6431
6728
|
|
|
6432
6729
|
// A meeting needs to be joined to leave
|
|
6433
6730
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -6451,6 +6748,67 @@ describe('plugin-meetings', () => {
|
|
|
6451
6748
|
assert.calledOnce(meeting.clearMeetingData);
|
|
6452
6749
|
});
|
|
6453
6750
|
|
|
6751
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /leave', async () => {
|
|
6752
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
6753
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
6754
|
+
|
|
6755
|
+
await meeting.leave();
|
|
6756
|
+
|
|
6757
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
6758
|
+
// annotation) must be detached before the /leave request so that
|
|
6759
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
6760
|
+
// (per Locus team recommendation).
|
|
6761
|
+
assert.callOrder(
|
|
6762
|
+
webex.internal.llm.off,
|
|
6763
|
+
webex.internal.mercury.off,
|
|
6764
|
+
meeting.stopTranscription,
|
|
6765
|
+
meeting.annotation.deregisterEvents,
|
|
6766
|
+
meeting.meetingRequest.leaveMeeting
|
|
6767
|
+
);
|
|
6768
|
+
assert.calledWithExactly(
|
|
6769
|
+
webex.internal.llm.off,
|
|
6770
|
+
'event:relay.event',
|
|
6771
|
+
meeting.processRelayEvent
|
|
6772
|
+
);
|
|
6773
|
+
assert.calledWithExactly(
|
|
6774
|
+
webex.internal.llm.off,
|
|
6775
|
+
LOCUS_LLM_EVENT,
|
|
6776
|
+
meeting.processLocusLLMEvent
|
|
6777
|
+
);
|
|
6778
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
6779
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
6780
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
6781
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
6782
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
6783
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
6784
|
+
assert.isUndefined(meeting.transcription);
|
|
6785
|
+
});
|
|
6786
|
+
|
|
6787
|
+
it('tears down llm/mercury/transcription/annotation even when /leave rejects', async () => {
|
|
6788
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
6789
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
6790
|
+
meeting.meetingRequest.leaveMeeting = sinon
|
|
6791
|
+
.stub()
|
|
6792
|
+
.returns(Promise.reject(new Error('leave failed')));
|
|
6793
|
+
|
|
6794
|
+
await meeting.leave().catch(() => {});
|
|
6795
|
+
|
|
6796
|
+
assert.calledWithExactly(
|
|
6797
|
+
webex.internal.llm.off,
|
|
6798
|
+
'event:relay.event',
|
|
6799
|
+
meeting.processRelayEvent
|
|
6800
|
+
);
|
|
6801
|
+
assert.calledWithExactly(
|
|
6802
|
+
webex.internal.llm.off,
|
|
6803
|
+
LOCUS_LLM_EVENT,
|
|
6804
|
+
meeting.processLocusLLMEvent
|
|
6805
|
+
);
|
|
6806
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
6807
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
6808
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
6809
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
6810
|
+
});
|
|
6811
|
+
|
|
6454
6812
|
it('should reset call diagnostic latencies correctly', async () => {
|
|
6455
6813
|
const leave = meeting.leave();
|
|
6456
6814
|
|
|
@@ -8458,6 +8816,9 @@ describe('plugin-meetings', () => {
|
|
|
8458
8816
|
|
|
8459
8817
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
8460
8818
|
webex.internal.llm.off = sinon.stub();
|
|
8819
|
+
webex.internal.mercury.off = sinon.stub();
|
|
8820
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
8821
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
8461
8822
|
|
|
8462
8823
|
// A meeting needs to be joined to end
|
|
8463
8824
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -8480,6 +8841,66 @@ describe('plugin-meetings', () => {
|
|
|
8480
8841
|
assert.calledOnce(meeting?.unsetPeerConnections);
|
|
8481
8842
|
assert.calledOnce(meeting?.clearMeetingData);
|
|
8482
8843
|
});
|
|
8844
|
+
|
|
8845
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /end', async () => {
|
|
8846
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
8847
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
8848
|
+
|
|
8849
|
+
await meeting.endMeetingForAll();
|
|
8850
|
+
|
|
8851
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
8852
|
+
// annotation) must be detached before the /end request so that
|
|
8853
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
8854
|
+
// (per Locus team recommendation).
|
|
8855
|
+
assert.callOrder(
|
|
8856
|
+
webex.internal.llm.off,
|
|
8857
|
+
webex.internal.mercury.off,
|
|
8858
|
+
meeting.stopTranscription,
|
|
8859
|
+
meeting.annotation.deregisterEvents,
|
|
8860
|
+
meeting.meetingRequest.endMeetingForAll
|
|
8861
|
+
);
|
|
8862
|
+
assert.calledWithExactly(
|
|
8863
|
+
webex.internal.llm.off,
|
|
8864
|
+
'event:relay.event',
|
|
8865
|
+
meeting.processRelayEvent
|
|
8866
|
+
);
|
|
8867
|
+
assert.calledWithExactly(
|
|
8868
|
+
webex.internal.llm.off,
|
|
8869
|
+
LOCUS_LLM_EVENT,
|
|
8870
|
+
meeting.processLocusLLMEvent
|
|
8871
|
+
);
|
|
8872
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
8873
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
8874
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
8875
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
8876
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
8877
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
8878
|
+
});
|
|
8879
|
+
|
|
8880
|
+
it('tears down llm/mercury/transcription/annotation even when /end rejects', async () => {
|
|
8881
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
8882
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
8883
|
+
meeting.meetingRequest.endMeetingForAll = sinon
|
|
8884
|
+
.stub()
|
|
8885
|
+
.returns(Promise.reject(new Error('end failed')));
|
|
8886
|
+
|
|
8887
|
+
await meeting.endMeetingForAll().catch(() => {});
|
|
8888
|
+
|
|
8889
|
+
assert.calledWithExactly(
|
|
8890
|
+
webex.internal.llm.off,
|
|
8891
|
+
'event:relay.event',
|
|
8892
|
+
meeting.processRelayEvent
|
|
8893
|
+
);
|
|
8894
|
+
assert.calledWithExactly(
|
|
8895
|
+
webex.internal.llm.off,
|
|
8896
|
+
LOCUS_LLM_EVENT,
|
|
8897
|
+
meeting.processLocusLLMEvent
|
|
8898
|
+
);
|
|
8899
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
8900
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
8901
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
8902
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
8903
|
+
});
|
|
8483
8904
|
});
|
|
8484
8905
|
|
|
8485
8906
|
describe('#moveTo', () => {
|
|
@@ -10416,14 +10837,24 @@ describe('plugin-meetings', () => {
|
|
|
10416
10837
|
);
|
|
10417
10838
|
done();
|
|
10418
10839
|
});
|
|
10419
|
-
it('listens to the self admitted guest event', (
|
|
10840
|
+
it('listens to the self admitted guest event without blocking on token prefetch', async () => {
|
|
10420
10841
|
meeting.stopKeepAlive = sinon.stub();
|
|
10421
10842
|
meeting.updateLLMConnection = sinon.stub();
|
|
10843
|
+
let resolvePrefetch;
|
|
10844
|
+
|
|
10845
|
+
meeting.ensureDefaultDatachannelTokenAfterAdmit = sinon
|
|
10846
|
+
.stub()
|
|
10847
|
+
.returns(new Promise((resolve) => {
|
|
10848
|
+
resolvePrefetch = resolve;
|
|
10849
|
+
}));
|
|
10422
10850
|
meeting.rtcMetrics = {
|
|
10423
10851
|
sendNextMetrics: sinon.stub(),
|
|
10424
10852
|
};
|
|
10853
|
+
|
|
10425
10854
|
meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
|
|
10855
|
+
|
|
10426
10856
|
assert.calledOnceWithExactly(meeting.stopKeepAlive);
|
|
10857
|
+
assert.calledOnceWithExactly(meeting.ensureDefaultDatachannelTokenAfterAdmit);
|
|
10427
10858
|
assert.calledThrice(TriggerProxy.trigger);
|
|
10428
10859
|
assert.calledWith(
|
|
10429
10860
|
TriggerProxy.trigger,
|
|
@@ -10442,7 +10873,11 @@ describe('plugin-meetings', () => {
|
|
|
10442
10873
|
correlation_id: meeting.correlationId,
|
|
10443
10874
|
}
|
|
10444
10875
|
);
|
|
10445
|
-
|
|
10876
|
+
|
|
10877
|
+
resolvePrefetch(false);
|
|
10878
|
+
await Promise.resolve();
|
|
10879
|
+
|
|
10880
|
+
assert.calledOnce(meeting.updateLLMConnection);
|
|
10446
10881
|
});
|
|
10447
10882
|
|
|
10448
10883
|
it('listens to the breakouts changed event', () => {
|
|
@@ -10956,6 +11391,92 @@ describe('plugin-meetings', () => {
|
|
|
10956
11391
|
);
|
|
10957
11392
|
});
|
|
10958
11393
|
|
|
11394
|
+
const recordingTestCases = [
|
|
11395
|
+
{
|
|
11396
|
+
description: 'triggers MEETING_STARTED_RECORDING when state is RECORDING',
|
|
11397
|
+
state: RECORDING_STATE.RECORDING,
|
|
11398
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_STARTED_RECORDING,
|
|
11399
|
+
expectedRecordingState: RECORDING_STATE.RECORDING,
|
|
11400
|
+
},
|
|
11401
|
+
{
|
|
11402
|
+
description: 'triggers MEETING_STOPPED_RECORDING when state is IDLE',
|
|
11403
|
+
state: RECORDING_STATE.IDLE,
|
|
11404
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_STOPPED_RECORDING,
|
|
11405
|
+
expectedRecordingState: RECORDING_STATE.IDLE,
|
|
11406
|
+
},
|
|
11407
|
+
{
|
|
11408
|
+
description: 'triggers MEETING_PAUSED_RECORDING when state is PAUSED',
|
|
11409
|
+
state: RECORDING_STATE.PAUSED,
|
|
11410
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_PAUSED_RECORDING,
|
|
11411
|
+
expectedRecordingState: RECORDING_STATE.PAUSED,
|
|
11412
|
+
},
|
|
11413
|
+
{
|
|
11414
|
+
description:
|
|
11415
|
+
'triggers MEETING_RESUMED_RECORDING and sets state to RECORDING when state is RESUMED',
|
|
11416
|
+
state: RECORDING_STATE.RESUMED,
|
|
11417
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_RESUMED_RECORDING,
|
|
11418
|
+
expectedRecordingState: RECORDING_STATE.RECORDING,
|
|
11419
|
+
},
|
|
11420
|
+
];
|
|
11421
|
+
|
|
11422
|
+
recordingTestCases.forEach(({description, state, expectedEvent, expectedRecordingState}) => {
|
|
11423
|
+
it(`listens to CONTROLS_RECORDING_UPDATED - ${description}`, async () => {
|
|
11424
|
+
const modifiedBy = 'user-id-123';
|
|
11425
|
+
const lastModified = '2026-01-01T00:00:00Z';
|
|
11426
|
+
|
|
11427
|
+
await meeting.locusInfo.emitScoped(
|
|
11428
|
+
{function: 'test', file: 'test'},
|
|
11429
|
+
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
11430
|
+
{state, modifiedBy, lastModified, modifiedByServiceAppName: undefined, modifiedByServiceAppId: undefined}
|
|
11431
|
+
);
|
|
11432
|
+
|
|
11433
|
+
assert.deepEqual(meeting.recording, {
|
|
11434
|
+
state: expectedRecordingState,
|
|
11435
|
+
modifiedBy,
|
|
11436
|
+
lastModified,
|
|
11437
|
+
modifiedByServiceAppName: undefined,
|
|
11438
|
+
modifiedByServiceAppId: undefined,
|
|
11439
|
+
});
|
|
11440
|
+
|
|
11441
|
+
assert.calledWith(
|
|
11442
|
+
TriggerProxy.trigger,
|
|
11443
|
+
meeting,
|
|
11444
|
+
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
11445
|
+
expectedEvent,
|
|
11446
|
+
meeting.recording
|
|
11447
|
+
);
|
|
11448
|
+
});
|
|
11449
|
+
});
|
|
11450
|
+
|
|
11451
|
+
it('listens to CONTROLS_RECORDING_UPDATED and includes modifiedByServiceAppName and modifiedByServiceAppId when present', async () => {
|
|
11452
|
+
const modifiedBy = 'user-id-123';
|
|
11453
|
+
const lastModified = '2026-01-01T00:00:00Z';
|
|
11454
|
+
const modifiedByServiceAppName = 'My Bot';
|
|
11455
|
+
const modifiedByServiceAppId = 'app-id-123';
|
|
11456
|
+
|
|
11457
|
+
await meeting.locusInfo.emitScoped(
|
|
11458
|
+
{function: 'test', file: 'test'},
|
|
11459
|
+
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
11460
|
+
{state: RECORDING_STATE.RECORDING, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}
|
|
11461
|
+
);
|
|
11462
|
+
|
|
11463
|
+
assert.deepEqual(meeting.recording, {
|
|
11464
|
+
state: RECORDING_STATE.RECORDING,
|
|
11465
|
+
modifiedBy,
|
|
11466
|
+
lastModified,
|
|
11467
|
+
modifiedByServiceAppName,
|
|
11468
|
+
modifiedByServiceAppId,
|
|
11469
|
+
});
|
|
11470
|
+
|
|
11471
|
+
assert.calledWith(
|
|
11472
|
+
TriggerProxy.trigger,
|
|
11473
|
+
meeting,
|
|
11474
|
+
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
11475
|
+
EVENT_TRIGGERS.MEETING_STARTED_RECORDING,
|
|
11476
|
+
meeting.recording
|
|
11477
|
+
);
|
|
11478
|
+
});
|
|
11479
|
+
|
|
10959
11480
|
it('listens to the locus interpretation update event', () => {
|
|
10960
11481
|
const interpretation = {
|
|
10961
11482
|
siLanguages: [{languageCode: 20, languageName: 'en'}],
|
|
@@ -11009,6 +11530,7 @@ describe('plugin-meetings', () => {
|
|
|
11009
11530
|
meeting.annotation.locusUrlUpdate = sinon.stub();
|
|
11010
11531
|
meeting.simultaneousInterpretation.locusUrlUpdate = sinon.stub();
|
|
11011
11532
|
meeting.webinar.locusUrlUpdate = sinon.stub();
|
|
11533
|
+
meeting.aiEnableRequest.locusUrlUpdate = sinon.stub();
|
|
11012
11534
|
|
|
11013
11535
|
meeting.locusInfo.emit(
|
|
11014
11536
|
{function: 'test', file: 'test'},
|
|
@@ -11023,6 +11545,7 @@ describe('plugin-meetings', () => {
|
|
|
11023
11545
|
assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, false);
|
|
11024
11546
|
assert.calledWith(meeting.simultaneousInterpretation.locusUrlUpdate, newLocusUrl);
|
|
11025
11547
|
assert.calledWith(meeting.webinar.locusUrlUpdate, newLocusUrl);
|
|
11548
|
+
assert.calledWith(meeting.aiEnableRequest.locusUrlUpdate, newLocusUrl);
|
|
11026
11549
|
assert.equal(meeting.locusUrl, newLocusUrl);
|
|
11027
11550
|
assert(meeting.locusId, '12345');
|
|
11028
11551
|
|
|
@@ -11338,6 +11861,93 @@ describe('plugin-meetings', () => {
|
|
|
11338
11861
|
});
|
|
11339
11862
|
});
|
|
11340
11863
|
|
|
11864
|
+
describe('#finalizeMeetingAfterInitialLocusSetup', () => {
|
|
11865
|
+
it('refreshes destination from synced locus when destination type is LOCUS_ID', () => {
|
|
11866
|
+
const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11867
|
+
|
|
11868
|
+
meeting.destinationType = DESTINATION_TYPE.LOCUS_ID;
|
|
11869
|
+
meeting.destination = {info: {topic: 'old'}};
|
|
11870
|
+
|
|
11871
|
+
meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
|
|
11872
|
+
|
|
11873
|
+
assert.equal(meeting.destination, syncedLocus);
|
|
11874
|
+
});
|
|
11875
|
+
|
|
11876
|
+
it('does not refresh destination when destination type is not LOCUS_ID', () => {
|
|
11877
|
+
const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11878
|
+
const originalDestination = {destination: 'original-destination'};
|
|
11879
|
+
|
|
11880
|
+
meeting.destinationType = DESTINATION_TYPE.CONVERSATION_URL;
|
|
11881
|
+
meeting.destination = originalDestination;
|
|
11882
|
+
|
|
11883
|
+
meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
|
|
11884
|
+
|
|
11885
|
+
assert.equal(meeting.destination, originalDestination);
|
|
11886
|
+
});
|
|
11887
|
+
|
|
11888
|
+
it('fetches meeting info when meetingInfo is empty and destination has info', () => {
|
|
11889
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
11890
|
+
|
|
11891
|
+
meeting.meetingInfo = {};
|
|
11892
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11893
|
+
|
|
11894
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
11895
|
+
|
|
11896
|
+
assert.calledOnceWithExactly(fetchMeetingInfoStub, {});
|
|
11897
|
+
});
|
|
11898
|
+
|
|
11899
|
+
it('does not fetch meeting info when destination has no info', () => {
|
|
11900
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
11901
|
+
|
|
11902
|
+
meeting.meetingInfo = {};
|
|
11903
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123'};
|
|
11904
|
+
|
|
11905
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
11906
|
+
|
|
11907
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
11908
|
+
});
|
|
11909
|
+
|
|
11910
|
+
it('does not fetch meeting info when meetingInfo is already populated', () => {
|
|
11911
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
11912
|
+
|
|
11913
|
+
meeting.meetingInfo = {meetingJoinUrl: 'https://example.com/join/abc'};
|
|
11914
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11915
|
+
|
|
11916
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
11917
|
+
|
|
11918
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
11919
|
+
});
|
|
11920
|
+
|
|
11921
|
+
it('does not fetch meeting info when delayed fetch timer is already scheduled', () => {
|
|
11922
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
11923
|
+
|
|
11924
|
+
meeting.meetingInfo = {};
|
|
11925
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11926
|
+
meeting.fetchMeetingInfoTimeoutId = 42;
|
|
11927
|
+
|
|
11928
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
11929
|
+
|
|
11930
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
11931
|
+
});
|
|
11932
|
+
|
|
11933
|
+
it('swallows async fetchMeetingInfo errors and logs info', async () => {
|
|
11934
|
+
const error = new Error('fetch failed');
|
|
11935
|
+
|
|
11936
|
+
meeting.meetingInfo = {};
|
|
11937
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11938
|
+
sinon.stub(meeting, 'fetchMeetingInfo').returns(Promise.reject(error));
|
|
11939
|
+
const loggerInfoStub = sinon.stub(LoggerProxy.logger, 'info');
|
|
11940
|
+
|
|
11941
|
+
await meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
11942
|
+
|
|
11943
|
+
assert.calledOnce(loggerInfoStub);
|
|
11944
|
+
assert.match(
|
|
11945
|
+
loggerInfoStub.firstCall.args[0],
|
|
11946
|
+
/Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: fetch failed/
|
|
11947
|
+
);
|
|
11948
|
+
});
|
|
11949
|
+
});
|
|
11950
|
+
|
|
11341
11951
|
describe('#emailInput', () => {
|
|
11342
11952
|
it('should set the email input', () => {
|
|
11343
11953
|
assert.notOk(meeting.emailInput);
|
|
@@ -11940,6 +12550,7 @@ describe('plugin-meetings', () => {
|
|
|
11940
12550
|
let showAutoEndMeetingWarningSpy;
|
|
11941
12551
|
let canAttendeeRequestAiAssistantEnabledSpy;
|
|
11942
12552
|
let attendeeRequestAiAssistantDeclinedAllSpy;
|
|
12553
|
+
let isAnonymizeDisplayNamesEnabledSpy;
|
|
11943
12554
|
// Due to import tree issues, hasHints must be stubed within the scope of the `it`.
|
|
11944
12555
|
|
|
11945
12556
|
beforeEach(() => {
|
|
@@ -11988,6 +12599,10 @@ describe('plugin-meetings', () => {
|
|
|
11988
12599
|
MeetingUtil,
|
|
11989
12600
|
'attendeeRequestAiAssistantDeclinedAll'
|
|
11990
12601
|
);
|
|
12602
|
+
isAnonymizeDisplayNamesEnabledSpy = sinon.spy(
|
|
12603
|
+
MeetingUtil,
|
|
12604
|
+
'isAnonymizeDisplayNamesEnabled'
|
|
12605
|
+
);
|
|
11991
12606
|
});
|
|
11992
12607
|
|
|
11993
12608
|
afterEach(() => {
|
|
@@ -11996,6 +12611,7 @@ describe('plugin-meetings', () => {
|
|
|
11996
12611
|
showAutoEndMeetingWarningSpy.restore();
|
|
11997
12612
|
canAttendeeRequestAiAssistantEnabledSpy.restore();
|
|
11998
12613
|
attendeeRequestAiAssistantDeclinedAllSpy.restore();
|
|
12614
|
+
isAnonymizeDisplayNamesEnabledSpy.restore();
|
|
11999
12615
|
});
|
|
12000
12616
|
|
|
12001
12617
|
forEach(
|
|
@@ -12553,6 +13169,7 @@ describe('plugin-meetings', () => {
|
|
|
12553
13169
|
meeting.roles
|
|
12554
13170
|
);
|
|
12555
13171
|
assert.calledWith(attendeeRequestAiAssistantDeclinedAllSpy, userDisplayHints);
|
|
13172
|
+
assert.calledWith(isAnonymizeDisplayNamesEnabledSpy, userDisplayHints);
|
|
12556
13173
|
|
|
12557
13174
|
assert.calledWith(ControlsOptionsUtil.hasHints, {
|
|
12558
13175
|
requiredHints: [DISPLAY_HINTS.MUTE_ALL],
|
|
@@ -13124,7 +13741,9 @@ describe('plugin-meetings', () => {
|
|
|
13124
13741
|
info: {datachannelUrl: 'a datachannel url'},
|
|
13125
13742
|
};
|
|
13126
13743
|
|
|
13127
|
-
webex.internal.llm.getDatachannelToken
|
|
13744
|
+
webex.internal.llm.getDatachannelToken
|
|
13745
|
+
.withArgs('llm-default-session')
|
|
13746
|
+
.returns('token-123');
|
|
13128
13747
|
|
|
13129
13748
|
await meeting.updateLLMConnection();
|
|
13130
13749
|
|
|
@@ -13178,6 +13797,131 @@ describe('plugin-meetings', () => {
|
|
|
13178
13797
|
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
13179
13798
|
});
|
|
13180
13799
|
|
|
13800
|
+
describe('ownership tag', () => {
|
|
13801
|
+
beforeEach(() => {
|
|
13802
|
+
// Make the owner stub dynamic so setOwnerMeetingId() writes
|
|
13803
|
+
// propagate back to getOwnerMeetingId() reads. This mirrors the
|
|
13804
|
+
// real LLM singleton behavior so the finally-block release in
|
|
13805
|
+
// cleanupLLMConneciton is reflected in subsequent reads.
|
|
13806
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub().returns(undefined);
|
|
13807
|
+
webex.internal.llm.setOwnerMeetingId = sinon.stub().callsFake((id) => {
|
|
13808
|
+
webex.internal.llm.getOwnerMeetingId.returns(id);
|
|
13809
|
+
});
|
|
13810
|
+
});
|
|
13811
|
+
|
|
13812
|
+
it('skips disconnect and reconnect when LLM is connected and owned by another meeting (regardless of URL)', async () => {
|
|
13813
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13814
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13815
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
13816
|
+
// Locus/datachannel URL mismatch is the *normal* case when
|
|
13817
|
+
// another meeting owns the live socket -- each meeting has its
|
|
13818
|
+
// own locus URL. URL mismatch must NOT trigger a reclaim,
|
|
13819
|
+
// because doing so would tear down the owning meeting's healthy
|
|
13820
|
+
// LLM socket and break its data channel.
|
|
13821
|
+
webex.internal.llm.getLocusUrl.returns('owner-locus-url');
|
|
13822
|
+
webex.internal.llm.getDatachannelUrl.returns('owner-dc-url');
|
|
13823
|
+
meeting.locusInfo = {
|
|
13824
|
+
url: 'a different url',
|
|
13825
|
+
info: {datachannelUrl: 'a different datachannel url'},
|
|
13826
|
+
self: {},
|
|
13827
|
+
};
|
|
13828
|
+
|
|
13829
|
+
const result = await meeting.updateLLMConnection();
|
|
13830
|
+
|
|
13831
|
+
assert.equal(result, undefined);
|
|
13832
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
13833
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
13834
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
13835
|
+
assert.notCalled(meeting.startLLMHealthCheckTimer);
|
|
13836
|
+
});
|
|
13837
|
+
|
|
13838
|
+
|
|
13839
|
+
it('clears stale owner tag in cleanup finally block even when disconnectLLM rejects', async () => {
|
|
13840
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13841
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13842
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13843
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13844
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13845
|
+
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
13846
|
+
meeting.locusInfo = {
|
|
13847
|
+
url: 'a different url',
|
|
13848
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13849
|
+
self: {},
|
|
13850
|
+
};
|
|
13851
|
+
|
|
13852
|
+
try {
|
|
13853
|
+
await meeting.updateLLMConnection();
|
|
13854
|
+
} catch (e) {
|
|
13855
|
+
/* updateLLMConnection may reject when cleanup throws */
|
|
13856
|
+
}
|
|
13857
|
+
|
|
13858
|
+
// The owner-eligible finally branch must release the tag so a
|
|
13859
|
+
// subsequent reconnect attempt from any meeting is not blocked.
|
|
13860
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId, undefined);
|
|
13861
|
+
});
|
|
13862
|
+
|
|
13863
|
+
it('proceeds normally when LLM is connected and owned by this meeting with URL change', async () => {
|
|
13864
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13865
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13866
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13867
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13868
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13869
|
+
meeting.locusInfo = {
|
|
13870
|
+
url: 'a different url',
|
|
13871
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13872
|
+
self: {},
|
|
13873
|
+
};
|
|
13874
|
+
|
|
13875
|
+
await meeting.updateLLMConnection();
|
|
13876
|
+
|
|
13877
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13878
|
+
code: 3050,
|
|
13879
|
+
reason: 'done (permanent)',
|
|
13880
|
+
});
|
|
13881
|
+
assert.calledWithExactly(
|
|
13882
|
+
webex.internal.llm.registerAndConnect,
|
|
13883
|
+
'a different url',
|
|
13884
|
+
'a datachannel url',
|
|
13885
|
+
undefined
|
|
13886
|
+
);
|
|
13887
|
+
// setOwnerMeetingId is called twice: first with undefined in
|
|
13888
|
+
// cleanupLLMConneciton's finally block (so a failed disconnect
|
|
13889
|
+
// cannot leave a stale owner), then with this meeting's id
|
|
13890
|
+
// after registerAndConnect resolves.
|
|
13891
|
+
assert.calledTwice(webex.internal.llm.setOwnerMeetingId);
|
|
13892
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.firstCall, undefined);
|
|
13893
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.lastCall, meeting.id);
|
|
13894
|
+
});
|
|
13895
|
+
|
|
13896
|
+
it('claims ownership after successful registerAndConnect on initial connect', async () => {
|
|
13897
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13898
|
+
webex.internal.llm.isConnected.returns(false);
|
|
13899
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
13900
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13901
|
+
|
|
13902
|
+
await meeting.updateLLMConnection();
|
|
13903
|
+
|
|
13904
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
13905
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
13906
|
+
});
|
|
13907
|
+
|
|
13908
|
+
it('proceeds to connect when LLM is not connected even if another ownerId lingers', async () => {
|
|
13909
|
+
// Defensive path: if the LLM reports not-connected but an old
|
|
13910
|
+
// ownerId is still present (e.g. race before a successful
|
|
13911
|
+
// connections.delete), this meeting can still claim a fresh
|
|
13912
|
+
// connection.
|
|
13913
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13914
|
+
webex.internal.llm.isConnected.returns(false);
|
|
13915
|
+
webex.internal.llm.getOwnerMeetingId.returns('stale-owner-id');
|
|
13916
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13917
|
+
|
|
13918
|
+
await meeting.updateLLMConnection();
|
|
13919
|
+
|
|
13920
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
13921
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
13922
|
+
});
|
|
13923
|
+
});
|
|
13924
|
+
|
|
13181
13925
|
describe('#clearMeetingData', () => {
|
|
13182
13926
|
beforeEach(() => {
|
|
13183
13927
|
webex.internal.llm.isConnected = sinon.stub().returns(true);
|
|
@@ -13209,10 +13953,13 @@ describe('plugin-meetings', () => {
|
|
|
13209
13953
|
meeting.processLocusLLMEvent
|
|
13210
13954
|
);
|
|
13211
13955
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13212
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13213
|
-
assert.isUndefined(meeting.transcription);
|
|
13214
13956
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13215
|
-
|
|
13957
|
+
// stopTranscription and annotation.deregisterEvents are not
|
|
13958
|
+
// called here: they run in stopListeningForMeetingEvents()
|
|
13959
|
+
// before /leave to avoid double-emitting
|
|
13960
|
+
// MEETING_STOPPED_RECEIVING_TRANSCRIPTION.
|
|
13961
|
+
assert.notCalled(meeting.stopTranscription);
|
|
13962
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13216
13963
|
});
|
|
13217
13964
|
it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
|
|
13218
13965
|
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
@@ -13231,19 +13978,67 @@ describe('plugin-meetings', () => {
|
|
|
13231
13978
|
meeting.processLocusLLMEvent
|
|
13232
13979
|
);
|
|
13233
13980
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13234
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13235
|
-
assert.isUndefined(meeting.transcription);
|
|
13236
13981
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13237
|
-
assert.
|
|
13982
|
+
assert.notCalled(meeting.stopTranscription);
|
|
13983
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13238
13984
|
});
|
|
13239
|
-
it('always calls stopTranscription even when transcription is undefined', async () => {
|
|
13240
|
-
meeting.transcription = undefined;
|
|
13241
13985
|
|
|
13242
|
-
|
|
13986
|
+
describe('ownership tag', () => {
|
|
13987
|
+
beforeEach(() => {
|
|
13988
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub();
|
|
13989
|
+
});
|
|
13243
13990
|
|
|
13244
|
-
|
|
13245
|
-
|
|
13246
|
-
|
|
13991
|
+
it('skips disconnectLLM but still removes this meeting listeners when another meeting owns the LLM', async () => {
|
|
13992
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
13993
|
+
|
|
13994
|
+
await meeting.clearMeetingData();
|
|
13995
|
+
|
|
13996
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
13997
|
+
// Shared data-channel auth tokens belong to the owner meeting's
|
|
13998
|
+
// live LLM session and must not be wiped by a non-owner
|
|
13999
|
+
// teardown, otherwise the owner's next reconnect would lose
|
|
14000
|
+
// its Data-Channel-Auth-Token.
|
|
14001
|
+
assert.notCalled(meeting.clearDataChannelToken);
|
|
14002
|
+
// Listeners owned by *this* Meeting instance must still be
|
|
14003
|
+
// removed so a leaving subordinate meeting stops receiving
|
|
14004
|
+
// relay/locus events from the shared singleton.
|
|
14005
|
+
assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
14006
|
+
assert.calledWithExactly(
|
|
14007
|
+
webex.internal.llm.off,
|
|
14008
|
+
'event:relay.event',
|
|
14009
|
+
meeting.processRelayEvent
|
|
14010
|
+
);
|
|
14011
|
+
assert.calledWithExactly(
|
|
14012
|
+
webex.internal.llm.off,
|
|
14013
|
+
'event:locus.state_message',
|
|
14014
|
+
meeting.processLocusLLMEvent
|
|
14015
|
+
);
|
|
14016
|
+
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
14017
|
+
});
|
|
14018
|
+
|
|
14019
|
+
it('calls disconnectLLM and clears data channel token when this meeting is the owner', async () => {
|
|
14020
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
14021
|
+
|
|
14022
|
+
await meeting.clearMeetingData();
|
|
14023
|
+
|
|
14024
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
14025
|
+
code: 3050,
|
|
14026
|
+
reason: 'done (permanent)',
|
|
14027
|
+
});
|
|
14028
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
14029
|
+
});
|
|
14030
|
+
|
|
14031
|
+
it('calls disconnectLLM and clears data channel token when no owner is recorded (first-claim / legacy)', async () => {
|
|
14032
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
14033
|
+
|
|
14034
|
+
await meeting.clearMeetingData();
|
|
14035
|
+
|
|
14036
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
14037
|
+
code: 3050,
|
|
14038
|
+
reason: 'done (permanent)',
|
|
14039
|
+
});
|
|
14040
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
14041
|
+
});
|
|
13247
14042
|
});
|
|
13248
14043
|
});
|
|
13249
14044
|
});
|