@webex/plugin-meetings 3.12.0-next.7 → 3.12.0-next.70

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 (178) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +30 -7
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +13 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +880 -382
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +42 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  27. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  28. package/dist/interceptors/locusRetry.js +23 -8
  29. package/dist/interceptors/locusRetry.js.map +1 -1
  30. package/dist/interpretation/index.js +10 -1
  31. package/dist/interpretation/index.js.map +1 -1
  32. package/dist/interpretation/interpretation.types.js +7 -0
  33. package/dist/interpretation/interpretation.types.js.map +1 -0
  34. package/dist/interpretation/siLanguage.js +1 -1
  35. package/dist/locus-info/controlsUtils.js +4 -1
  36. package/dist/locus-info/controlsUtils.js.map +1 -1
  37. package/dist/locus-info/index.js +298 -87
  38. package/dist/locus-info/index.js.map +1 -1
  39. package/dist/locus-info/types.js +19 -0
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/index.js +3 -1
  42. package/dist/media/index.js.map +1 -1
  43. package/dist/media/properties.js +1 -0
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +3 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +1046 -689
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/muteState.js +10 -1
  50. package/dist/meeting/muteState.js.map +1 -1
  51. package/dist/meeting/request.js +5 -2
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/util.js +20 -2
  54. package/dist/meeting/util.js.map +1 -1
  55. package/dist/meeting-info/meeting-info-v2.js +2 -2
  56. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  57. package/dist/meetings/index.js +231 -78
  58. package/dist/meetings/index.js.map +1 -1
  59. package/dist/meetings/meetings.types.js +6 -1
  60. package/dist/meetings/meetings.types.js.map +1 -1
  61. package/dist/meetings/request.js +39 -0
  62. package/dist/meetings/request.js.map +1 -1
  63. package/dist/meetings/util.js +79 -5
  64. package/dist/meetings/util.js.map +1 -1
  65. package/dist/member/index.js +10 -0
  66. package/dist/member/index.js.map +1 -1
  67. package/dist/member/types.js.map +1 -1
  68. package/dist/member/util.js +3 -0
  69. package/dist/member/util.js.map +1 -1
  70. package/dist/metrics/constants.js +4 -1
  71. package/dist/metrics/constants.js.map +1 -1
  72. package/dist/multistream/codec/constants.js +63 -0
  73. package/dist/multistream/codec/constants.js.map +1 -0
  74. package/dist/multistream/mediaRequestManager.js +62 -15
  75. package/dist/multistream/mediaRequestManager.js.map +1 -1
  76. package/dist/multistream/receiveSlot.js +9 -0
  77. package/dist/multistream/receiveSlot.js.map +1 -1
  78. package/dist/reactions/reactions.type.js.map +1 -1
  79. package/dist/recording-controller/index.js +1 -3
  80. package/dist/recording-controller/index.js.map +1 -1
  81. package/dist/types/config.d.ts +2 -0
  82. package/dist/types/constants.d.ts +9 -1
  83. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  84. package/dist/types/controls-options-manager/index.d.ts +10 -0
  85. package/dist/types/hashTree/constants.d.ts +2 -0
  86. package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
  87. package/dist/types/hashTree/utils.d.ts +18 -0
  88. package/dist/types/index.d.ts +3 -0
  89. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  90. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  91. package/dist/types/locus-info/index.d.ts +50 -6
  92. package/dist/types/locus-info/types.d.ts +21 -1
  93. package/dist/types/media/properties.d.ts +1 -0
  94. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  95. package/dist/types/meeting/index.d.ts +78 -5
  96. package/dist/types/meeting/request.d.ts +1 -0
  97. package/dist/types/meeting/util.d.ts +8 -0
  98. package/dist/types/meetings/index.d.ts +30 -2
  99. package/dist/types/meetings/meetings.types.d.ts +15 -0
  100. package/dist/types/meetings/request.d.ts +14 -0
  101. package/dist/types/member/index.d.ts +1 -0
  102. package/dist/types/member/types.d.ts +1 -0
  103. package/dist/types/member/util.d.ts +1 -0
  104. package/dist/types/metrics/constants.d.ts +3 -0
  105. package/dist/types/multistream/codec/constants.d.ts +7 -0
  106. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  107. package/dist/types/reactions/reactions.type.d.ts +3 -0
  108. package/dist/webinar/index.js +305 -159
  109. package/dist/webinar/index.js.map +1 -1
  110. package/package.json +22 -22
  111. package/src/aiEnableRequest/index.ts +16 -0
  112. package/src/breakouts/breakout.ts +3 -1
  113. package/src/breakouts/index.ts +31 -0
  114. package/src/config.ts +2 -0
  115. package/src/constants.ts +13 -2
  116. package/src/controls-options-manager/constants.ts +14 -1
  117. package/src/controls-options-manager/index.ts +47 -24
  118. package/src/controls-options-manager/util.ts +81 -1
  119. package/src/hashTree/constants.ts +16 -0
  120. package/src/hashTree/hashTreeParser.ts +580 -196
  121. package/src/hashTree/utils.ts +36 -0
  122. package/src/index.ts +6 -0
  123. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  124. package/src/interceptors/locusRetry.ts +25 -4
  125. package/src/interpretation/index.ts +27 -9
  126. package/src/interpretation/interpretation.types.ts +11 -0
  127. package/src/locus-info/controlsUtils.ts +3 -1
  128. package/src/locus-info/index.ts +293 -97
  129. package/src/locus-info/types.ts +25 -1
  130. package/src/media/index.ts +3 -0
  131. package/src/media/properties.ts +1 -0
  132. package/src/meeting/in-meeting-actions.ts +4 -0
  133. package/src/meeting/index.ts +386 -48
  134. package/src/meeting/muteState.ts +10 -1
  135. package/src/meeting/request.ts +11 -0
  136. package/src/meeting/util.ts +21 -2
  137. package/src/meeting-info/meeting-info-v2.ts +4 -2
  138. package/src/meetings/index.ts +134 -44
  139. package/src/meetings/meetings.types.ts +19 -0
  140. package/src/meetings/request.ts +43 -0
  141. package/src/meetings/util.ts +97 -1
  142. package/src/member/index.ts +10 -0
  143. package/src/member/types.ts +1 -0
  144. package/src/member/util.ts +3 -0
  145. package/src/metrics/constants.ts +3 -0
  146. package/src/multistream/codec/constants.ts +58 -0
  147. package/src/multistream/mediaRequestManager.ts +119 -28
  148. package/src/multistream/receiveSlot.ts +18 -0
  149. package/src/reactions/reactions.type.ts +3 -0
  150. package/src/recording-controller/index.ts +1 -2
  151. package/src/webinar/index.ts +214 -36
  152. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  153. package/test/unit/spec/breakouts/breakout.ts +9 -3
  154. package/test/unit/spec/breakouts/index.ts +49 -0
  155. package/test/unit/spec/controls-options-manager/index.js +140 -29
  156. package/test/unit/spec/controls-options-manager/util.js +165 -0
  157. package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
  158. package/test/unit/spec/hashTree/utils.ts +125 -1
  159. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  160. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  161. package/test/unit/spec/interpretation/index.ts +26 -4
  162. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  163. package/test/unit/spec/locus-info/index.js +487 -81
  164. package/test/unit/spec/media/index.ts +31 -0
  165. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  166. package/test/unit/spec/meeting/index.js +1240 -37
  167. package/test/unit/spec/meeting/muteState.js +81 -0
  168. package/test/unit/spec/meeting/request.js +12 -0
  169. package/test/unit/spec/meeting/utils.js +33 -0
  170. package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
  171. package/test/unit/spec/meetings/index.js +360 -10
  172. package/test/unit/spec/meetings/request.js +141 -0
  173. package/test/unit/spec/meetings/utils.js +189 -0
  174. package/test/unit/spec/member/index.js +7 -0
  175. package/test/unit/spec/member/util.js +24 -0
  176. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  177. package/test/unit/spec/recording-controller/index.js +9 -8
  178. package/test/unit/spec/webinar/index.ts +329 -28
@@ -1,5 +1,12 @@
1
1
  import {HashTreeObject, ObjectType} from '../../../../src/hashTree/types';
2
- import {deleteNestedObjectsWithHtMeta, isSelf} from '../../../../src/hashTree/utils';
2
+ import {
3
+ deleteNestedObjectsWithHtMeta,
4
+ isSelf,
5
+ sleep,
6
+ sortByInitPriority,
7
+ } from '../../../../src/hashTree/utils';
8
+ import {DataSetNames, DATA_SET_INIT_PRIORITY} from '../../../../src/hashTree/constants';
9
+ import sinon from 'sinon';
3
10
 
4
11
  import {assert} from '@webex/test-helper-chai';
5
12
 
@@ -137,4 +144,121 @@ describe('Hash Tree Utils', () => {
137
144
  assert.isFalse(isSelf(participantObject));
138
145
  });
139
146
  });
147
+
148
+ describe('#sortByInitPriority', () => {
149
+ [
150
+ {
151
+ description: 'places "main" and "self" first when both appear',
152
+ input: ['atd-active', 'main', 'atd-unmuted', 'self'],
153
+ expected: ['main', 'self', 'atd-active', 'atd-unmuted'],
154
+ },
155
+ {
156
+ description: 'preserves original order of non-priority items',
157
+ input: ['atd-unmuted', 'atd-active', 'self'],
158
+ expected: ['self', 'atd-unmuted', 'atd-active'],
159
+ },
160
+ {
161
+ description: 'returns items unchanged when no priority items present',
162
+ input: ['atd-active', 'atd-unmuted'],
163
+ expected: ['atd-active', 'atd-unmuted'],
164
+ },
165
+ {
166
+ description: 'reorders when only priority items present',
167
+ input: ['self', 'main'],
168
+ expected: ['main', 'self'],
169
+ },
170
+ {
171
+ description: 'handles empty list',
172
+ input: [],
173
+ expected: [],
174
+ },
175
+ {
176
+ description: 'handles only some priority items present',
177
+ input: ['atd-active', 'main'],
178
+ expected: ['main', 'atd-active'],
179
+ },
180
+ {
181
+ description: 'handles single non-priority item',
182
+ input: ['atd-active'],
183
+ expected: ['atd-active'],
184
+ },
185
+ {
186
+ description: 'handles single priority item',
187
+ input: ['self'],
188
+ expected: ['self'],
189
+ },
190
+ ].forEach(({description, input, expected}) => {
191
+ it(description, () => {
192
+ const items = input.map((name) => ({name}));
193
+
194
+ const result = sortByInitPriority(items, DATA_SET_INIT_PRIORITY);
195
+
196
+ assert.deepEqual(
197
+ result.map((i) => i.name),
198
+ expected
199
+ );
200
+ });
201
+ });
202
+
203
+ it('should not mutate the original array', () => {
204
+ const items = [{name: DataSetNames.ATD_ACTIVE}, {name: DataSetNames.SELF}];
205
+ const originalOrder = items.map((i) => i.name);
206
+
207
+ sortByInitPriority(items, DATA_SET_INIT_PRIORITY);
208
+
209
+ assert.deepEqual(
210
+ items.map((i) => i.name),
211
+ originalOrder
212
+ );
213
+ });
214
+
215
+ it('should preserve extra properties on items', () => {
216
+ const items = [
217
+ {name: DataSetNames.ATD_ACTIVE, url: 'url1'},
218
+ {name: DataSetNames.SELF, url: 'url2'},
219
+ ];
220
+
221
+ const result = sortByInitPriority(items, DATA_SET_INIT_PRIORITY);
222
+
223
+ assert.deepEqual(result, [
224
+ {name: DataSetNames.SELF, url: 'url2'},
225
+ {name: DataSetNames.ATD_ACTIVE, url: 'url1'},
226
+ ]);
227
+ });
228
+ });
229
+
230
+ describe('#sleep', () => {
231
+ let clock;
232
+
233
+ beforeEach(() => {
234
+ clock = sinon.useFakeTimers();
235
+ });
236
+
237
+ afterEach(() => {
238
+ clock.restore();
239
+ });
240
+
241
+ [0, -1, -100].forEach((ms) => {
242
+ it(`resolves immediately when ms is ${ms}`, async () => {
243
+ const result = sleep(ms);
244
+
245
+ assert.instanceOf(result, Promise);
246
+ await result;
247
+ });
248
+ });
249
+
250
+ it('resolves after the specified delay', async () => {
251
+ let resolved = false;
252
+
253
+ sleep(500).then(() => { resolved = true; });
254
+
255
+ assert.isFalse(resolved);
256
+
257
+ await clock.tickAsync(499);
258
+ assert.isFalse(resolved);
259
+
260
+ await clock.tickAsync(1);
261
+ assert.isTrue(resolved);
262
+ });
263
+ });
140
264
  });
@@ -7,6 +7,7 @@ import DataChannelAuthTokenInterceptor from '@webex/plugin-meetings/src/intercep
7
7
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
8
8
  import * as utils from '@webex/plugin-meetings/src/interceptors/utils';
9
9
  import {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY} from '@webex/plugin-meetings/src/interceptors/constant';
10
+ import {LOCUS_URL} from '@webex/plugin-meetings/src/constants';
10
11
 
11
12
  describe('plugin-meetings', () => {
12
13
  describe('Interceptors', () => {
@@ -178,6 +179,28 @@ describe('plugin-meetings', () => {
178
179
  expect(result).to.equal('mock-response');
179
180
  });
180
181
 
182
+ it('passes request URL to _refreshDataChannelToken', async () => {
183
+ const psOptions = {
184
+ headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'},
185
+ method: 'POST',
186
+ uri: 'https://locus.example.com/practiceSession/datachannel',
187
+ };
188
+
189
+ interceptor._refreshDataChannelToken.resolves('new-token');
190
+ webex.request.resolves('mock-response');
191
+
192
+ const promise = interceptor.refreshTokenAndRetryWithDelay(psOptions);
193
+
194
+ clock.tick(2000);
195
+
196
+ await promise;
197
+
198
+ sinon.assert.calledOnceWithExactly(
199
+ interceptor._refreshDataChannelToken,
200
+ psOptions.uri
201
+ );
202
+ });
203
+
181
204
  it('rejects when refreshDataChannelToken fails', async () => {
182
205
  interceptor._refreshDataChannelToken.rejects(new Error('refresh failed'));
183
206
 
@@ -205,6 +228,179 @@ describe('plugin-meetings', () => {
205
228
  );
206
229
  });
207
230
  });
231
+
232
+ describe('refreshDataChannelToken routing (factory dispatcher)', () => {
233
+ let llmMock;
234
+ let meetingA;
235
+ let meetingsMock;
236
+ let dispatcherInterceptor;
237
+
238
+ const PS_DATACHANNEL_URL = 'https://board-a.wbx2.com/datachannel/api/v1/locus/cHJhY3RpY2Vfc2Vzc2lvbl9sb2N1cw==/registrations';
239
+ const DEFAULT_DATACHANNEL_URL = 'https://board-a.wbx2.com/datachannel/api/v1/locus/aHR0cHM6Ly9sb2N1cy1hLndieDIuY29t/registrations';
240
+
241
+ beforeEach(() => {
242
+ meetingA = {
243
+ id: 'meeting-a',
244
+ refreshDataChannelToken: sinon.stub().resolves({
245
+ body: {
246
+ datachannelToken: 'token-from-meeting-a',
247
+ dataChannelTokenType: 'llm-practice-session',
248
+ },
249
+ }),
250
+ };
251
+
252
+ llmMock = {
253
+ isDataChannelTokenEnabled: sinon.stub().resolves(true),
254
+ getSessionIdByDatachannelUrl: sinon.stub(),
255
+ getLocusUrlByDatachannelUrl: sinon.stub(),
256
+ getOwnerMeetingId: sinon.stub().returns(undefined),
257
+ refreshDataChannelToken: sinon.stub().resolves({
258
+ body: {
259
+ datachannelToken: 'token-from-llm-fallback',
260
+ dataChannelTokenType: 'llm-default-session',
261
+ },
262
+ }),
263
+ setDatachannelToken: sinon.stub(),
264
+ };
265
+
266
+ meetingsMock = {
267
+ getMeetingByType: sinon.stub(),
268
+ };
269
+
270
+ const context = {
271
+ internal: {llm: llmMock},
272
+ meetings: meetingsMock,
273
+ };
274
+
275
+ dispatcherInterceptor = Reflect.apply(DataChannelAuthTokenInterceptor.create, context, []);
276
+ });
277
+
278
+ it('routes PS request URL to PS session handler', async () => {
279
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns('llm-practice-session');
280
+ llmMock.refreshDataChannelToken
281
+ .withArgs('llm-practice-session')
282
+ .resolves({
283
+ body: {
284
+ datachannelToken: 'token-from-ps-session',
285
+ dataChannelTokenType: 'llm-practice-session',
286
+ },
287
+ });
288
+
289
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
290
+
291
+ expect(token).to.equal('token-from-ps-session');
292
+ sinon.assert.calledOnceWithExactly(llmMock.refreshDataChannelToken, 'llm-practice-session');
293
+ sinon.assert.calledOnceWithExactly(
294
+ llmMock.setDatachannelToken,
295
+ 'token-from-ps-session',
296
+ 'llm-practice-session',
297
+ undefined
298
+ );
299
+ });
300
+
301
+ it('routes non-PS URL to default session handler', async () => {
302
+ llmMock.getSessionIdByDatachannelUrl.withArgs(DEFAULT_DATACHANNEL_URL).returns('llm-default-session');
303
+ llmMock.refreshDataChannelToken
304
+ .withArgs('llm-default-session')
305
+ .resolves({
306
+ body: {
307
+ datachannelToken: 'token-from-default-session',
308
+ dataChannelTokenType: 'llm-default-session',
309
+ },
310
+ });
311
+
312
+ const token = await dispatcherInterceptor._refreshDataChannelToken(DEFAULT_DATACHANNEL_URL);
313
+
314
+ expect(token).to.equal('token-from-default-session');
315
+ sinon.assert.calledOnceWithExactly(llmMock.refreshDataChannelToken, 'llm-default-session');
316
+ sinon.assert.calledOnceWithExactly(
317
+ llmMock.setDatachannelToken,
318
+ 'token-from-default-session',
319
+ 'llm-default-session',
320
+ undefined
321
+ );
322
+ });
323
+
324
+ it('falls back to default refresh when URL does not match any session or meeting route', async () => {
325
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
326
+ llmMock.getLocusUrlByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
327
+ llmMock.refreshDataChannelToken.withArgs(undefined).resolves({
328
+ body: {
329
+ datachannelToken: 'token-from-default-fallback',
330
+ dataChannelTokenType: 'llm-default-session',
331
+ },
332
+ });
333
+
334
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
335
+
336
+ expect(token).to.equal('token-from-default-fallback');
337
+ sinon.assert.calledOnceWithExactly(llmMock.refreshDataChannelToken, undefined);
338
+ sinon.assert.calledOnceWithExactly(
339
+ llmMock.setDatachannelToken,
340
+ 'token-from-default-fallback',
341
+ 'llm-default-session',
342
+ undefined
343
+ );
344
+ });
345
+
346
+ it('falls back to meeting lookup by locusUrl when session cannot be resolved', async () => {
347
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
348
+ llmMock.getLocusUrlByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns('https://locus-a.example.com');
349
+ meetingsMock.getMeetingByType.withArgs(LOCUS_URL, 'https://locus-a.example.com').returns(meetingA);
350
+
351
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
352
+
353
+ expect(token).to.equal('token-from-meeting-a');
354
+ sinon.assert.calledOnceWithExactly(meetingA.refreshDataChannelToken);
355
+ sinon.assert.notCalled(llmMock.refreshDataChannelToken);
356
+ sinon.assert.calledOnceWithExactly(
357
+ llmMock.setDatachannelToken,
358
+ 'token-from-meeting-a',
359
+ 'llm-practice-session',
360
+ 'meeting-a'
361
+ );
362
+ });
363
+
364
+ it('falls back to active meeting datachannel URL lookup when session/locus routing is unavailable', async () => {
365
+ llmMock.getSessionIdByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
366
+ llmMock.getLocusUrlByDatachannelUrl.withArgs(PS_DATACHANNEL_URL).returns(undefined);
367
+ meetingsMock.getAllMeetings = sinon.stub().returns({
368
+ 'meeting-a': {
369
+ ...meetingA,
370
+ locusInfo: {
371
+ info: {
372
+ practiceSessionDatachannelUrl: PS_DATACHANNEL_URL,
373
+ datachannelUrl: DEFAULT_DATACHANNEL_URL,
374
+ },
375
+ },
376
+ },
377
+ });
378
+
379
+ const token = await dispatcherInterceptor._refreshDataChannelToken(PS_DATACHANNEL_URL);
380
+
381
+ expect(token).to.equal('token-from-meeting-a');
382
+ sinon.assert.calledOnceWithExactly(meetingA.refreshDataChannelToken);
383
+ sinon.assert.notCalled(llmMock.refreshDataChannelToken);
384
+ sinon.assert.calledOnceWithExactly(
385
+ llmMock.setDatachannelToken,
386
+ 'token-from-meeting-a',
387
+ 'llm-practice-session',
388
+ 'meeting-a'
389
+ );
390
+ });
391
+
392
+ it('throws when refresh returns no payload', async () => {
393
+ llmMock.getSessionIdByDatachannelUrl.returns('llm-default-session');
394
+ llmMock.refreshDataChannelToken.withArgs('llm-default-session').resolves(null);
395
+
396
+ await assert.isRejected(
397
+ dispatcherInterceptor._refreshDataChannelToken(
398
+ 'https://unknown-datachannel.example.com/registrations'
399
+ ),
400
+ /DataChannel token refresh returned no payload/
401
+ );
402
+ });
403
+ });
208
404
  });
209
405
  });
210
406
  });
@@ -36,6 +36,27 @@ describe('plugin-meetings', () => {
36
36
  uri: `https://locus-test.webex.com/locus/api/v1/loci/call`,
37
37
  body: 'foo'
38
38
  };
39
+
40
+ const hashTreeOptions = {
41
+ method: 'GET',
42
+ headers: {
43
+ trackingid: 'test',
44
+ 'retry-after': 1000,
45
+ },
46
+ uri: `https://locus-test.webex.com/locus/api/v1/loci/12345/session/abc/datasets/main/hashtree`,
47
+ body: undefined,
48
+ };
49
+
50
+ const syncOptions = {
51
+ method: 'POST',
52
+ headers: {
53
+ trackingid: 'test',
54
+ 'retry-after': 1000,
55
+ },
56
+ uri: `https://locus-test.webex.com/locus/api/v1/loci/12345/session/abc/datasets/main/sync`,
57
+ body: 'foo',
58
+ };
59
+
39
60
  const reason1 = new WebexHttpError.MethodNotAllowed({
40
61
  statusCode: 403,
41
62
  options: {
@@ -68,14 +89,194 @@ describe('plugin-meetings', () => {
68
89
  });
69
90
 
70
91
  it('calls handleRetryRequestLocusServiceError with correct retry time when locus service unavailable error', () => {
71
- interceptor.webex.request = sinon.stub().returns(Promise.resolve());
72
- const handleRetryStub = sinon.stub(interceptor, 'handleRetryRequestLocusServiceError');
92
+ interceptor.webex.request = sinon.stub().returns(Promise.resolve());
93
+ const handleRetryStub = sinon.stub(
94
+ interceptor,
95
+ 'handleRetryRequestLocusServiceError'
96
+ );
97
+ handleRetryStub.returns(Promise.resolve());
98
+
99
+ return interceptor.onResponseError(options, reason2).then(() => {
100
+ expect(handleRetryStub.calledWith(options, 1000)).to.be.true;
101
+ });
102
+ });
103
+
104
+ [429, 500, 502, 503, 504].forEach((statusCode) => {
105
+ it(`does not retry /hashtree requests on ${statusCode}`, () => {
106
+ const reason = new WebexHttpError.MethodNotAllowed({
107
+ statusCode,
108
+ options: {
109
+ headers: {trackingid: 'test', 'retry-after': 1000},
110
+ uri: hashTreeOptions.uri,
111
+ },
112
+ body: {error: `Fake ${statusCode}`},
113
+ });
114
+
115
+ const handleRetryStub = sinon.stub(
116
+ interceptor,
117
+ 'handleRetryRequestLocusServiceError'
118
+ );
119
+ handleRetryStub.returns(Promise.resolve());
120
+
121
+ return interceptor.onResponseError(hashTreeOptions, reason).then(
122
+ () => assert.fail('Expected promise to be rejected'),
123
+ (err) => {
124
+ expect(err).to.equal(reason);
125
+ expect(handleRetryStub.called).to.be.false;
126
+ handleRetryStub.restore();
127
+ }
128
+ );
129
+ });
130
+
131
+ it(`does not retry /sync requests on ${statusCode}`, () => {
132
+ const reason = new WebexHttpError.MethodNotAllowed({
133
+ statusCode,
134
+ options: {
135
+ headers: {trackingid: 'test', 'retry-after': 1000},
136
+ uri: syncOptions.uri,
137
+ },
138
+ body: {error: `Fake ${statusCode}`},
139
+ });
140
+
141
+ const handleRetryStub = sinon.stub(
142
+ interceptor,
143
+ 'handleRetryRequestLocusServiceError'
144
+ );
73
145
  handleRetryStub.returns(Promise.resolve());
74
146
 
75
- return interceptor.onResponseError(options, reason2).then(() => {
76
- expect(handleRetryStub.calledWith(options, 1000)).to.be.true;
147
+ return interceptor.onResponseError(syncOptions, reason).then(
148
+ () => assert.fail('Expected promise to be rejected'),
149
+ (err) => {
150
+ expect(err).to.equal(reason);
151
+ expect(handleRetryStub.called).to.be.false;
152
+ handleRetryStub.restore();
153
+ }
154
+ );
155
+ });
156
+ });
157
+
158
+ it('still retries other locus requests on 429', () => {
159
+ const reason429 = new WebexHttpError.MethodNotAllowed({
160
+ statusCode: 429,
161
+ options: {
162
+ headers: {trackingid: 'test', 'retry-after': 1000},
163
+ uri: options.uri,
164
+ },
165
+ body: {error: 'Too Many Requests'},
166
+ });
167
+
168
+ interceptor.webex.request = sinon.stub().returns(Promise.resolve());
169
+ const handleRetryStub = sinon.stub(
170
+ interceptor,
171
+ 'handleRetryRequestLocusServiceError'
172
+ );
173
+ handleRetryStub.returns(Promise.resolve());
77
174
 
175
+ return interceptor.onResponseError(options, reason429).then(() => {
176
+ expect(handleRetryStub.calledOnce).to.be.true;
177
+ handleRetryStub.restore();
178
+ });
179
+ });
180
+
181
+ it('still retries other locus requests on 503', () => {
182
+ interceptor.webex.request = sinon.stub().returns(Promise.resolve());
183
+ const handleRetryStub = sinon.stub(
184
+ interceptor,
185
+ 'handleRetryRequestLocusServiceError'
186
+ );
187
+ handleRetryStub.returns(Promise.resolve());
188
+
189
+ return interceptor.onResponseError(options, reason2).then(() => {
190
+ expect(handleRetryStub.calledOnce).to.be.true;
191
+ handleRetryStub.restore();
192
+ });
193
+ });
194
+
195
+ describe('URI parsing edge cases', () => {
196
+ const make503Reason = (uri) =>
197
+ new WebexHttpError.MethodNotAllowed({
198
+ statusCode: 503,
199
+ options: {headers: {trackingid: 'test', 'retry-after': 1000}, uri},
200
+ body: {error: 'Service Unavailable'},
201
+ });
202
+
203
+ const makeOptions = (uri) => ({
204
+ method: 'GET',
205
+ headers: {trackingid: 'test', 'retry-after': 1000},
206
+ uri,
207
+ body: undefined,
208
+ });
209
+
210
+ [
211
+ 'https://locus.webex.com/locus/api/v1/loci/123/session/abc/datasets/main/hashtree?rootHash=xyz',
212
+ 'https://locus.webex.com/locus/api/v1/loci/123/session/abc/datasets/main/sync?seq=5',
213
+ ].forEach((uri) => {
214
+ it(`skips retry even with query params: ${uri.split('/').pop()}`, () => {
215
+ const opts = makeOptions(uri);
216
+ const reason = make503Reason(uri);
217
+ const stub = sinon
218
+ .stub(interceptor, 'handleRetryRequestLocusServiceError')
219
+ .returns(Promise.resolve());
220
+
221
+ return interceptor.onResponseError(opts, reason).then(
222
+ () => assert.fail('Expected promise to be rejected'),
223
+ (err) => {
224
+ expect(err).to.equal(reason);
225
+ expect(stub.called).to.be.false;
226
+ stub.restore();
227
+ }
228
+ );
78
229
  });
230
+ });
231
+
232
+ [
233
+ 'https://locus.webex.com/locus/api/v1/loci/123/hashtree-v2',
234
+ 'https://locus.webex.com/locus/api/v1/loci/123/syncData',
235
+ 'https://locus.webex.com/locus/api/v1/loci/123/async',
236
+ 'https://locus.webex.com/locus/api/v1/loci/123/hashtree/metadata',
237
+ ].forEach((uri) => {
238
+ it(`still retries when path only partially matches: ${uri
239
+ .split('/')
240
+ .pop()}`, () => {
241
+ const opts = makeOptions(uri);
242
+ const reason = make503Reason(uri);
243
+ interceptor.webex.request = sinon.stub().returns(Promise.resolve());
244
+ const stub = sinon
245
+ .stub(interceptor, 'handleRetryRequestLocusServiceError')
246
+ .returns(Promise.resolve());
247
+
248
+ return interceptor.onResponseError(opts, reason).then(() => {
249
+ expect(stub.calledOnce).to.be.true;
250
+ stub.restore();
251
+ });
252
+ });
253
+ });
254
+
255
+ it('still retries when /hashtree is on a non-locus host', () => {
256
+ const uri = 'https://other-service.webex.com/api/v1/hashtree';
257
+ const opts = makeOptions(uri);
258
+ const reason = make503Reason(uri);
259
+
260
+ return interceptor.onResponseError(opts, reason).then(
261
+ () => assert.fail('Expected promise to be rejected'),
262
+ (err) => {
263
+ expect(err).to.equal(reason);
264
+ }
265
+ );
266
+ });
267
+
268
+ it('still retries when URI is malformed', () => {
269
+ const uri = 'not-a-valid-url';
270
+ const opts = makeOptions(uri);
271
+ const reason = make503Reason(uri);
272
+
273
+ return interceptor.onResponseError(opts, reason).then(
274
+ () => assert.fail('Expected promise to be rejected'),
275
+ (err) => {
276
+ expect(err).to.equal(reason);
277
+ }
278
+ );
279
+ });
79
280
  });
80
281
  });
81
282
 
@@ -9,6 +9,7 @@ describe('plugin-meetings', () => {
9
9
  describe('SimultaneousInterpretation', () => {
10
10
  let webex;
11
11
  let interpretation;
12
+ let mockMeeting;
12
13
 
13
14
  beforeEach(() => {
14
15
  // @ts-ignore
@@ -17,8 +18,17 @@ describe('plugin-meetings', () => {
17
18
  interpretation = new SimultaneousInterpretation({}, {parent: webex});
18
19
  interpretation.locusUrl = 'locusUrl';
19
20
  webex.request = sinon.stub().returns(Promise.resolve('REQUEST_RETURN_VALUE'));
20
- webex.meetings = {};
21
- webex.meetings.getMeetingByType = sinon.stub();
21
+ mockMeeting = {
22
+ locusInfo: {
23
+ handleLocusAPIResponse: sinon.stub(),
24
+ },
25
+ };
26
+ webex.meetings = {
27
+ getMeetingByType: sinon.stub(),
28
+ meetingCollection: {
29
+ getByKey: sinon.stub().returns(mockMeeting),
30
+ },
31
+ };
22
32
  });
23
33
 
24
34
  describe('#initialize', () => {
@@ -316,7 +326,8 @@ describe('plugin-meetings', () => {
316
326
  order : 0,
317
327
  isActive : true
318
328
  },];
319
- webex.request.returns(Promise.resolve({}));
329
+ const mockResponse = {body: {locus: {url: 'locusUrl'}}};
330
+ webex.request.returns(Promise.resolve(mockResponse));
320
331
 
321
332
  await interpretation.updateInterpreters(sampleData);
322
333
  assert.calledOnceWithExactly(webex.request, {
@@ -328,6 +339,11 @@ describe('plugin-meetings', () => {
328
339
  },
329
340
  },
330
341
  });
342
+ assert.calledOnceWithExactly(
343
+ mockMeeting.locusInfo.handleLocusAPIResponse,
344
+ mockMeeting,
345
+ mockResponse.body
346
+ );
331
347
  });
332
348
 
333
349
  it('rejects with error', async () => {
@@ -354,7 +370,8 @@ describe('plugin-meetings', () => {
354
370
  order: 0,
355
371
  selfParticipantId: '123',
356
372
  });
357
- webex.request.returns(Promise.resolve({}));
373
+ const mockResponse = {body: {locus: {url: 'locusUrl'}}};
374
+ webex.request.returns(Promise.resolve(mockResponse));
358
375
 
359
376
  await interpretation.changeDirection();
360
377
  assert.calledOnceWithExactly(webex.request, {
@@ -369,6 +386,11 @@ describe('plugin-meetings', () => {
369
386
  },
370
387
  },
371
388
  });
389
+ assert.calledOnceWithExactly(
390
+ mockMeeting.locusInfo.handleLocusAPIResponse,
391
+ mockMeeting,
392
+ mockResponse.body
393
+ );
372
394
  });
373
395
 
374
396
  it('request rejects with error', async () => {