@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.
- 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 +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +30 -7
- 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 +13 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +880 -382
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +42 -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/dataChannelAuthToken.js +75 -15
- package/dist/interceptors/dataChannelAuthToken.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/interpretation.types.js +7 -0
- package/dist/interpretation/interpretation.types.js.map +1 -0
- 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 +298 -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/index.js +3 -1
- package/dist/media/index.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 +1046 -689
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +10 -1
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.js +5 -2
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +20 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +2 -2
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +231 -78
- 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 +79 -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/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.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 +2 -0
- package/dist/types/constants.d.ts +9 -1
- 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 +2 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
- package/dist/types/hashTree/utils.d.ts +18 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/interpretation/interpretation.types.d.ts +10 -0
- package/dist/types/locus-info/index.d.ts +50 -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 +78 -5
- package/dist/types/meeting/request.d.ts +1 -0
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +30 -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/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +305 -159
- 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 +31 -0
- package/src/config.ts +2 -0
- package/src/constants.ts +13 -2
- 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 +16 -0
- package/src/hashTree/hashTreeParser.ts +580 -196
- package/src/hashTree/utils.ts +36 -0
- package/src/index.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +88 -12
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +27 -9
- package/src/interpretation/interpretation.types.ts +11 -0
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +293 -97
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +386 -48
- package/src/meeting/muteState.ts +10 -1
- package/src/meeting/request.ts +11 -0
- package/src/meeting/util.ts +21 -2
- package/src/meeting-info/meeting-info-v2.ts +4 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -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/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- 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 +214 -36
- 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 +49 -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 +1838 -180
- package/test/unit/spec/hashTree/utils.ts +125 -1
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
- 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 +487 -81
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +1240 -37
- package/test/unit/spec/meeting/muteState.js +81 -0
- package/test/unit/spec/meeting/request.js +12 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
- package/test/unit/spec/meetings/index.js +360 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +329 -28
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import {HashTreeObject, ObjectType} from '../../../../src/hashTree/types';
|
|
2
|
-
import {
|
|
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
|
-
|
|
72
|
-
|
|
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(
|
|
76
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 () => {
|