@webex/plugin-meetings 3.12.0-mobius-socket.2 → 3.12.0-mobius-socket.4
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 +3 -2
- package/dist/breakouts/index.js.map +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 +651 -382
- 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 -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/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 +848 -582
- 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 +4 -1
- package/dist/metrics/constants.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 +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 +65 -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 +3 -0
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +68 -17
- 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 +1 -0
- 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 +375 -197
- 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 -97
- 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 +260 -23
- 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 +3 -0
- 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 +88 -21
- 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 +2 -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 +1263 -157
- 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 +902 -14
- 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 +81 -16
|
@@ -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,
|
|
@@ -1977,6 +1980,113 @@ describe('plugin-meetings', () => {
|
|
|
1977
1980
|
fakeProcessedReaction
|
|
1978
1981
|
);
|
|
1979
1982
|
});
|
|
1983
|
+
|
|
1984
|
+
[
|
|
1985
|
+
{
|
|
1986
|
+
title: 'should skip a reaction when the default relay route does not match the LLM binding',
|
|
1987
|
+
isPracticeSessionConnected: false,
|
|
1988
|
+
route: 'wrong-default-route',
|
|
1989
|
+
defaultBinding: 'default-route',
|
|
1990
|
+
practiceBinding: 'practice-route',
|
|
1991
|
+
shouldProcess: false,
|
|
1992
|
+
expectedSessionLabel: 'default session',
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
title: 'should process a reaction when the default relay route matches the LLM binding',
|
|
1996
|
+
isPracticeSessionConnected: false,
|
|
1997
|
+
route: 'default-route',
|
|
1998
|
+
defaultBinding: 'default-route',
|
|
1999
|
+
practiceBinding: 'practice-route',
|
|
2000
|
+
shouldProcess: true,
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
title:
|
|
2004
|
+
'should process a reaction when the practice-session relay route matches the practice-session LLM binding',
|
|
2005
|
+
isPracticeSessionConnected: true,
|
|
2006
|
+
route: 'practice-route',
|
|
2007
|
+
defaultBinding: 'default-route',
|
|
2008
|
+
practiceBinding: 'practice-route',
|
|
2009
|
+
shouldProcess: true,
|
|
2010
|
+
},
|
|
2011
|
+
{
|
|
2012
|
+
title:
|
|
2013
|
+
'should skip a reaction when the practice-session relay route does not match the practice-session LLM binding',
|
|
2014
|
+
isPracticeSessionConnected: true,
|
|
2015
|
+
route: 'default-route',
|
|
2016
|
+
defaultBinding: 'default-route',
|
|
2017
|
+
practiceBinding: 'practice-route',
|
|
2018
|
+
shouldProcess: false,
|
|
2019
|
+
expectedSessionLabel: 'practice session',
|
|
2020
|
+
},
|
|
2021
|
+
].forEach(
|
|
2022
|
+
({
|
|
2023
|
+
title,
|
|
2024
|
+
isPracticeSessionConnected,
|
|
2025
|
+
route,
|
|
2026
|
+
defaultBinding,
|
|
2027
|
+
practiceBinding,
|
|
2028
|
+
shouldProcess,
|
|
2029
|
+
expectedSessionLabel,
|
|
2030
|
+
}) => {
|
|
2031
|
+
it(title, () => {
|
|
2032
|
+
meeting.isReactionsSupported = sinon.stub().returns(true);
|
|
2033
|
+
meeting.config.receiveReactions = true;
|
|
2034
|
+
const fakeSendersName = 'Fake reactors name';
|
|
2035
|
+
meeting.members.membersCollection.get = sinon.stub().returns({name: fakeSendersName});
|
|
2036
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((llmSessionId) => {
|
|
2037
|
+
return llmSessionId === LLM_PRACTICE_SESSION && isPracticeSessionConnected;
|
|
2038
|
+
});
|
|
2039
|
+
webex.internal.llm.getBinding = sinon.stub().callsFake((llmSessionId) => {
|
|
2040
|
+
if (llmSessionId === LLM_PRACTICE_SESSION) {
|
|
2041
|
+
return practiceBinding;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
return defaultBinding;
|
|
2045
|
+
});
|
|
2046
|
+
const fakeReactionPayload = {
|
|
2047
|
+
type: 'fake_type',
|
|
2048
|
+
codepoints: 'fake_codepoints',
|
|
2049
|
+
shortcodes: 'fake_shortcodes',
|
|
2050
|
+
};
|
|
2051
|
+
const fakeSenderPayload = {
|
|
2052
|
+
participantId: 'fake_participant_id',
|
|
2053
|
+
};
|
|
2054
|
+
const fakeRelayEvent = {
|
|
2055
|
+
headers: {route},
|
|
2056
|
+
data: {
|
|
2057
|
+
relayType: REACTION_RELAY_TYPES.REACTION,
|
|
2058
|
+
reaction: fakeReactionPayload,
|
|
2059
|
+
sender: fakeSenderPayload,
|
|
2060
|
+
},
|
|
2061
|
+
};
|
|
2062
|
+
const fakeProcessedReaction = {
|
|
2063
|
+
reaction: fakeReactionPayload,
|
|
2064
|
+
sender: {
|
|
2065
|
+
id: fakeSenderPayload.participantId,
|
|
2066
|
+
name: fakeSendersName,
|
|
2067
|
+
},
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
TriggerProxy.trigger.resetHistory();
|
|
2071
|
+
meeting.processRelayEvent(fakeRelayEvent);
|
|
2072
|
+
|
|
2073
|
+
if (shouldProcess) {
|
|
2074
|
+
assert.calledWith(
|
|
2075
|
+
TriggerProxy.trigger,
|
|
2076
|
+
sinon.match.instanceOf(Meeting),
|
|
2077
|
+
{
|
|
2078
|
+
file: 'meeting/index',
|
|
2079
|
+
function: 'join',
|
|
2080
|
+
},
|
|
2081
|
+
EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
|
|
2082
|
+
fakeProcessedReaction
|
|
2083
|
+
);
|
|
2084
|
+
} else {
|
|
2085
|
+
assert.notCalled(TriggerProxy.trigger);
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
);
|
|
1980
2090
|
});
|
|
1981
2091
|
|
|
1982
2092
|
describe('#handleLLMOnline', () => {
|
|
@@ -4534,6 +4644,297 @@ describe('plugin-meetings', () => {
|
|
|
4534
4644
|
},
|
|
4535
4645
|
});
|
|
4536
4646
|
});
|
|
4647
|
+
|
|
4648
|
+
describe('handles STATS_UPDATE event for SRTP cipher detection', () => {
|
|
4649
|
+
it('emits MEETING_SRTP_CIPHER_UPDATED event when srtpCipher is found in transport stats', async () => {
|
|
4650
|
+
const fakeStats = new Map([
|
|
4651
|
+
[
|
|
4652
|
+
'transport-1',
|
|
4653
|
+
{
|
|
4654
|
+
type: 'transport',
|
|
4655
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4656
|
+
dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
|
4657
|
+
},
|
|
4658
|
+
],
|
|
4659
|
+
[
|
|
4660
|
+
'outbound-rtp-1',
|
|
4661
|
+
{
|
|
4662
|
+
type: 'outbound-rtp',
|
|
4663
|
+
ssrc: 12345,
|
|
4664
|
+
},
|
|
4665
|
+
],
|
|
4666
|
+
]);
|
|
4667
|
+
|
|
4668
|
+
statsAnalyzerStub.emit(
|
|
4669
|
+
{file: 'test', function: 'test'},
|
|
4670
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4671
|
+
{stats: fakeStats}
|
|
4672
|
+
);
|
|
4673
|
+
|
|
4674
|
+
assert.calledWith(
|
|
4675
|
+
TriggerProxy.trigger,
|
|
4676
|
+
sinon.match.instanceOf(Meeting),
|
|
4677
|
+
{
|
|
4678
|
+
file: 'meeting/index',
|
|
4679
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4680
|
+
},
|
|
4681
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4682
|
+
{srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
|
|
4683
|
+
);
|
|
4684
|
+
|
|
4685
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4686
|
+
});
|
|
4687
|
+
|
|
4688
|
+
it('updates meeting.mediaProperties.srtpCipher when cipher changes', async () => {
|
|
4689
|
+
const firstStats = new Map([
|
|
4690
|
+
[
|
|
4691
|
+
'transport-1',
|
|
4692
|
+
{
|
|
4693
|
+
type: 'transport',
|
|
4694
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4695
|
+
},
|
|
4696
|
+
],
|
|
4697
|
+
]);
|
|
4698
|
+
|
|
4699
|
+
statsAnalyzerStub.emit(
|
|
4700
|
+
{file: 'test', function: 'test'},
|
|
4701
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4702
|
+
{stats: firstStats}
|
|
4703
|
+
);
|
|
4704
|
+
|
|
4705
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4706
|
+
|
|
4707
|
+
const secondStats = new Map([
|
|
4708
|
+
[
|
|
4709
|
+
'transport-1',
|
|
4710
|
+
{
|
|
4711
|
+
type: 'transport',
|
|
4712
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
4713
|
+
},
|
|
4714
|
+
],
|
|
4715
|
+
]);
|
|
4716
|
+
|
|
4717
|
+
TriggerProxy.trigger.resetHistory();
|
|
4718
|
+
|
|
4719
|
+
statsAnalyzerStub.emit(
|
|
4720
|
+
{file: 'test', function: 'test'},
|
|
4721
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4722
|
+
{stats: secondStats}
|
|
4723
|
+
);
|
|
4724
|
+
|
|
4725
|
+
assert.calledWith(
|
|
4726
|
+
TriggerProxy.trigger,
|
|
4727
|
+
sinon.match.instanceOf(Meeting),
|
|
4728
|
+
{
|
|
4729
|
+
file: 'meeting/index',
|
|
4730
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4731
|
+
},
|
|
4732
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4733
|
+
{srtpCipher: 'AEAD_AES_256_GCM'}
|
|
4734
|
+
);
|
|
4735
|
+
|
|
4736
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AEAD_AES_256_GCM');
|
|
4737
|
+
});
|
|
4738
|
+
|
|
4739
|
+
it('does not emit event when srtpCipher has not changed', async () => {
|
|
4740
|
+
const firstStats = new Map([
|
|
4741
|
+
[
|
|
4742
|
+
'transport-1',
|
|
4743
|
+
{
|
|
4744
|
+
type: 'transport',
|
|
4745
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4746
|
+
},
|
|
4747
|
+
],
|
|
4748
|
+
]);
|
|
4749
|
+
|
|
4750
|
+
statsAnalyzerStub.emit(
|
|
4751
|
+
{file: 'test', function: 'test'},
|
|
4752
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4753
|
+
{stats: firstStats}
|
|
4754
|
+
);
|
|
4755
|
+
|
|
4756
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4757
|
+
|
|
4758
|
+
TriggerProxy.trigger.resetHistory();
|
|
4759
|
+
|
|
4760
|
+
// Emit same cipher again
|
|
4761
|
+
statsAnalyzerStub.emit(
|
|
4762
|
+
{file: 'test', function: 'test'},
|
|
4763
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4764
|
+
{stats: firstStats}
|
|
4765
|
+
);
|
|
4766
|
+
|
|
4767
|
+
// Should not trigger event again
|
|
4768
|
+
assert.neverCalledWith(
|
|
4769
|
+
TriggerProxy.trigger,
|
|
4770
|
+
sinon.match.instanceOf(Meeting),
|
|
4771
|
+
sinon.match.any,
|
|
4772
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4773
|
+
sinon.match.any
|
|
4774
|
+
);
|
|
4775
|
+
|
|
4776
|
+
// Cipher should remain the same
|
|
4777
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4778
|
+
});
|
|
4779
|
+
|
|
4780
|
+
it('does not emit event when stats contain no transport with srtpCipher', async () => {
|
|
4781
|
+
const fakeStats = new Map([
|
|
4782
|
+
[
|
|
4783
|
+
'outbound-rtp-1',
|
|
4784
|
+
{
|
|
4785
|
+
type: 'outbound-rtp',
|
|
4786
|
+
ssrc: 12345,
|
|
4787
|
+
},
|
|
4788
|
+
],
|
|
4789
|
+
[
|
|
4790
|
+
'inbound-rtp-1',
|
|
4791
|
+
{
|
|
4792
|
+
type: 'inbound-rtp',
|
|
4793
|
+
ssrc: 67890,
|
|
4794
|
+
},
|
|
4795
|
+
],
|
|
4796
|
+
]);
|
|
4797
|
+
|
|
4798
|
+
statsAnalyzerStub.emit(
|
|
4799
|
+
{file: 'test', function: 'test'},
|
|
4800
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4801
|
+
{stats: fakeStats}
|
|
4802
|
+
);
|
|
4803
|
+
|
|
4804
|
+
assert.neverCalledWith(
|
|
4805
|
+
TriggerProxy.trigger,
|
|
4806
|
+
sinon.match.instanceOf(Meeting),
|
|
4807
|
+
sinon.match.any,
|
|
4808
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4809
|
+
sinon.match.any
|
|
4810
|
+
);
|
|
4811
|
+
|
|
4812
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
4813
|
+
});
|
|
4814
|
+
|
|
4815
|
+
it('does not emit event when transport stat has no srtpCipher property', async () => {
|
|
4816
|
+
const fakeStats = new Map([
|
|
4817
|
+
[
|
|
4818
|
+
'transport-1',
|
|
4819
|
+
{
|
|
4820
|
+
type: 'transport',
|
|
4821
|
+
dtlsCipher: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
|
4822
|
+
// no srtpCipher property
|
|
4823
|
+
},
|
|
4824
|
+
],
|
|
4825
|
+
]);
|
|
4826
|
+
|
|
4827
|
+
statsAnalyzerStub.emit(
|
|
4828
|
+
{file: 'test', function: 'test'},
|
|
4829
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4830
|
+
{stats: fakeStats}
|
|
4831
|
+
);
|
|
4832
|
+
|
|
4833
|
+
assert.neverCalledWith(
|
|
4834
|
+
TriggerProxy.trigger,
|
|
4835
|
+
sinon.match.instanceOf(Meeting),
|
|
4836
|
+
sinon.match.any,
|
|
4837
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4838
|
+
sinon.match.any
|
|
4839
|
+
);
|
|
4840
|
+
|
|
4841
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
4842
|
+
});
|
|
4843
|
+
|
|
4844
|
+
it('uses first transport with srtpCipher when multiple transports exist', async () => {
|
|
4845
|
+
const fakeStats = new Map([
|
|
4846
|
+
[
|
|
4847
|
+
'transport-1',
|
|
4848
|
+
{
|
|
4849
|
+
type: 'transport',
|
|
4850
|
+
srtpCipher: 'AES_CM_128_HMAC_SHA1_80',
|
|
4851
|
+
},
|
|
4852
|
+
],
|
|
4853
|
+
[
|
|
4854
|
+
'transport-2',
|
|
4855
|
+
{
|
|
4856
|
+
type: 'transport',
|
|
4857
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
4858
|
+
},
|
|
4859
|
+
],
|
|
4860
|
+
[
|
|
4861
|
+
'outbound-rtp-1',
|
|
4862
|
+
{
|
|
4863
|
+
type: 'outbound-rtp',
|
|
4864
|
+
ssrc: 12345,
|
|
4865
|
+
},
|
|
4866
|
+
],
|
|
4867
|
+
]);
|
|
4868
|
+
|
|
4869
|
+
statsAnalyzerStub.emit(
|
|
4870
|
+
{file: 'test', function: 'test'},
|
|
4871
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4872
|
+
{stats: fakeStats}
|
|
4873
|
+
);
|
|
4874
|
+
|
|
4875
|
+
assert.calledWith(
|
|
4876
|
+
TriggerProxy.trigger,
|
|
4877
|
+
sinon.match.instanceOf(Meeting),
|
|
4878
|
+
{
|
|
4879
|
+
file: 'meeting/index',
|
|
4880
|
+
function: 'setupStatsAnalyzerEventHandlers',
|
|
4881
|
+
},
|
|
4882
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4883
|
+
{srtpCipher: 'AES_CM_128_HMAC_SHA1_80'}
|
|
4884
|
+
);
|
|
4885
|
+
|
|
4886
|
+
assert.equal(meeting.mediaProperties.srtpCipher, 'AES_CM_128_HMAC_SHA1_80');
|
|
4887
|
+
});
|
|
4888
|
+
|
|
4889
|
+
it('handles empty stats map without errors', async () => {
|
|
4890
|
+
const emptyStats = new Map();
|
|
4891
|
+
|
|
4892
|
+
statsAnalyzerStub.emit(
|
|
4893
|
+
{file: 'test', function: 'test'},
|
|
4894
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4895
|
+
{stats: emptyStats}
|
|
4896
|
+
);
|
|
4897
|
+
|
|
4898
|
+
assert.neverCalledWith(
|
|
4899
|
+
TriggerProxy.trigger,
|
|
4900
|
+
sinon.match.instanceOf(Meeting),
|
|
4901
|
+
sinon.match.any,
|
|
4902
|
+
EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
|
|
4903
|
+
sinon.match.any
|
|
4904
|
+
);
|
|
4905
|
+
|
|
4906
|
+
assert.isUndefined(meeting.mediaProperties.srtpCipher);
|
|
4907
|
+
});
|
|
4908
|
+
|
|
4909
|
+
it('logs cipher change when cipher is updated', async () => {
|
|
4910
|
+
const loggerSpy = sinon.spy(LoggerProxy.logger, 'info');
|
|
4911
|
+
|
|
4912
|
+
meeting.mediaProperties.srtpCipher = 'AES_CM_128_HMAC_SHA1_80';
|
|
4913
|
+
|
|
4914
|
+
const newStats = new Map([
|
|
4915
|
+
[
|
|
4916
|
+
'transport-1',
|
|
4917
|
+
{
|
|
4918
|
+
type: 'transport',
|
|
4919
|
+
srtpCipher: 'AEAD_AES_256_GCM',
|
|
4920
|
+
},
|
|
4921
|
+
],
|
|
4922
|
+
]);
|
|
4923
|
+
|
|
4924
|
+
statsAnalyzerStub.emit(
|
|
4925
|
+
{file: 'test', function: 'test'},
|
|
4926
|
+
StatsAnalyzerEventNames.STATS_UPDATE,
|
|
4927
|
+
{stats: newStats}
|
|
4928
|
+
);
|
|
4929
|
+
|
|
4930
|
+
assert.calledWithMatch(
|
|
4931
|
+
loggerSpy,
|
|
4932
|
+
sinon.match(/SRTP cipher changed from AES_CM_128_HMAC_SHA1_80 to AEAD_AES_256_GCM/)
|
|
4933
|
+
);
|
|
4934
|
+
|
|
4935
|
+
loggerSpy.restore();
|
|
4936
|
+
});
|
|
4937
|
+
});
|
|
4537
4938
|
});
|
|
4538
4939
|
|
|
4539
4940
|
describe('handles StatsMonitor events', () => {
|
|
@@ -6429,6 +6830,9 @@ describe('plugin-meetings', () => {
|
|
|
6429
6830
|
|
|
6430
6831
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
6431
6832
|
webex.internal.llm.off = sinon.stub();
|
|
6833
|
+
webex.internal.mercury.off = sinon.stub();
|
|
6834
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
6835
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
6432
6836
|
|
|
6433
6837
|
// A meeting needs to be joined to leave
|
|
6434
6838
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -6452,6 +6856,67 @@ describe('plugin-meetings', () => {
|
|
|
6452
6856
|
assert.calledOnce(meeting.clearMeetingData);
|
|
6453
6857
|
});
|
|
6454
6858
|
|
|
6859
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /leave', async () => {
|
|
6860
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
6861
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
6862
|
+
|
|
6863
|
+
await meeting.leave();
|
|
6864
|
+
|
|
6865
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
6866
|
+
// annotation) must be detached before the /leave request so that
|
|
6867
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
6868
|
+
// (per Locus team recommendation).
|
|
6869
|
+
assert.callOrder(
|
|
6870
|
+
webex.internal.llm.off,
|
|
6871
|
+
webex.internal.mercury.off,
|
|
6872
|
+
meeting.stopTranscription,
|
|
6873
|
+
meeting.annotation.deregisterEvents,
|
|
6874
|
+
meeting.meetingRequest.leaveMeeting
|
|
6875
|
+
);
|
|
6876
|
+
assert.calledWithExactly(
|
|
6877
|
+
webex.internal.llm.off,
|
|
6878
|
+
'event:relay.event',
|
|
6879
|
+
meeting.processRelayEvent
|
|
6880
|
+
);
|
|
6881
|
+
assert.calledWithExactly(
|
|
6882
|
+
webex.internal.llm.off,
|
|
6883
|
+
LOCUS_LLM_EVENT,
|
|
6884
|
+
meeting.processLocusLLMEvent
|
|
6885
|
+
);
|
|
6886
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
6887
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
6888
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
6889
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
6890
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
6891
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
6892
|
+
assert.isUndefined(meeting.transcription);
|
|
6893
|
+
});
|
|
6894
|
+
|
|
6895
|
+
it('tears down llm/mercury/transcription/annotation even when /leave rejects', async () => {
|
|
6896
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
6897
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
6898
|
+
meeting.meetingRequest.leaveMeeting = sinon
|
|
6899
|
+
.stub()
|
|
6900
|
+
.returns(Promise.reject(new Error('leave failed')));
|
|
6901
|
+
|
|
6902
|
+
await meeting.leave().catch(() => {});
|
|
6903
|
+
|
|
6904
|
+
assert.calledWithExactly(
|
|
6905
|
+
webex.internal.llm.off,
|
|
6906
|
+
'event:relay.event',
|
|
6907
|
+
meeting.processRelayEvent
|
|
6908
|
+
);
|
|
6909
|
+
assert.calledWithExactly(
|
|
6910
|
+
webex.internal.llm.off,
|
|
6911
|
+
LOCUS_LLM_EVENT,
|
|
6912
|
+
meeting.processLocusLLMEvent
|
|
6913
|
+
);
|
|
6914
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
6915
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
6916
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
6917
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
6918
|
+
});
|
|
6919
|
+
|
|
6455
6920
|
it('should reset call diagnostic latencies correctly', async () => {
|
|
6456
6921
|
const leave = meeting.leave();
|
|
6457
6922
|
|
|
@@ -8459,6 +8924,9 @@ describe('plugin-meetings', () => {
|
|
|
8459
8924
|
|
|
8460
8925
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
8461
8926
|
webex.internal.llm.off = sinon.stub();
|
|
8927
|
+
webex.internal.mercury.off = sinon.stub();
|
|
8928
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
8929
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
8462
8930
|
|
|
8463
8931
|
// A meeting needs to be joined to end
|
|
8464
8932
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -8481,6 +8949,66 @@ describe('plugin-meetings', () => {
|
|
|
8481
8949
|
assert.calledOnce(meeting?.unsetPeerConnections);
|
|
8482
8950
|
assert.calledOnce(meeting?.clearMeetingData);
|
|
8483
8951
|
});
|
|
8952
|
+
|
|
8953
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /end', async () => {
|
|
8954
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
8955
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
8956
|
+
|
|
8957
|
+
await meeting.endMeetingForAll();
|
|
8958
|
+
|
|
8959
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
8960
|
+
// annotation) must be detached before the /end request so that
|
|
8961
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
8962
|
+
// (per Locus team recommendation).
|
|
8963
|
+
assert.callOrder(
|
|
8964
|
+
webex.internal.llm.off,
|
|
8965
|
+
webex.internal.mercury.off,
|
|
8966
|
+
meeting.stopTranscription,
|
|
8967
|
+
meeting.annotation.deregisterEvents,
|
|
8968
|
+
meeting.meetingRequest.endMeetingForAll
|
|
8969
|
+
);
|
|
8970
|
+
assert.calledWithExactly(
|
|
8971
|
+
webex.internal.llm.off,
|
|
8972
|
+
'event:relay.event',
|
|
8973
|
+
meeting.processRelayEvent
|
|
8974
|
+
);
|
|
8975
|
+
assert.calledWithExactly(
|
|
8976
|
+
webex.internal.llm.off,
|
|
8977
|
+
LOCUS_LLM_EVENT,
|
|
8978
|
+
meeting.processLocusLLMEvent
|
|
8979
|
+
);
|
|
8980
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
8981
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
8982
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
8983
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
8984
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
8985
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
8986
|
+
});
|
|
8987
|
+
|
|
8988
|
+
it('tears down llm/mercury/transcription/annotation even when /end rejects', async () => {
|
|
8989
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
8990
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
8991
|
+
meeting.meetingRequest.endMeetingForAll = sinon
|
|
8992
|
+
.stub()
|
|
8993
|
+
.returns(Promise.reject(new Error('end failed')));
|
|
8994
|
+
|
|
8995
|
+
await meeting.endMeetingForAll().catch(() => {});
|
|
8996
|
+
|
|
8997
|
+
assert.calledWithExactly(
|
|
8998
|
+
webex.internal.llm.off,
|
|
8999
|
+
'event:relay.event',
|
|
9000
|
+
meeting.processRelayEvent
|
|
9001
|
+
);
|
|
9002
|
+
assert.calledWithExactly(
|
|
9003
|
+
webex.internal.llm.off,
|
|
9004
|
+
LOCUS_LLM_EVENT,
|
|
9005
|
+
meeting.processLocusLLMEvent
|
|
9006
|
+
);
|
|
9007
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
9008
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
9009
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
9010
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
9011
|
+
});
|
|
8484
9012
|
});
|
|
8485
9013
|
|
|
8486
9014
|
describe('#moveTo', () => {
|
|
@@ -10971,6 +11499,92 @@ describe('plugin-meetings', () => {
|
|
|
10971
11499
|
);
|
|
10972
11500
|
});
|
|
10973
11501
|
|
|
11502
|
+
const recordingTestCases = [
|
|
11503
|
+
{
|
|
11504
|
+
description: 'triggers MEETING_STARTED_RECORDING when state is RECORDING',
|
|
11505
|
+
state: RECORDING_STATE.RECORDING,
|
|
11506
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_STARTED_RECORDING,
|
|
11507
|
+
expectedRecordingState: RECORDING_STATE.RECORDING,
|
|
11508
|
+
},
|
|
11509
|
+
{
|
|
11510
|
+
description: 'triggers MEETING_STOPPED_RECORDING when state is IDLE',
|
|
11511
|
+
state: RECORDING_STATE.IDLE,
|
|
11512
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_STOPPED_RECORDING,
|
|
11513
|
+
expectedRecordingState: RECORDING_STATE.IDLE,
|
|
11514
|
+
},
|
|
11515
|
+
{
|
|
11516
|
+
description: 'triggers MEETING_PAUSED_RECORDING when state is PAUSED',
|
|
11517
|
+
state: RECORDING_STATE.PAUSED,
|
|
11518
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_PAUSED_RECORDING,
|
|
11519
|
+
expectedRecordingState: RECORDING_STATE.PAUSED,
|
|
11520
|
+
},
|
|
11521
|
+
{
|
|
11522
|
+
description:
|
|
11523
|
+
'triggers MEETING_RESUMED_RECORDING and sets state to RECORDING when state is RESUMED',
|
|
11524
|
+
state: RECORDING_STATE.RESUMED,
|
|
11525
|
+
expectedEvent: EVENT_TRIGGERS.MEETING_RESUMED_RECORDING,
|
|
11526
|
+
expectedRecordingState: RECORDING_STATE.RECORDING,
|
|
11527
|
+
},
|
|
11528
|
+
];
|
|
11529
|
+
|
|
11530
|
+
recordingTestCases.forEach(({description, state, expectedEvent, expectedRecordingState}) => {
|
|
11531
|
+
it(`listens to CONTROLS_RECORDING_UPDATED - ${description}`, async () => {
|
|
11532
|
+
const modifiedBy = 'user-id-123';
|
|
11533
|
+
const lastModified = '2026-01-01T00:00:00Z';
|
|
11534
|
+
|
|
11535
|
+
await meeting.locusInfo.emitScoped(
|
|
11536
|
+
{function: 'test', file: 'test'},
|
|
11537
|
+
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
11538
|
+
{state, modifiedBy, lastModified, modifiedByServiceAppName: undefined, modifiedByServiceAppId: undefined}
|
|
11539
|
+
);
|
|
11540
|
+
|
|
11541
|
+
assert.deepEqual(meeting.recording, {
|
|
11542
|
+
state: expectedRecordingState,
|
|
11543
|
+
modifiedBy,
|
|
11544
|
+
lastModified,
|
|
11545
|
+
modifiedByServiceAppName: undefined,
|
|
11546
|
+
modifiedByServiceAppId: undefined,
|
|
11547
|
+
});
|
|
11548
|
+
|
|
11549
|
+
assert.calledWith(
|
|
11550
|
+
TriggerProxy.trigger,
|
|
11551
|
+
meeting,
|
|
11552
|
+
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
11553
|
+
expectedEvent,
|
|
11554
|
+
meeting.recording
|
|
11555
|
+
);
|
|
11556
|
+
});
|
|
11557
|
+
});
|
|
11558
|
+
|
|
11559
|
+
it('listens to CONTROLS_RECORDING_UPDATED and includes modifiedByServiceAppName and modifiedByServiceAppId when present', async () => {
|
|
11560
|
+
const modifiedBy = 'user-id-123';
|
|
11561
|
+
const lastModified = '2026-01-01T00:00:00Z';
|
|
11562
|
+
const modifiedByServiceAppName = 'My Bot';
|
|
11563
|
+
const modifiedByServiceAppId = 'app-id-123';
|
|
11564
|
+
|
|
11565
|
+
await meeting.locusInfo.emitScoped(
|
|
11566
|
+
{function: 'test', file: 'test'},
|
|
11567
|
+
LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
|
|
11568
|
+
{state: RECORDING_STATE.RECORDING, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}
|
|
11569
|
+
);
|
|
11570
|
+
|
|
11571
|
+
assert.deepEqual(meeting.recording, {
|
|
11572
|
+
state: RECORDING_STATE.RECORDING,
|
|
11573
|
+
modifiedBy,
|
|
11574
|
+
lastModified,
|
|
11575
|
+
modifiedByServiceAppName,
|
|
11576
|
+
modifiedByServiceAppId,
|
|
11577
|
+
});
|
|
11578
|
+
|
|
11579
|
+
assert.calledWith(
|
|
11580
|
+
TriggerProxy.trigger,
|
|
11581
|
+
meeting,
|
|
11582
|
+
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
11583
|
+
EVENT_TRIGGERS.MEETING_STARTED_RECORDING,
|
|
11584
|
+
meeting.recording
|
|
11585
|
+
);
|
|
11586
|
+
});
|
|
11587
|
+
|
|
10974
11588
|
it('listens to the locus interpretation update event', () => {
|
|
10975
11589
|
const interpretation = {
|
|
10976
11590
|
siLanguages: [{languageCode: 20, languageName: 'en'}],
|
|
@@ -11024,6 +11638,7 @@ describe('plugin-meetings', () => {
|
|
|
11024
11638
|
meeting.annotation.locusUrlUpdate = sinon.stub();
|
|
11025
11639
|
meeting.simultaneousInterpretation.locusUrlUpdate = sinon.stub();
|
|
11026
11640
|
meeting.webinar.locusUrlUpdate = sinon.stub();
|
|
11641
|
+
meeting.aiEnableRequest.locusUrlUpdate = sinon.stub();
|
|
11027
11642
|
|
|
11028
11643
|
meeting.locusInfo.emit(
|
|
11029
11644
|
{function: 'test', file: 'test'},
|
|
@@ -11038,6 +11653,7 @@ describe('plugin-meetings', () => {
|
|
|
11038
11653
|
assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, false);
|
|
11039
11654
|
assert.calledWith(meeting.simultaneousInterpretation.locusUrlUpdate, newLocusUrl);
|
|
11040
11655
|
assert.calledWith(meeting.webinar.locusUrlUpdate, newLocusUrl);
|
|
11656
|
+
assert.calledWith(meeting.aiEnableRequest.locusUrlUpdate, newLocusUrl);
|
|
11041
11657
|
assert.equal(meeting.locusUrl, newLocusUrl);
|
|
11042
11658
|
assert(meeting.locusId, '12345');
|
|
11043
11659
|
|
|
@@ -11353,6 +11969,93 @@ describe('plugin-meetings', () => {
|
|
|
11353
11969
|
});
|
|
11354
11970
|
});
|
|
11355
11971
|
|
|
11972
|
+
describe('#finalizeMeetingAfterInitialLocusSetup', () => {
|
|
11973
|
+
it('refreshes destination from synced locus when destination type is LOCUS_ID', () => {
|
|
11974
|
+
const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11975
|
+
|
|
11976
|
+
meeting.destinationType = DESTINATION_TYPE.LOCUS_ID;
|
|
11977
|
+
meeting.destination = {info: {topic: 'old'}};
|
|
11978
|
+
|
|
11979
|
+
meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
|
|
11980
|
+
|
|
11981
|
+
assert.equal(meeting.destination, syncedLocus);
|
|
11982
|
+
});
|
|
11983
|
+
|
|
11984
|
+
it('does not refresh destination when destination type is not LOCUS_ID', () => {
|
|
11985
|
+
const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
11986
|
+
const originalDestination = {destination: 'original-destination'};
|
|
11987
|
+
|
|
11988
|
+
meeting.destinationType = DESTINATION_TYPE.CONVERSATION_URL;
|
|
11989
|
+
meeting.destination = originalDestination;
|
|
11990
|
+
|
|
11991
|
+
meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
|
|
11992
|
+
|
|
11993
|
+
assert.equal(meeting.destination, originalDestination);
|
|
11994
|
+
});
|
|
11995
|
+
|
|
11996
|
+
it('fetches meeting info when meetingInfo is empty and destination has info', () => {
|
|
11997
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
11998
|
+
|
|
11999
|
+
meeting.meetingInfo = {};
|
|
12000
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12001
|
+
|
|
12002
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12003
|
+
|
|
12004
|
+
assert.calledOnceWithExactly(fetchMeetingInfoStub, {});
|
|
12005
|
+
});
|
|
12006
|
+
|
|
12007
|
+
it('does not fetch meeting info when destination has no info', () => {
|
|
12008
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12009
|
+
|
|
12010
|
+
meeting.meetingInfo = {};
|
|
12011
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123'};
|
|
12012
|
+
|
|
12013
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12014
|
+
|
|
12015
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
12016
|
+
});
|
|
12017
|
+
|
|
12018
|
+
it('does not fetch meeting info when meetingInfo is already populated', () => {
|
|
12019
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12020
|
+
|
|
12021
|
+
meeting.meetingInfo = {meetingJoinUrl: 'https://example.com/join/abc'};
|
|
12022
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12023
|
+
|
|
12024
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12025
|
+
|
|
12026
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
12027
|
+
});
|
|
12028
|
+
|
|
12029
|
+
it('does not fetch meeting info when delayed fetch timer is already scheduled', () => {
|
|
12030
|
+
const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
|
|
12031
|
+
|
|
12032
|
+
meeting.meetingInfo = {};
|
|
12033
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12034
|
+
meeting.fetchMeetingInfoTimeoutId = 42;
|
|
12035
|
+
|
|
12036
|
+
meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12037
|
+
|
|
12038
|
+
assert.notCalled(fetchMeetingInfoStub);
|
|
12039
|
+
});
|
|
12040
|
+
|
|
12041
|
+
it('swallows async fetchMeetingInfo errors and logs info', async () => {
|
|
12042
|
+
const error = new Error('fetch failed');
|
|
12043
|
+
|
|
12044
|
+
meeting.meetingInfo = {};
|
|
12045
|
+
meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
|
|
12046
|
+
sinon.stub(meeting, 'fetchMeetingInfo').returns(Promise.reject(error));
|
|
12047
|
+
const loggerInfoStub = sinon.stub(LoggerProxy.logger, 'info');
|
|
12048
|
+
|
|
12049
|
+
await meeting.finalizeMeetingAfterInitialLocusSetup({});
|
|
12050
|
+
|
|
12051
|
+
assert.calledOnce(loggerInfoStub);
|
|
12052
|
+
assert.match(
|
|
12053
|
+
loggerInfoStub.firstCall.args[0],
|
|
12054
|
+
/Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: fetch failed/
|
|
12055
|
+
);
|
|
12056
|
+
});
|
|
12057
|
+
});
|
|
12058
|
+
|
|
11356
12059
|
describe('#emailInput', () => {
|
|
11357
12060
|
it('should set the email input', () => {
|
|
11358
12061
|
assert.notOk(meeting.emailInput);
|
|
@@ -11955,6 +12658,7 @@ describe('plugin-meetings', () => {
|
|
|
11955
12658
|
let showAutoEndMeetingWarningSpy;
|
|
11956
12659
|
let canAttendeeRequestAiAssistantEnabledSpy;
|
|
11957
12660
|
let attendeeRequestAiAssistantDeclinedAllSpy;
|
|
12661
|
+
let isAnonymizeDisplayNamesEnabledSpy;
|
|
11958
12662
|
// Due to import tree issues, hasHints must be stubed within the scope of the `it`.
|
|
11959
12663
|
|
|
11960
12664
|
beforeEach(() => {
|
|
@@ -12003,6 +12707,10 @@ describe('plugin-meetings', () => {
|
|
|
12003
12707
|
MeetingUtil,
|
|
12004
12708
|
'attendeeRequestAiAssistantDeclinedAll'
|
|
12005
12709
|
);
|
|
12710
|
+
isAnonymizeDisplayNamesEnabledSpy = sinon.spy(
|
|
12711
|
+
MeetingUtil,
|
|
12712
|
+
'isAnonymizeDisplayNamesEnabled'
|
|
12713
|
+
);
|
|
12006
12714
|
});
|
|
12007
12715
|
|
|
12008
12716
|
afterEach(() => {
|
|
@@ -12011,6 +12719,7 @@ describe('plugin-meetings', () => {
|
|
|
12011
12719
|
showAutoEndMeetingWarningSpy.restore();
|
|
12012
12720
|
canAttendeeRequestAiAssistantEnabledSpy.restore();
|
|
12013
12721
|
attendeeRequestAiAssistantDeclinedAllSpy.restore();
|
|
12722
|
+
isAnonymizeDisplayNamesEnabledSpy.restore();
|
|
12014
12723
|
});
|
|
12015
12724
|
|
|
12016
12725
|
forEach(
|
|
@@ -12568,6 +13277,7 @@ describe('plugin-meetings', () => {
|
|
|
12568
13277
|
meeting.roles
|
|
12569
13278
|
);
|
|
12570
13279
|
assert.calledWith(attendeeRequestAiAssistantDeclinedAllSpy, userDisplayHints);
|
|
13280
|
+
assert.calledWith(isAnonymizeDisplayNamesEnabledSpy, userDisplayHints);
|
|
12571
13281
|
|
|
12572
13282
|
assert.calledWith(ControlsOptionsUtil.hasHints, {
|
|
12573
13283
|
requiredHints: [DISPLAY_HINTS.MUTE_ALL],
|
|
@@ -13139,7 +13849,9 @@ describe('plugin-meetings', () => {
|
|
|
13139
13849
|
info: {datachannelUrl: 'a datachannel url'},
|
|
13140
13850
|
};
|
|
13141
13851
|
|
|
13142
|
-
webex.internal.llm.getDatachannelToken
|
|
13852
|
+
webex.internal.llm.getDatachannelToken
|
|
13853
|
+
.withArgs('llm-default-session')
|
|
13854
|
+
.returns('token-123');
|
|
13143
13855
|
|
|
13144
13856
|
await meeting.updateLLMConnection();
|
|
13145
13857
|
|
|
@@ -13193,6 +13905,131 @@ describe('plugin-meetings', () => {
|
|
|
13193
13905
|
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
13194
13906
|
});
|
|
13195
13907
|
|
|
13908
|
+
describe('ownership tag', () => {
|
|
13909
|
+
beforeEach(() => {
|
|
13910
|
+
// Make the owner stub dynamic so setOwnerMeetingId() writes
|
|
13911
|
+
// propagate back to getOwnerMeetingId() reads. This mirrors the
|
|
13912
|
+
// real LLM singleton behavior so the finally-block release in
|
|
13913
|
+
// cleanupLLMConneciton is reflected in subsequent reads.
|
|
13914
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub().returns(undefined);
|
|
13915
|
+
webex.internal.llm.setOwnerMeetingId = sinon.stub().callsFake((id) => {
|
|
13916
|
+
webex.internal.llm.getOwnerMeetingId.returns(id);
|
|
13917
|
+
});
|
|
13918
|
+
});
|
|
13919
|
+
|
|
13920
|
+
it('skips disconnect and reconnect when LLM is connected and owned by another meeting (regardless of URL)', async () => {
|
|
13921
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13922
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13923
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
13924
|
+
// Locus/datachannel URL mismatch is the *normal* case when
|
|
13925
|
+
// another meeting owns the live socket -- each meeting has its
|
|
13926
|
+
// own locus URL. URL mismatch must NOT trigger a reclaim,
|
|
13927
|
+
// because doing so would tear down the owning meeting's healthy
|
|
13928
|
+
// LLM socket and break its data channel.
|
|
13929
|
+
webex.internal.llm.getLocusUrl.returns('owner-locus-url');
|
|
13930
|
+
webex.internal.llm.getDatachannelUrl.returns('owner-dc-url');
|
|
13931
|
+
meeting.locusInfo = {
|
|
13932
|
+
url: 'a different url',
|
|
13933
|
+
info: {datachannelUrl: 'a different datachannel url'},
|
|
13934
|
+
self: {},
|
|
13935
|
+
};
|
|
13936
|
+
|
|
13937
|
+
const result = await meeting.updateLLMConnection();
|
|
13938
|
+
|
|
13939
|
+
assert.equal(result, undefined);
|
|
13940
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
13941
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
13942
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
13943
|
+
assert.notCalled(meeting.startLLMHealthCheckTimer);
|
|
13944
|
+
});
|
|
13945
|
+
|
|
13946
|
+
|
|
13947
|
+
it('clears stale owner tag in cleanup finally block even when disconnectLLM rejects', async () => {
|
|
13948
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13949
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13950
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13951
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13952
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13953
|
+
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
13954
|
+
meeting.locusInfo = {
|
|
13955
|
+
url: 'a different url',
|
|
13956
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13957
|
+
self: {},
|
|
13958
|
+
};
|
|
13959
|
+
|
|
13960
|
+
try {
|
|
13961
|
+
await meeting.updateLLMConnection();
|
|
13962
|
+
} catch (e) {
|
|
13963
|
+
/* updateLLMConnection may reject when cleanup throws */
|
|
13964
|
+
}
|
|
13965
|
+
|
|
13966
|
+
// The owner-eligible finally branch must release the tag so a
|
|
13967
|
+
// subsequent reconnect attempt from any meeting is not blocked.
|
|
13968
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId, undefined);
|
|
13969
|
+
});
|
|
13970
|
+
|
|
13971
|
+
it('proceeds normally when LLM is connected and owned by this meeting with URL change', async () => {
|
|
13972
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13973
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13974
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13975
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13976
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13977
|
+
meeting.locusInfo = {
|
|
13978
|
+
url: 'a different url',
|
|
13979
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13980
|
+
self: {},
|
|
13981
|
+
};
|
|
13982
|
+
|
|
13983
|
+
await meeting.updateLLMConnection();
|
|
13984
|
+
|
|
13985
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13986
|
+
code: 3050,
|
|
13987
|
+
reason: 'done (permanent)',
|
|
13988
|
+
});
|
|
13989
|
+
assert.calledWithExactly(
|
|
13990
|
+
webex.internal.llm.registerAndConnect,
|
|
13991
|
+
'a different url',
|
|
13992
|
+
'a datachannel url',
|
|
13993
|
+
undefined
|
|
13994
|
+
);
|
|
13995
|
+
// setOwnerMeetingId is called twice: first with undefined in
|
|
13996
|
+
// cleanupLLMConneciton's finally block (so a failed disconnect
|
|
13997
|
+
// cannot leave a stale owner), then with this meeting's id
|
|
13998
|
+
// after registerAndConnect resolves.
|
|
13999
|
+
assert.calledTwice(webex.internal.llm.setOwnerMeetingId);
|
|
14000
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.firstCall, undefined);
|
|
14001
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.lastCall, meeting.id);
|
|
14002
|
+
});
|
|
14003
|
+
|
|
14004
|
+
it('claims ownership after successful registerAndConnect on initial connect', async () => {
|
|
14005
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
14006
|
+
webex.internal.llm.isConnected.returns(false);
|
|
14007
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
14008
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
14009
|
+
|
|
14010
|
+
await meeting.updateLLMConnection();
|
|
14011
|
+
|
|
14012
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
14013
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
14014
|
+
});
|
|
14015
|
+
|
|
14016
|
+
it('proceeds to connect when LLM is not connected even if another ownerId lingers', async () => {
|
|
14017
|
+
// Defensive path: if the LLM reports not-connected but an old
|
|
14018
|
+
// ownerId is still present (e.g. race before a successful
|
|
14019
|
+
// connections.delete), this meeting can still claim a fresh
|
|
14020
|
+
// connection.
|
|
14021
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
14022
|
+
webex.internal.llm.isConnected.returns(false);
|
|
14023
|
+
webex.internal.llm.getOwnerMeetingId.returns('stale-owner-id');
|
|
14024
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
14025
|
+
|
|
14026
|
+
await meeting.updateLLMConnection();
|
|
14027
|
+
|
|
14028
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
14029
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
14030
|
+
});
|
|
14031
|
+
});
|
|
14032
|
+
|
|
13196
14033
|
describe('#clearMeetingData', () => {
|
|
13197
14034
|
beforeEach(() => {
|
|
13198
14035
|
webex.internal.llm.isConnected = sinon.stub().returns(true);
|
|
@@ -13224,10 +14061,13 @@ describe('plugin-meetings', () => {
|
|
|
13224
14061
|
meeting.processLocusLLMEvent
|
|
13225
14062
|
);
|
|
13226
14063
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13227
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13228
|
-
assert.isUndefined(meeting.transcription);
|
|
13229
14064
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13230
|
-
|
|
14065
|
+
// stopTranscription and annotation.deregisterEvents are not
|
|
14066
|
+
// called here: they run in stopListeningForMeetingEvents()
|
|
14067
|
+
// before /leave to avoid double-emitting
|
|
14068
|
+
// MEETING_STOPPED_RECEIVING_TRANSCRIPTION.
|
|
14069
|
+
assert.notCalled(meeting.stopTranscription);
|
|
14070
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13231
14071
|
});
|
|
13232
14072
|
it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
|
|
13233
14073
|
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
@@ -13246,19 +14086,67 @@ describe('plugin-meetings', () => {
|
|
|
13246
14086
|
meeting.processLocusLLMEvent
|
|
13247
14087
|
);
|
|
13248
14088
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13249
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13250
|
-
assert.isUndefined(meeting.transcription);
|
|
13251
14089
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13252
|
-
assert.
|
|
14090
|
+
assert.notCalled(meeting.stopTranscription);
|
|
14091
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13253
14092
|
});
|
|
13254
|
-
it('always calls stopTranscription even when transcription is undefined', async () => {
|
|
13255
|
-
meeting.transcription = undefined;
|
|
13256
14093
|
|
|
13257
|
-
|
|
14094
|
+
describe('ownership tag', () => {
|
|
14095
|
+
beforeEach(() => {
|
|
14096
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub();
|
|
14097
|
+
});
|
|
13258
14098
|
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
14099
|
+
it('skips disconnectLLM but still removes this meeting listeners when another meeting owns the LLM', async () => {
|
|
14100
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
14101
|
+
|
|
14102
|
+
await meeting.clearMeetingData();
|
|
14103
|
+
|
|
14104
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
14105
|
+
// Shared data-channel auth tokens belong to the owner meeting's
|
|
14106
|
+
// live LLM session and must not be wiped by a non-owner
|
|
14107
|
+
// teardown, otherwise the owner's next reconnect would lose
|
|
14108
|
+
// its Data-Channel-Auth-Token.
|
|
14109
|
+
assert.notCalled(meeting.clearDataChannelToken);
|
|
14110
|
+
// Listeners owned by *this* Meeting instance must still be
|
|
14111
|
+
// removed so a leaving subordinate meeting stops receiving
|
|
14112
|
+
// relay/locus events from the shared singleton.
|
|
14113
|
+
assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
14114
|
+
assert.calledWithExactly(
|
|
14115
|
+
webex.internal.llm.off,
|
|
14116
|
+
'event:relay.event',
|
|
14117
|
+
meeting.processRelayEvent
|
|
14118
|
+
);
|
|
14119
|
+
assert.calledWithExactly(
|
|
14120
|
+
webex.internal.llm.off,
|
|
14121
|
+
'event:locus.state_message',
|
|
14122
|
+
meeting.processLocusLLMEvent
|
|
14123
|
+
);
|
|
14124
|
+
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
14125
|
+
});
|
|
14126
|
+
|
|
14127
|
+
it('calls disconnectLLM and clears data channel token when this meeting is the owner', async () => {
|
|
14128
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
14129
|
+
|
|
14130
|
+
await meeting.clearMeetingData();
|
|
14131
|
+
|
|
14132
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
14133
|
+
code: 3050,
|
|
14134
|
+
reason: 'done (permanent)',
|
|
14135
|
+
});
|
|
14136
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
14137
|
+
});
|
|
14138
|
+
|
|
14139
|
+
it('calls disconnectLLM and clears data channel token when no owner is recorded (first-claim / legacy)', async () => {
|
|
14140
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
14141
|
+
|
|
14142
|
+
await meeting.clearMeetingData();
|
|
14143
|
+
|
|
14144
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
14145
|
+
code: 3050,
|
|
14146
|
+
reason: 'done (permanent)',
|
|
14147
|
+
});
|
|
14148
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
14149
|
+
});
|
|
13262
14150
|
});
|
|
13263
14151
|
});
|
|
13264
14152
|
});
|
|
@@ -16045,4 +16933,4 @@ describe('plugin-meetings', () => {
|
|
|
16045
16933
|
assert.calledOnceWithExactly(meeting.meetingRequest.cancelSipCallOut, participantId);
|
|
16046
16934
|
});
|
|
16047
16935
|
});
|
|
16048
|
-
});
|
|
16936
|
+
});
|