@webex/plugin-meetings 3.12.0-next.44 → 3.12.0-next.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +94 -20
- package/dist/meeting/index.js.map +1 -1
- package/dist/types/meeting/index.d.ts +6 -0
- package/dist/webinar/index.js +63 -17
- package/dist/webinar/index.js.map +1 -1
- package/package.json +3 -3
- package/src/meeting/index.ts +79 -6
- package/src/webinar/index.ts +75 -17
- package/test/unit/spec/meeting/index.js +183 -0
- package/test/unit/spec/webinar/index.ts +81 -16
package/src/webinar/index.ts
CHANGED
|
@@ -98,13 +98,49 @@ const Webinar = WebexPlugin.extend({
|
|
|
98
98
|
return {isPromoted, isDemoted};
|
|
99
99
|
},
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Resolves the meeting associated with this webinar instance, guarded against the
|
|
103
|
+
* meetingId pointer drifting onto an unrelated transient meeting (e.g. an inbound
|
|
104
|
+
* 1:1 call) that may exist in the meeting collection. Returns the meeting only when
|
|
105
|
+
* its locusUrl matches this webinar's tracked locusUrl. Returns undefined (with a
|
|
106
|
+
* warning) when the meeting cannot be resolved or when the webinar's locusUrl has
|
|
107
|
+
* not been initialized yet — callers must treat this as "no owned meeting" rather
|
|
108
|
+
* than fall through to an unvalidated lookup.
|
|
109
|
+
* @returns {object|undefined}
|
|
110
|
+
*/
|
|
111
|
+
getValidatedWebinarMeeting() {
|
|
112
|
+
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
|
|
113
|
+
|
|
114
|
+
if (!meeting) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!this.locusUrl) {
|
|
119
|
+
LoggerProxy.logger.warn(
|
|
120
|
+
`Webinar:index#getValidatedWebinarMeeting --> skipping; webinar locusUrl is not yet initialized for meetingId ${this.meetingId}`
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (meeting.locusUrl !== this.locusUrl) {
|
|
127
|
+
LoggerProxy.logger.warn(
|
|
128
|
+
`Webinar:index#getValidatedWebinarMeeting --> skipping; meeting ${this.meetingId} locusUrl ${meeting.locusUrl} does not match webinar locusUrl ${this.locusUrl}`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return meeting;
|
|
135
|
+
},
|
|
136
|
+
|
|
101
137
|
/**
|
|
102
138
|
* should join practice session data channel or not
|
|
103
139
|
* @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
|
|
104
140
|
* @returns {void}
|
|
105
141
|
*/
|
|
106
142
|
updateStatusByRole({isPromoted, isDemoted}) {
|
|
107
|
-
const meeting = this.
|
|
143
|
+
const meeting = this.getValidatedWebinarMeeting();
|
|
108
144
|
|
|
109
145
|
if (
|
|
110
146
|
(isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
|
|
@@ -128,6 +164,9 @@ const Webinar = WebexPlugin.extend({
|
|
|
128
164
|
|
|
129
165
|
/**
|
|
130
166
|
* Disconnects the practice session data channel and removes its relay listener.
|
|
167
|
+
* The listener reference removed here is the exact callback captured at subscribe
|
|
168
|
+
* time (see updatePSDataChannel) so that cleanup is correct even if the underlying
|
|
169
|
+
* meeting can no longer be resolved (e.g. locusUrl mismatch).
|
|
131
170
|
* @returns {Promise<void>}
|
|
132
171
|
*/
|
|
133
172
|
async cleanupPSDataChannel() {
|
|
@@ -137,8 +176,6 @@ const Webinar = WebexPlugin.extend({
|
|
|
137
176
|
this._pendingOnlineListener = null;
|
|
138
177
|
}
|
|
139
178
|
|
|
140
|
-
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
|
|
141
|
-
|
|
142
179
|
// @ts-ignore - Fix type
|
|
143
180
|
await this.webex.internal.llm.disconnectLLM(
|
|
144
181
|
{
|
|
@@ -147,15 +184,21 @@ const Webinar = WebexPlugin.extend({
|
|
|
147
184
|
},
|
|
148
185
|
LLM_PRACTICE_SESSION
|
|
149
186
|
);
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
187
|
+
|
|
188
|
+
if (this._practiceSessionRelayListener) {
|
|
189
|
+
// @ts-ignore - Fix type
|
|
190
|
+
this.webex.internal.llm.off(
|
|
191
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
192
|
+
this._practiceSessionRelayListener
|
|
193
|
+
);
|
|
194
|
+
this._practiceSessionRelayListener = null;
|
|
195
|
+
}
|
|
155
196
|
},
|
|
156
197
|
|
|
157
198
|
/**
|
|
158
199
|
* Ensures practice-session token exists before registering the practice LLM channel.
|
|
200
|
+
* Caller is responsible for passing a meeting that has already been resolved via
|
|
201
|
+
* getValidatedWebinarMeeting() — this method does not re-validate ownership.
|
|
159
202
|
* @param {object} meeting
|
|
160
203
|
* @returns {Promise<string|undefined>}
|
|
161
204
|
*/
|
|
@@ -211,7 +254,7 @@ const Webinar = WebexPlugin.extend({
|
|
|
211
254
|
this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
|
|
212
255
|
const invocationSequence = this._updatePSDataChannelSequence;
|
|
213
256
|
|
|
214
|
-
const meeting = this.
|
|
257
|
+
const meeting = this.getValidatedWebinarMeeting();
|
|
215
258
|
const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
|
|
216
259
|
|
|
217
260
|
if (!isPracticeSession) {
|
|
@@ -312,15 +355,21 @@ const Webinar = WebexPlugin.extend({
|
|
|
312
355
|
LLM_PRACTICE_SESSION
|
|
313
356
|
)
|
|
314
357
|
.then((registerAndConnectResult) => {
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
358
|
+
// Track the exact listener reference so cleanupPSDataChannel can
|
|
359
|
+
// unsubscribe deterministically, even if the meeting can no longer
|
|
360
|
+
// be resolved at cleanup time.
|
|
361
|
+
if (this._practiceSessionRelayListener) {
|
|
362
|
+
// @ts-ignore - Fix type
|
|
363
|
+
this.webex.internal.llm.off(
|
|
364
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
365
|
+
this._practiceSessionRelayListener
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
this._practiceSessionRelayListener = meeting?.processRelayEvent;
|
|
320
369
|
// @ts-ignore - Fix type
|
|
321
370
|
this.webex.internal.llm.on(
|
|
322
371
|
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
323
|
-
|
|
372
|
+
this._practiceSessionRelayListener
|
|
324
373
|
);
|
|
325
374
|
// @ts-ignore - Fix type
|
|
326
375
|
this.webex.internal.voicea?.announce?.();
|
|
@@ -532,7 +581,14 @@ const Webinar = WebexPlugin.extend({
|
|
|
532
581
|
* @returns {Promise}
|
|
533
582
|
*/
|
|
534
583
|
async searchLargeScaleWebinarAttendees(payload) {
|
|
535
|
-
const meeting = this.
|
|
584
|
+
const meeting = this.getValidatedWebinarMeeting();
|
|
585
|
+
if (!meeting) {
|
|
586
|
+
LoggerProxy.logger.error(
|
|
587
|
+
'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> webinar meeting could not be validated'
|
|
588
|
+
);
|
|
589
|
+
throw new Error('Meeting:webinar5k#Webinar meeting is not resolvable for the current locus');
|
|
590
|
+
}
|
|
591
|
+
|
|
536
592
|
const rawParams = {
|
|
537
593
|
search_text: payload?.queryString,
|
|
538
594
|
limit: payload?.limit ?? DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
|
|
@@ -540,7 +596,9 @@ const Webinar = WebexPlugin.extend({
|
|
|
540
596
|
};
|
|
541
597
|
const attendeeSearchUrl = meeting?.locusInfo?.links?.resources?.attendeeSearch?.url;
|
|
542
598
|
if (!attendeeSearchUrl) {
|
|
543
|
-
LoggerProxy.logger.error(
|
|
599
|
+
LoggerProxy.logger.error(
|
|
600
|
+
'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> attendee search url unavailable'
|
|
601
|
+
);
|
|
544
602
|
throw new Error('Meeting:webinar5k#Attendee search url is not available');
|
|
545
603
|
}
|
|
546
604
|
|
|
@@ -13703,6 +13703,131 @@ describe('plugin-meetings', () => {
|
|
|
13703
13703
|
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
13704
13704
|
});
|
|
13705
13705
|
|
|
13706
|
+
describe('ownership tag', () => {
|
|
13707
|
+
beforeEach(() => {
|
|
13708
|
+
// Make the owner stub dynamic so setOwnerMeetingId() writes
|
|
13709
|
+
// propagate back to getOwnerMeetingId() reads. This mirrors the
|
|
13710
|
+
// real LLM singleton behavior so the finally-block release in
|
|
13711
|
+
// cleanupLLMConneciton is reflected in subsequent reads.
|
|
13712
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub().returns(undefined);
|
|
13713
|
+
webex.internal.llm.setOwnerMeetingId = sinon.stub().callsFake((id) => {
|
|
13714
|
+
webex.internal.llm.getOwnerMeetingId.returns(id);
|
|
13715
|
+
});
|
|
13716
|
+
});
|
|
13717
|
+
|
|
13718
|
+
it('skips disconnect and reconnect when LLM is connected and owned by another meeting (regardless of URL)', async () => {
|
|
13719
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13720
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13721
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
13722
|
+
// Locus/datachannel URL mismatch is the *normal* case when
|
|
13723
|
+
// another meeting owns the live socket -- each meeting has its
|
|
13724
|
+
// own locus URL. URL mismatch must NOT trigger a reclaim,
|
|
13725
|
+
// because doing so would tear down the owning meeting's healthy
|
|
13726
|
+
// LLM socket and break its data channel.
|
|
13727
|
+
webex.internal.llm.getLocusUrl.returns('owner-locus-url');
|
|
13728
|
+
webex.internal.llm.getDatachannelUrl.returns('owner-dc-url');
|
|
13729
|
+
meeting.locusInfo = {
|
|
13730
|
+
url: 'a different url',
|
|
13731
|
+
info: {datachannelUrl: 'a different datachannel url'},
|
|
13732
|
+
self: {},
|
|
13733
|
+
};
|
|
13734
|
+
|
|
13735
|
+
const result = await meeting.updateLLMConnection();
|
|
13736
|
+
|
|
13737
|
+
assert.equal(result, undefined);
|
|
13738
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
13739
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
13740
|
+
assert.notCalled(webex.internal.llm.setOwnerMeetingId);
|
|
13741
|
+
assert.notCalled(meeting.startLLMHealthCheckTimer);
|
|
13742
|
+
});
|
|
13743
|
+
|
|
13744
|
+
|
|
13745
|
+
it('clears stale owner tag in cleanup finally block even when disconnectLLM rejects', async () => {
|
|
13746
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13747
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13748
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13749
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13750
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13751
|
+
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
13752
|
+
meeting.locusInfo = {
|
|
13753
|
+
url: 'a different url',
|
|
13754
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13755
|
+
self: {},
|
|
13756
|
+
};
|
|
13757
|
+
|
|
13758
|
+
try {
|
|
13759
|
+
await meeting.updateLLMConnection();
|
|
13760
|
+
} catch (e) {
|
|
13761
|
+
/* updateLLMConnection may reject when cleanup throws */
|
|
13762
|
+
}
|
|
13763
|
+
|
|
13764
|
+
// The owner-eligible finally branch must release the tag so a
|
|
13765
|
+
// subsequent reconnect attempt from any meeting is not blocked.
|
|
13766
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId, undefined);
|
|
13767
|
+
});
|
|
13768
|
+
|
|
13769
|
+
it('proceeds normally when LLM is connected and owned by this meeting with URL change', async () => {
|
|
13770
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13771
|
+
webex.internal.llm.isConnected.returns(true);
|
|
13772
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13773
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
13774
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
13775
|
+
meeting.locusInfo = {
|
|
13776
|
+
url: 'a different url',
|
|
13777
|
+
info: {datachannelUrl: 'a datachannel url'},
|
|
13778
|
+
self: {},
|
|
13779
|
+
};
|
|
13780
|
+
|
|
13781
|
+
await meeting.updateLLMConnection();
|
|
13782
|
+
|
|
13783
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13784
|
+
code: 3050,
|
|
13785
|
+
reason: 'done (permanent)',
|
|
13786
|
+
});
|
|
13787
|
+
assert.calledWithExactly(
|
|
13788
|
+
webex.internal.llm.registerAndConnect,
|
|
13789
|
+
'a different url',
|
|
13790
|
+
'a datachannel url',
|
|
13791
|
+
undefined
|
|
13792
|
+
);
|
|
13793
|
+
// setOwnerMeetingId is called twice: first with undefined in
|
|
13794
|
+
// cleanupLLMConneciton's finally block (so a failed disconnect
|
|
13795
|
+
// cannot leave a stale owner), then with this meeting's id
|
|
13796
|
+
// after registerAndConnect resolves.
|
|
13797
|
+
assert.calledTwice(webex.internal.llm.setOwnerMeetingId);
|
|
13798
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.firstCall, undefined);
|
|
13799
|
+
assert.calledWith(webex.internal.llm.setOwnerMeetingId.lastCall, meeting.id);
|
|
13800
|
+
});
|
|
13801
|
+
|
|
13802
|
+
it('claims ownership after successful registerAndConnect on initial connect', async () => {
|
|
13803
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13804
|
+
webex.internal.llm.isConnected.returns(false);
|
|
13805
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
13806
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13807
|
+
|
|
13808
|
+
await meeting.updateLLMConnection();
|
|
13809
|
+
|
|
13810
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
13811
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
13812
|
+
});
|
|
13813
|
+
|
|
13814
|
+
it('proceeds to connect when LLM is not connected even if another ownerId lingers', async () => {
|
|
13815
|
+
// Defensive path: if the LLM reports not-connected but an old
|
|
13816
|
+
// ownerId is still present (e.g. race before a successful
|
|
13817
|
+
// connections.delete), this meeting can still claim a fresh
|
|
13818
|
+
// connection.
|
|
13819
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
13820
|
+
webex.internal.llm.isConnected.returns(false);
|
|
13821
|
+
webex.internal.llm.getOwnerMeetingId.returns('stale-owner-id');
|
|
13822
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
13823
|
+
|
|
13824
|
+
await meeting.updateLLMConnection();
|
|
13825
|
+
|
|
13826
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
13827
|
+
assert.calledOnceWithExactly(webex.internal.llm.setOwnerMeetingId, meeting.id);
|
|
13828
|
+
});
|
|
13829
|
+
});
|
|
13830
|
+
|
|
13706
13831
|
describe('#clearMeetingData', () => {
|
|
13707
13832
|
beforeEach(() => {
|
|
13708
13833
|
webex.internal.llm.isConnected = sinon.stub().returns(true);
|
|
@@ -13763,6 +13888,64 @@ describe('plugin-meetings', () => {
|
|
|
13763
13888
|
assert.notCalled(meeting.stopTranscription);
|
|
13764
13889
|
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13765
13890
|
});
|
|
13891
|
+
|
|
13892
|
+
describe('ownership tag', () => {
|
|
13893
|
+
beforeEach(() => {
|
|
13894
|
+
webex.internal.llm.getOwnerMeetingId = sinon.stub();
|
|
13895
|
+
});
|
|
13896
|
+
|
|
13897
|
+
it('skips disconnectLLM but still removes this meeting listeners when another meeting owns the LLM', async () => {
|
|
13898
|
+
webex.internal.llm.getOwnerMeetingId.returns('some-other-meeting-id');
|
|
13899
|
+
|
|
13900
|
+
await meeting.clearMeetingData();
|
|
13901
|
+
|
|
13902
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
13903
|
+
// Shared data-channel auth tokens belong to the owner meeting's
|
|
13904
|
+
// live LLM session and must not be wiped by a non-owner
|
|
13905
|
+
// teardown, otherwise the owner's next reconnect would lose
|
|
13906
|
+
// its Data-Channel-Auth-Token.
|
|
13907
|
+
assert.notCalled(meeting.clearDataChannelToken);
|
|
13908
|
+
// Listeners owned by *this* Meeting instance must still be
|
|
13909
|
+
// removed so a leaving subordinate meeting stops receiving
|
|
13910
|
+
// relay/locus events from the shared singleton.
|
|
13911
|
+
assert.calledWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
13912
|
+
assert.calledWithExactly(
|
|
13913
|
+
webex.internal.llm.off,
|
|
13914
|
+
'event:relay.event',
|
|
13915
|
+
meeting.processRelayEvent
|
|
13916
|
+
);
|
|
13917
|
+
assert.calledWithExactly(
|
|
13918
|
+
webex.internal.llm.off,
|
|
13919
|
+
'event:locus.state_message',
|
|
13920
|
+
meeting.processLocusLLMEvent
|
|
13921
|
+
);
|
|
13922
|
+
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13923
|
+
});
|
|
13924
|
+
|
|
13925
|
+
it('calls disconnectLLM and clears data channel token when this meeting is the owner', async () => {
|
|
13926
|
+
webex.internal.llm.getOwnerMeetingId.returns(meeting.id);
|
|
13927
|
+
|
|
13928
|
+
await meeting.clearMeetingData();
|
|
13929
|
+
|
|
13930
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13931
|
+
code: 3050,
|
|
13932
|
+
reason: 'done (permanent)',
|
|
13933
|
+
});
|
|
13934
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13935
|
+
});
|
|
13936
|
+
|
|
13937
|
+
it('calls disconnectLLM and clears data channel token when no owner is recorded (first-claim / legacy)', async () => {
|
|
13938
|
+
webex.internal.llm.getOwnerMeetingId.returns(undefined);
|
|
13939
|
+
|
|
13940
|
+
await meeting.clearMeetingData();
|
|
13941
|
+
|
|
13942
|
+
assert.calledOnceWithExactly(webex.internal.llm.disconnectLLM, {
|
|
13943
|
+
code: 3050,
|
|
13944
|
+
reason: 'done (permanent)',
|
|
13945
|
+
});
|
|
13946
|
+
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13947
|
+
});
|
|
13948
|
+
});
|
|
13766
13949
|
});
|
|
13767
13950
|
});
|
|
13768
13951
|
|
|
@@ -176,6 +176,42 @@ describe('plugin-meetings', () => {
|
|
|
176
176
|
});
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
describe('#getValidatedWebinarMeeting', () => {
|
|
180
|
+
it('returns the meeting when its locusUrl matches the webinar locusUrl', () => {
|
|
181
|
+
const meeting = {locusUrl: 'locusUrl'};
|
|
182
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
183
|
+
webinar.locusUrl = 'locusUrl';
|
|
184
|
+
|
|
185
|
+
assert.equal(webinar.getValidatedWebinarMeeting(), meeting);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns undefined and warns when the resolved meeting locusUrl does not match', () => {
|
|
189
|
+
const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
|
|
190
|
+
const meeting = {locusUrl: 'other-locus-url'};
|
|
191
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
192
|
+
webinar.locusUrl = 'locusUrl';
|
|
193
|
+
|
|
194
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
195
|
+
assert.calledOnce(warnStub);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('returns undefined when no meeting is resolved', () => {
|
|
199
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(undefined);
|
|
200
|
+
|
|
201
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('returns undefined and warns when webinar locusUrl is not yet initialized', () => {
|
|
205
|
+
const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
|
|
206
|
+
const meeting = {locusUrl: 'some-url'};
|
|
207
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
208
|
+
webinar.locusUrl = undefined;
|
|
209
|
+
|
|
210
|
+
assert.isUndefined(webinar.getValidatedWebinarMeeting());
|
|
211
|
+
assert.calledOnce(warnStub);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
179
215
|
describe('#cleanUp', () => {
|
|
180
216
|
it('delegates to cleanupPSDataChannel', () => {
|
|
181
217
|
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
@@ -187,17 +223,14 @@ describe('plugin-meetings', () => {
|
|
|
187
223
|
});
|
|
188
224
|
|
|
189
225
|
describe('#cleanupPSDataChannel', () => {
|
|
190
|
-
let
|
|
226
|
+
let relayListener;
|
|
191
227
|
|
|
192
228
|
beforeEach(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
229
|
+
relayListener = sinon.stub();
|
|
230
|
+
webinar._practiceSessionRelayListener = relayListener;
|
|
198
231
|
});
|
|
199
232
|
|
|
200
|
-
it('disconnects the practice session channel and removes the relay listener', async () => {
|
|
233
|
+
it('disconnects the practice session channel and removes the tracked relay listener', async () => {
|
|
201
234
|
await webinar.cleanupPSDataChannel();
|
|
202
235
|
|
|
203
236
|
assert.calledOnceWithExactly(
|
|
@@ -208,8 +241,28 @@ describe('plugin-meetings', () => {
|
|
|
208
241
|
assert.calledOnceWithExactly(
|
|
209
242
|
webex.internal.llm.off,
|
|
210
243
|
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
211
|
-
|
|
244
|
+
relayListener
|
|
212
245
|
);
|
|
246
|
+
assert.isNull(webinar._practiceSessionRelayListener);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('skips relay listener removal when no listener has been tracked', async () => {
|
|
250
|
+
webinar._practiceSessionRelayListener = null;
|
|
251
|
+
|
|
252
|
+
await webinar.cleanupPSDataChannel();
|
|
253
|
+
|
|
254
|
+
const relayOffCalls = webex.internal.llm.off.args.filter(
|
|
255
|
+
([event]) => event === `event:relay.event:${LLM_PRACTICE_SESSION}`
|
|
256
|
+
);
|
|
257
|
+
assert.equal(relayOffCalls.length, 0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('does not consult the meeting collection during cleanup', async () => {
|
|
261
|
+
webex.meetings.getMeetingByType = sinon.stub();
|
|
262
|
+
|
|
263
|
+
await webinar.cleanupPSDataChannel();
|
|
264
|
+
|
|
265
|
+
assert.notCalled(webex.meetings.getMeetingByType);
|
|
213
266
|
});
|
|
214
267
|
|
|
215
268
|
it('removes a pending online listener if one exists', async () => {
|
|
@@ -240,6 +293,7 @@ describe('plugin-meetings', () => {
|
|
|
240
293
|
beforeEach(() => {
|
|
241
294
|
processRelayEvent = sinon.stub();
|
|
242
295
|
meeting = {
|
|
296
|
+
locusUrl: 'locusUrl',
|
|
243
297
|
isJoined: sinon.stub().returns(true),
|
|
244
298
|
processRelayEvent,
|
|
245
299
|
locusInfo: {
|
|
@@ -418,19 +472,30 @@ describe('plugin-meetings', () => {
|
|
|
418
472
|
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
419
473
|
});
|
|
420
474
|
|
|
421
|
-
it('
|
|
475
|
+
it('tracks and binds the relay listener after successful connect', async () => {
|
|
422
476
|
await webinar.updatePSDataChannel();
|
|
423
477
|
|
|
478
|
+
// Stores the exact listener reference for deterministic cleanup
|
|
479
|
+
assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
|
|
424
480
|
assert.calledWith(
|
|
425
|
-
webex.internal.llm.
|
|
481
|
+
webex.internal.llm.on,
|
|
426
482
|
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
427
483
|
processRelayEvent
|
|
428
484
|
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('removes a previously tracked relay listener before re-binding on reconnect', async () => {
|
|
488
|
+
const previousListener = sinon.stub();
|
|
489
|
+
webinar._practiceSessionRelayListener = previousListener;
|
|
490
|
+
|
|
491
|
+
await webinar.updatePSDataChannel();
|
|
492
|
+
|
|
429
493
|
assert.calledWith(
|
|
430
|
-
webex.internal.llm.
|
|
494
|
+
webex.internal.llm.off,
|
|
431
495
|
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
432
|
-
|
|
496
|
+
previousListener
|
|
433
497
|
);
|
|
498
|
+
assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
|
|
434
499
|
});
|
|
435
500
|
|
|
436
501
|
it('subscribes to transcription when caption intent is enabled', async () => {
|
|
@@ -551,7 +616,7 @@ describe('plugin-meetings', () => {
|
|
|
551
616
|
updateMediaShares = sinon.stub()
|
|
552
617
|
webinar.webex.meetings = {
|
|
553
618
|
getMeetingByType: sinon.stub().returns({
|
|
554
|
-
id: 'meeting-id',
|
|
619
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
555
620
|
isJoined: sinon.stub().returns(false),
|
|
556
621
|
updateLLMConnection: sinon.stub(),
|
|
557
622
|
shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
|
|
@@ -606,7 +671,7 @@ describe('plugin-meetings', () => {
|
|
|
606
671
|
|
|
607
672
|
webinar.webex.meetings = {
|
|
608
673
|
getMeetingByType: sinon.stub().returns({
|
|
609
|
-
id: 'meeting-id',
|
|
674
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
610
675
|
isJoined: sinon.stub().returns(false),
|
|
611
676
|
updateLLMConnection: sinon.stub(),
|
|
612
677
|
shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
|
|
@@ -1034,7 +1099,7 @@ describe('plugin-meetings', () => {
|
|
|
1034
1099
|
// @ts-ignore
|
|
1035
1100
|
webinar.webex.meetings = {
|
|
1036
1101
|
getMeetingByType: sinon.stub().returns({
|
|
1037
|
-
id: 'meeting-id',
|
|
1102
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
1038
1103
|
locusInfo: {
|
|
1039
1104
|
links:{
|
|
1040
1105
|
resources: {
|
|
@@ -1051,7 +1116,7 @@ describe('plugin-meetings', () => {
|
|
|
1051
1116
|
it('throws an error if attendeeSearchUrl is not available', async () => {
|
|
1052
1117
|
webinar.webex.meetings = {
|
|
1053
1118
|
getMeetingByType: sinon.stub().returns({
|
|
1054
|
-
id: 'meeting-id',
|
|
1119
|
+
id: 'meeting-id', locusUrl: 'locusUrl',
|
|
1055
1120
|
locusInfo: {
|
|
1056
1121
|
links:{
|
|
1057
1122
|
resources: {
|