@webex/plugin-meetings 3.12.0-next.44 → 3.12.0-next.46

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.
Files changed (33) hide show
  1. package/dist/aiEnableRequest/index.js +1 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/controls-options-manager/index.js +17 -5
  5. package/dist/controls-options-manager/index.js.map +1 -1
  6. package/dist/interpretation/index.js +10 -1
  7. package/dist/interpretation/index.js.map +1 -1
  8. package/dist/interpretation/siLanguage.js +1 -1
  9. package/dist/meeting/index.js +94 -20
  10. package/dist/meeting/index.js.map +1 -1
  11. package/dist/meeting/util.js +15 -2
  12. package/dist/meeting/util.js.map +1 -1
  13. package/dist/recording-controller/index.js +1 -3
  14. package/dist/recording-controller/index.js.map +1 -1
  15. package/dist/types/controls-options-manager/index.d.ts +10 -0
  16. package/dist/types/meeting/index.d.ts +6 -0
  17. package/dist/types/meeting/util.d.ts +7 -0
  18. package/dist/webinar/index.js +68 -17
  19. package/dist/webinar/index.js.map +1 -1
  20. package/package.json +3 -3
  21. package/src/controls-options-manager/index.ts +22 -6
  22. package/src/interpretation/index.ts +25 -8
  23. package/src/meeting/index.ts +79 -6
  24. package/src/meeting/util.ts +16 -2
  25. package/src/recording-controller/index.ts +1 -2
  26. package/src/webinar/index.ts +88 -21
  27. package/test/unit/spec/controls-options-manager/index.js +35 -32
  28. package/test/unit/spec/interpretation/index.ts +26 -4
  29. package/test/unit/spec/meeting/index.js +183 -0
  30. package/test/unit/spec/meeting/muteState.js +3 -0
  31. package/test/unit/spec/meeting/utils.js +25 -0
  32. package/test/unit/spec/recording-controller/index.js +9 -8
  33. package/test/unit/spec/webinar/index.ts +81 -16
@@ -6407,6 +6407,12 @@ export default class Meeting extends StatelessWebexPlugin {
6407
6407
 
6408
6408
  /**
6409
6409
  * Disconnects and cleans up the default LLM session listeners/timers.
6410
+ *
6411
+ * Ownership-aware: only calls `disconnectLLM` when this meeting is the
6412
+ * current owner of the default LLM session (or when no owner is recorded).
6413
+ * Event listeners belonging to this meeting instance are always detached
6414
+ * so they do not receive another meeting's relay events.
6415
+ *
6410
6416
  * @param {Object} options
6411
6417
  * @param {boolean} [options.removeOnlineListener=true] removes the one-time online listener
6412
6418
  * @param {boolean} [options.throwOnError=true] rethrows disconnect errors when true
@@ -6419,12 +6425,22 @@ export default class Meeting extends StatelessWebexPlugin {
6419
6425
  removeOnlineListener?: boolean;
6420
6426
  throwOnError?: boolean;
6421
6427
  } = {}): Promise<void> => {
6428
+ // @ts-ignore - Fix type
6429
+ const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
6430
+ const isOwner = !currentOwner || currentOwner === this.id;
6431
+
6422
6432
  try {
6423
- // @ts-ignore - Fix type
6424
- await this.webex.internal.llm.disconnectLLM({
6425
- code: 3050,
6426
- reason: 'done (permanent)',
6427
- });
6433
+ if (isOwner) {
6434
+ // @ts-ignore - Fix type
6435
+ await this.webex.internal.llm.disconnectLLM({
6436
+ code: 3050,
6437
+ reason: 'done (permanent)',
6438
+ });
6439
+ } else {
6440
+ LoggerProxy.logger.info(
6441
+ `Meeting:index#cleanupLLMConneciton --> skipping disconnect; LLM owned by meeting ${currentOwner}, not ${this.id}`
6442
+ );
6443
+ }
6428
6444
  } catch (error) {
6429
6445
  LoggerProxy.logger.error(
6430
6446
  'Meeting:index#cleanupLLMConneciton --> Failed to disconnect default LLM session',
@@ -6440,6 +6456,17 @@ export default class Meeting extends StatelessWebexPlugin {
6440
6456
  this.webex.internal.llm.off('online', this.handleLLMOnline);
6441
6457
  }
6442
6458
  this.stopListeningForLLMEvents();
6459
+
6460
+ // If this meeting owned (or could have owned) the default LLM session,
6461
+ // always release the owner tag here regardless of whether disconnectLLM
6462
+ // resolved. `disconnectLLM` only clears the owner on its success path,
6463
+ // so a failed disconnect would otherwise leave a stale owner pointing
6464
+ // at a torn-down meeting and permanently block other meetings'
6465
+ // `updateLLMConnection` calls via the ownership guard.
6466
+ if (isOwner) {
6467
+ // @ts-ignore - Fix type
6468
+ this.webex.internal.llm.setOwnerMeetingId?.(undefined);
6469
+ }
6443
6470
  }
6444
6471
  };
6445
6472
 
@@ -6541,8 +6568,33 @@ export default class Meeting extends StatelessWebexPlugin {
6541
6568
 
6542
6569
  const dataChannelUrl = datachannelUrl;
6543
6570
 
6571
+ // Ownership guard: when the default LLM session is already connected and
6572
+ // owned by a *different* Meeting instance, do not disconnect or reconfigure
6573
+ // it. Another meeting's `updateLLMConnection` must be ignored here to
6574
+ // avoid killing the socket it relies on. We only proceed to manage the
6575
+ // connection when this meeting is the current owner, or when no owner is
6576
+ // set yet (first claim).
6577
+ // @ts-ignore - Fix type
6578
+ const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
6579
+
6544
6580
  // @ts-ignore - Fix type
6545
6581
  if (this.webex.internal.llm.isConnected()) {
6582
+ if (currentOwner && currentOwner !== this.id) {
6583
+ // Another meeting owns the live LLM socket. We must not disconnect
6584
+ // or reconfigure it -- doing so would tear down a session the
6585
+ // owning meeting still relies on. Locus/datachannel URL mismatch is
6586
+ // expected here (each meeting has its own locus URL) and is NOT a
6587
+ // valid signal of staleness, so we never reclaim from this path.
6588
+ // The only safe reclaim mechanism is the `finally`-block owner-tag
6589
+ // release in `cleanupLLMConneciton`, which fires when this meeting
6590
+ // itself is being torn down.
6591
+ LoggerProxy.logger.info(
6592
+ `Meeting:index#updateLLMConnection --> skipping; LLM owned by meeting ${currentOwner}, not ${this.id}`
6593
+ );
6594
+
6595
+ return undefined;
6596
+ }
6597
+
6546
6598
  if (
6547
6599
  // @ts-ignore - Fix type
6548
6600
  url === this.webex.internal.llm.getLocusUrl() &&
@@ -6563,6 +6615,11 @@ export default class Meeting extends StatelessWebexPlugin {
6563
6615
  return this.webex.internal.llm
6564
6616
  .registerAndConnect(url, dataChannelUrl, datachannelToken)
6565
6617
  .then((registerAndConnectResult) => {
6618
+ // Record ownership of the default LLM session for this meeting so
6619
+ // subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
6620
+ // calls can detect and skip work that doesn't belong to them.
6621
+ // @ts-ignore - Fix type
6622
+ this.webex.internal.llm.setOwnerMeetingId?.(this.id);
6566
6623
  // @ts-ignore - Fix type
6567
6624
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6568
6625
  // @ts-ignore - Fix type
@@ -9857,7 +9914,23 @@ export default class Meeting extends StatelessWebexPlugin {
9857
9914
  // received mid-teardown do not trigger Locus syncs. Calling it here
9858
9915
  // again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
9859
9916
  // because stopTranscription() always fires its trigger.
9860
- this.clearDataChannelToken();
9917
+ //
9918
+ // Ownership-aware token clear: only clear the shared LLM data channel
9919
+ // tokens when this meeting owns (or no meeting owns) the default LLM
9920
+ // session. Otherwise we would wipe tokens still in use by another
9921
+ // meeting's active LLM connection.
9922
+ // @ts-ignore - Fix type
9923
+ const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
9924
+ const isOwner = !currentOwner || currentOwner === this.id;
9925
+
9926
+ if (isOwner) {
9927
+ this.clearDataChannelToken();
9928
+ } else {
9929
+ LoggerProxy.logger.info(
9930
+ `Meeting:index#clearMeetingData --> skipping clearDataChannelToken; LLM owned by meeting ${currentOwner}, not ${this.id}`
9931
+ );
9932
+ }
9933
+
9861
9934
  await this.cleanupLLMConneciton({throwOnError: false});
9862
9935
  };
9863
9936
 
@@ -846,6 +846,19 @@ const MeetingUtil = {
846
846
  requestBody.sequence = sequence;
847
847
  },
848
848
 
849
+ /**
850
+ * Checks if Locus API response contains a Locus DTO
851
+ *
852
+ * @param {any} response http response from Locus API call
853
+ * @returns {boolean} true if response contains a Locus DTO
854
+ */
855
+ isLocusDtoInAPIResponse(response: any) {
856
+ return (
857
+ response?.body?.locus || // for APIs called on our participant - locus is one of props in the response body
858
+ response?.body?.url // for APIs that act on locus itself (like mute all), the body is the locus
859
+ );
860
+ },
861
+
849
862
  /**
850
863
  * Updates the locus info for the meeting with the locus
851
864
  * information returned from API requests made to Locus
@@ -854,12 +867,13 @@ const MeetingUtil = {
854
867
  * @param {Object} response The response of the http request
855
868
  * @returns {Object}
856
869
  */
857
- updateLocusFromApiResponse: (meeting, response) => {
870
+ updateLocusFromApiResponse: (meeting: any, response: any) => {
858
871
  if (!meeting) {
859
872
  return response;
860
873
  }
861
874
 
862
- if (response?.body?.locus) {
875
+ // locus API responses can come in different shapes:
876
+ if (MeetingUtil.isLocusDtoInAPIResponse(response)) {
863
877
  meeting.locusInfo.handleLocusAPIResponse(meeting, response.body);
864
878
  }
865
879
 
@@ -261,8 +261,7 @@ export default class RecordingController {
261
261
 
262
262
  LoggerProxy.logger.log(`RecordingController:index#recordingControls --> ${record}`);
263
263
 
264
- // @ts-ignore
265
- return this.request.request({
264
+ return this.request.locusDeltaRequest({
266
265
  uri: `${this.locusUrl}/${CONTROLS}`,
267
266
  body: {
268
267
  record,
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  import WebinarCollection from './collection';
20
20
  import LoggerProxy from '../common/logs/logger-proxy';
21
+ import MeetingUtil from '../meeting/util';
21
22
  import {sanitizeParams} from './utils';
22
23
 
23
24
  /**
@@ -98,13 +99,49 @@ const Webinar = WebexPlugin.extend({
98
99
  return {isPromoted, isDemoted};
99
100
  },
100
101
 
102
+ /**
103
+ * Resolves the meeting associated with this webinar instance, guarded against the
104
+ * meetingId pointer drifting onto an unrelated transient meeting (e.g. an inbound
105
+ * 1:1 call) that may exist in the meeting collection. Returns the meeting only when
106
+ * its locusUrl matches this webinar's tracked locusUrl. Returns undefined (with a
107
+ * warning) when the meeting cannot be resolved or when the webinar's locusUrl has
108
+ * not been initialized yet — callers must treat this as "no owned meeting" rather
109
+ * than fall through to an unvalidated lookup.
110
+ * @returns {object|undefined}
111
+ */
112
+ getValidatedWebinarMeeting() {
113
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
114
+
115
+ if (!meeting) {
116
+ return undefined;
117
+ }
118
+
119
+ if (!this.locusUrl) {
120
+ LoggerProxy.logger.warn(
121
+ `Webinar:index#getValidatedWebinarMeeting --> skipping; webinar locusUrl is not yet initialized for meetingId ${this.meetingId}`
122
+ );
123
+
124
+ return undefined;
125
+ }
126
+
127
+ if (meeting.locusUrl !== this.locusUrl) {
128
+ LoggerProxy.logger.warn(
129
+ `Webinar:index#getValidatedWebinarMeeting --> skipping; meeting ${this.meetingId} locusUrl ${meeting.locusUrl} does not match webinar locusUrl ${this.locusUrl}`
130
+ );
131
+
132
+ return undefined;
133
+ }
134
+
135
+ return meeting;
136
+ },
137
+
101
138
  /**
102
139
  * should join practice session data channel or not
103
140
  * @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
104
141
  * @returns {void}
105
142
  */
106
143
  updateStatusByRole({isPromoted, isDemoted}) {
107
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
144
+ const meeting = this.getValidatedWebinarMeeting();
108
145
 
109
146
  if (
110
147
  (isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
@@ -128,6 +165,9 @@ const Webinar = WebexPlugin.extend({
128
165
 
129
166
  /**
130
167
  * Disconnects the practice session data channel and removes its relay listener.
168
+ * The listener reference removed here is the exact callback captured at subscribe
169
+ * time (see updatePSDataChannel) so that cleanup is correct even if the underlying
170
+ * meeting can no longer be resolved (e.g. locusUrl mismatch).
131
171
  * @returns {Promise<void>}
132
172
  */
133
173
  async cleanupPSDataChannel() {
@@ -137,8 +177,6 @@ const Webinar = WebexPlugin.extend({
137
177
  this._pendingOnlineListener = null;
138
178
  }
139
179
 
140
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
141
-
142
180
  // @ts-ignore - Fix type
143
181
  await this.webex.internal.llm.disconnectLLM(
144
182
  {
@@ -147,15 +185,21 @@ const Webinar = WebexPlugin.extend({
147
185
  },
148
186
  LLM_PRACTICE_SESSION
149
187
  );
150
- // @ts-ignore - Fix type
151
- this.webex.internal.llm.off(
152
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
153
- meeting?.processRelayEvent
154
- );
188
+
189
+ if (this._practiceSessionRelayListener) {
190
+ // @ts-ignore - Fix type
191
+ this.webex.internal.llm.off(
192
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
193
+ this._practiceSessionRelayListener
194
+ );
195
+ this._practiceSessionRelayListener = null;
196
+ }
155
197
  },
156
198
 
157
199
  /**
158
200
  * Ensures practice-session token exists before registering the practice LLM channel.
201
+ * Caller is responsible for passing a meeting that has already been resolved via
202
+ * getValidatedWebinarMeeting() — this method does not re-validate ownership.
159
203
  * @param {object} meeting
160
204
  * @returns {Promise<string|undefined>}
161
205
  */
@@ -211,7 +255,7 @@ const Webinar = WebexPlugin.extend({
211
255
  this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
212
256
  const invocationSequence = this._updatePSDataChannelSequence;
213
257
 
214
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
258
+ const meeting = this.getValidatedWebinarMeeting();
215
259
  const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
216
260
 
217
261
  if (!isPracticeSession) {
@@ -312,15 +356,21 @@ const Webinar = WebexPlugin.extend({
312
356
  LLM_PRACTICE_SESSION
313
357
  )
314
358
  .then((registerAndConnectResult) => {
315
- // @ts-ignore - Fix type
316
- this.webex.internal.llm.off(
317
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
318
- meeting?.processRelayEvent
319
- );
359
+ // Track the exact listener reference so cleanupPSDataChannel can
360
+ // unsubscribe deterministically, even if the meeting can no longer
361
+ // be resolved at cleanup time.
362
+ if (this._practiceSessionRelayListener) {
363
+ // @ts-ignore - Fix type
364
+ this.webex.internal.llm.off(
365
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
366
+ this._practiceSessionRelayListener
367
+ );
368
+ }
369
+ this._practiceSessionRelayListener = meeting?.processRelayEvent;
320
370
  // @ts-ignore - Fix type
321
371
  this.webex.internal.llm.on(
322
372
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
323
- meeting?.processRelayEvent
373
+ this._practiceSessionRelayListener
324
374
  );
325
375
  // @ts-ignore - Fix type
326
376
  this.webex.internal.voicea?.announce?.();
@@ -341,6 +391,8 @@ const Webinar = WebexPlugin.extend({
341
391
  * @returns {Promise}
342
392
  */
343
393
  setPracticeSessionState(enabled) {
394
+ const meeting = this.getValidatedWebinarMeeting();
395
+
344
396
  return this.request({
345
397
  method: HTTP_VERBS.PATCH,
346
398
  uri: `${this.locusUrl}/controls`,
@@ -349,10 +401,16 @@ const Webinar = WebexPlugin.extend({
349
401
  enabled,
350
402
  },
351
403
  },
352
- }).catch((error) => {
353
- LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
354
- throw error;
355
- });
404
+ })
405
+ .then((response) => {
406
+ MeetingUtil.updateLocusFromApiResponse(meeting, response);
407
+
408
+ return response;
409
+ })
410
+ .catch((error) => {
411
+ LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
412
+ throw error;
413
+ });
356
414
  },
357
415
 
358
416
  /**
@@ -532,7 +590,14 @@ const Webinar = WebexPlugin.extend({
532
590
  * @returns {Promise}
533
591
  */
534
592
  async searchLargeScaleWebinarAttendees(payload) {
535
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
593
+ const meeting = this.getValidatedWebinarMeeting();
594
+ if (!meeting) {
595
+ LoggerProxy.logger.error(
596
+ 'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> webinar meeting could not be validated'
597
+ );
598
+ throw new Error('Meeting:webinar5k#Webinar meeting is not resolvable for the current locus');
599
+ }
600
+
536
601
  const rawParams = {
537
602
  search_text: payload?.queryString,
538
603
  limit: payload?.limit ?? DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
@@ -540,7 +605,9 @@ const Webinar = WebexPlugin.extend({
540
605
  };
541
606
  const attendeeSearchUrl = meeting?.locusInfo?.links?.resources?.attendeeSearch?.url;
542
607
  if (!attendeeSearchUrl) {
543
- LoggerProxy.logger.error(`Meeting:webinar5k#searchLargeScaleWebinarAttendees failed`);
608
+ LoggerProxy.logger.error(
609
+ 'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> attendee search url unavailable'
610
+ );
544
611
  throw new Error('Meeting:webinar5k#Attendee search url is not available');
545
612
  }
546
613
 
@@ -27,6 +27,7 @@ describe('plugin-meetings', () => {
27
27
  beforeEach(() => {
28
28
  request = {
29
29
  request: sinon.stub().returns(Promise.resolve()),
30
+ locusDeltaRequest: sinon.stub().returns(Promise.resolve()),
30
31
  };
31
32
 
32
33
  manager = new ControlsOptionsManager(request);
@@ -59,11 +60,11 @@ describe('plugin-meetings', () => {
59
60
 
60
61
  const result = manager.setMuteOnEntry(true);
61
62
 
62
- assert.calledWith(request.request, { uri: 'test/id/controls',
63
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
63
64
  body: { muteOnEntry: { enabled: true } },
64
65
  method: HTTP_VERBS.PATCH});
65
66
 
66
- assert.deepEqual(result, request.request.firstCall.returnValue);
67
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
67
68
  });
68
69
 
69
70
  it('can set mute on entry when the display hint is available enabled=false', () => {
@@ -71,11 +72,11 @@ describe('plugin-meetings', () => {
71
72
 
72
73
  const result = manager.setMuteOnEntry(false);
73
74
 
74
- assert.calledWith(request.request, { uri: 'test/id/controls',
75
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
75
76
  body: { muteOnEntry: { enabled: false } },
76
77
  method: HTTP_VERBS.PATCH});
77
78
 
78
- assert.deepEqual(result, request.request.firstCall.returnValue);
79
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
79
80
  });
80
81
 
81
82
  it('should send setMuteOnEntry to locusUrl without authorizingLocusUrl when in breakout', () => {
@@ -84,11 +85,11 @@ describe('plugin-meetings', () => {
84
85
 
85
86
  const result = manager.setMuteOnEntry(true);
86
87
 
87
- assert.calledWith(request.request, { uri: 'test/id/controls',
88
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
88
89
  body: { muteOnEntry: { enabled: true } },
89
90
  method: HTTP_VERBS.PATCH});
90
91
 
91
- assert.deepEqual(result, request.request.firstCall.returnValue);
92
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
92
93
  });
93
94
  });
94
95
 
@@ -114,11 +115,11 @@ describe('plugin-meetings', () => {
114
115
 
115
116
  const result = manager.setDisallowUnmute(true);
116
117
 
117
- assert.calledWith(request.request, { uri: 'test/id/controls',
118
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
118
119
  body: { disallowUnmute: { enabled: true } },
119
120
  method: HTTP_VERBS.PATCH});
120
121
 
121
- assert.deepEqual(result, request.request.firstCall.returnValue);
122
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
122
123
  });
123
124
 
124
125
  it('can set allow unmute when DISABLE_HARD_MUTE display hint is available', () => {
@@ -126,11 +127,11 @@ describe('plugin-meetings', () => {
126
127
 
127
128
  const result = manager.setDisallowUnmute(false);
128
129
 
129
- assert.calledWith(request.request, { uri: 'test/id/controls',
130
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
130
131
  body: { disallowUnmute: { enabled: false } },
131
132
  method: HTTP_VERBS.PATCH});
132
133
 
133
- assert.deepEqual(result, request.request.firstCall.returnValue);
134
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
134
135
  });
135
136
 
136
137
  it('should send setDisallowUnmute to locusUrl without authorizingLocusUrl when in breakout', () => {
@@ -139,11 +140,11 @@ describe('plugin-meetings', () => {
139
140
 
140
141
  const result = manager.setDisallowUnmute(true);
141
142
 
142
- assert.calledWith(request.request, { uri: 'test/id/controls',
143
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
143
144
  body: { disallowUnmute: { enabled: true } },
144
145
  method: HTTP_VERBS.PATCH});
145
146
 
146
- assert.deepEqual(result, request.request.firstCall.returnValue);
147
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
147
148
  });
148
149
  });
149
150
  });
@@ -154,6 +155,7 @@ describe('plugin-meetings', () => {
154
155
  beforeEach(() => {
155
156
  request = {
156
157
  request: sinon.stub().resolves(),
158
+ locusDeltaRequest: sinon.stub().resolves(),
157
159
  };
158
160
 
159
161
  manager = new ControlsOptionsManager(request);
@@ -202,7 +204,7 @@ describe('plugin-meetings', () => {
202
204
 
203
205
  return manager.update(audio, reactions)
204
206
  .then(() => {
205
- assert.calledWith(request.request, {
207
+ assert.calledWith(request.locusDeltaRequest, {
206
208
  uri: 'test/id/controls',
207
209
  body: {
208
210
  audio: audio.properties,
@@ -210,7 +212,7 @@ describe('plugin-meetings', () => {
210
212
  method: HTTP_VERBS.PATCH,
211
213
  });
212
214
 
213
- assert.calledWith(request.request, {
215
+ assert.calledWith(request.locusDeltaRequest, {
214
216
  uri: 'test/id/controls',
215
217
  body: {
216
218
  reactions: reactions.properties,
@@ -253,7 +255,7 @@ describe('plugin-meetings', () => {
253
255
  return manager.update(audio, reactions)
254
256
  .then(() => {
255
257
  // Audio controls go directly to current locusUrl (no cross-locus authorization)
256
- assert.calledWith(request.request, {
258
+ assert.calledWith(request.locusDeltaRequest, {
257
259
  uri: 'test/id/controls',
258
260
  body: {
259
261
  audio: audio.properties,
@@ -284,7 +286,7 @@ describe('plugin-meetings', () => {
284
286
 
285
287
  return manager.update(audio)
286
288
  .then(() => {
287
- assert.calledWith(request.request, {
289
+ assert.calledWith(request.locusDeltaRequest, {
288
290
  uri: 'test/id/controls',
289
291
  body: {
290
292
  audio: audio.properties,
@@ -324,6 +326,7 @@ describe('plugin-meetings', () => {
324
326
  beforeEach(() => {
325
327
  request = {
326
328
  request: sinon.stub().returns(Promise.resolve()),
329
+ locusDeltaRequest: sinon.stub().returns(Promise.resolve()),
327
330
  };
328
331
 
329
332
  manager = new ControlsOptionsManager(request);
@@ -368,11 +371,11 @@ describe('plugin-meetings', () => {
368
371
 
369
372
  const result = manager.setMuteAll(true, true, true);
370
373
 
371
- assert.calledWith(request.request, { uri: 'test/id/controls',
374
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
372
375
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true } },
373
376
  method: HTTP_VERBS.PATCH});
374
377
 
375
- assert.deepEqual(result, request.request.firstCall.returnValue);
378
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
376
379
  });
377
380
 
378
381
  it('can set mute all when the display hint is available mutedEnabled=true', () => {
@@ -380,11 +383,11 @@ describe('plugin-meetings', () => {
380
383
 
381
384
  const result = manager.setMuteAll(true, true, true);
382
385
 
383
- assert.calledWith(request.request, { uri: 'test/id/controls',
386
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
384
387
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true } },
385
388
  method: HTTP_VERBS.PATCH});
386
389
 
387
- assert.deepEqual(result, request.request.firstCall.returnValue);
390
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
388
391
  });
389
392
 
390
393
  it('can set mute all when the display hint is available mutedEnabled=true', () => {
@@ -392,11 +395,11 @@ describe('plugin-meetings', () => {
392
395
 
393
396
  const result = manager.setMuteAll(true, true, true);
394
397
 
395
- assert.calledWith(request.request, { uri: 'test/id/controls',
398
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
396
399
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true } },
397
400
  method: HTTP_VERBS.PATCH});
398
401
 
399
- assert.deepEqual(result, request.request.firstCall.returnValue);
402
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
400
403
  });
401
404
 
402
405
  it('can set mute all when the display hint is available mutedEnabled=false', () => {
@@ -404,11 +407,11 @@ describe('plugin-meetings', () => {
404
407
 
405
408
  const result = manager.setMuteAll(false, false, false);
406
409
 
407
- assert.calledWith(request.request, { uri: 'test/id/controls',
410
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
408
411
  body: { audio: { muted: false, disallowUnmute: false, muteOnEntry: false } },
409
412
  method: HTTP_VERBS.PATCH});
410
413
 
411
- assert.deepEqual(result, request.request.firstCall.returnValue);
414
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
412
415
  });
413
416
 
414
417
  it('can set mute all panelists when the display hint is available mutedEnabled=true', () => {
@@ -416,11 +419,11 @@ describe('plugin-meetings', () => {
416
419
 
417
420
  const result = manager.setMuteAll(true, true, true, ['panelist']);
418
421
 
419
- assert.calledWith(request.request, { uri: 'test/id/controls',
422
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
420
423
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['panelist'] } },
421
424
  method: HTTP_VERBS.PATCH});
422
425
 
423
- assert.deepEqual(result, request.request.firstCall.returnValue);
426
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
424
427
  });
425
428
 
426
429
  it('can set mute all attendees when the display hint is available mutedEnabled=true', () => {
@@ -428,11 +431,11 @@ describe('plugin-meetings', () => {
428
431
 
429
432
  const result = manager.setMuteAll(true, true, true, ['attendee']);
430
433
 
431
- assert.calledWith(request.request, { uri: 'test/id/controls',
434
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
432
435
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } },
433
436
  method: HTTP_VERBS.PATCH});
434
437
 
435
- assert.deepEqual(result, request.request.firstCall.returnValue);
438
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
436
439
  });
437
440
 
438
441
  it('should send setMuteAll to locusUrl without authorizingLocusUrl when in breakout', () => {
@@ -441,11 +444,11 @@ describe('plugin-meetings', () => {
441
444
 
442
445
  const result = manager.setMuteAll(true, true, true, ['attendee']);
443
446
 
444
- assert.calledWith(request.request, { uri: 'test/id/controls',
447
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
445
448
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } },
446
449
  method: HTTP_VERBS.PATCH});
447
450
 
448
- assert.deepEqual(result, request.request.firstCall.returnValue);
451
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
449
452
  });
450
453
 
451
454
  it('should send setMuteAll with PANELIST role to locusUrl without authorizingLocusUrl when in breakout', () => {
@@ -454,11 +457,11 @@ describe('plugin-meetings', () => {
454
457
 
455
458
  const result = manager.setMuteAll(true, true, true, ['PANELIST']);
456
459
 
457
- assert.calledWith(request.request, { uri: 'test/id/controls',
460
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
458
461
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['PANELIST'] } },
459
462
  method: HTTP_VERBS.PATCH});
460
463
 
461
- assert.deepEqual(result, request.request.firstCall.returnValue);
464
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
462
465
  });
463
466
  });
464
467
  });