@webex/internal-plugin-llm 3.12.0-next.4 → 3.12.0-next.41
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/README.md +2 -2
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/llm.js +332 -47
- package/dist/llm.js.map +1 -1
- package/dist/llm.types.js +5 -1
- package/dist/llm.types.js.map +1 -1
- package/package.json +6 -6
- package/src/constants.ts +1 -0
- package/src/index.ts +3 -0
- package/src/llm.ts +344 -42
- package/src/llm.types.ts +55 -9
- package/test/unit/spec/llm.js +407 -19
package/test/unit/spec/llm.js
CHANGED
|
@@ -3,6 +3,7 @@ import {assert} from '@webex/test-helper-chai';
|
|
|
3
3
|
import sinon from 'sinon';
|
|
4
4
|
import Mercury from '@webex/internal-plugin-mercury';
|
|
5
5
|
import LLMService from '@webex/internal-plugin-llm';
|
|
6
|
+
import LLMChannel from '@webex/internal-plugin-llm/src/llm';
|
|
6
7
|
|
|
7
8
|
describe('plugin-llm', () => {
|
|
8
9
|
const locusUrl = 'locusUrl';
|
|
@@ -172,6 +173,72 @@ describe('plugin-llm', () => {
|
|
|
172
173
|
|
|
173
174
|
buildSpy.restore();
|
|
174
175
|
});
|
|
176
|
+
|
|
177
|
+
it('rejects when the registration response has no websocket URL', async () => {
|
|
178
|
+
llmService.connections.set('llm-default-session', {});
|
|
179
|
+
llmService.register = sinon.stub().resolves();
|
|
180
|
+
|
|
181
|
+
await assert.isRejected(
|
|
182
|
+
llmService.registerAndConnect(locusUrl, datachannelUrl),
|
|
183
|
+
/returned no websocket URL/
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
sinon.assert.notCalled(llmService.connect);
|
|
187
|
+
assert.equal(llmService.isConnected(), false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('attaches the measured datachannel timing to the error when the websocket connect fails', async () => {
|
|
191
|
+
const clock = sinon.useFakeTimers();
|
|
192
|
+
|
|
193
|
+
llmService.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
194
|
+
llmService.register = sinon.stub().callsFake(async () => {
|
|
195
|
+
clock.tick(37);
|
|
196
|
+
const sessionData = llmService.connections.get('llm-default-session') || {};
|
|
197
|
+
sessionData.webSocketUrl = 'wss://example.com/socket';
|
|
198
|
+
sessionData.binding = 'binding';
|
|
199
|
+
llmService.connections.set('llm-default-session', sessionData);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const connectError = new Error('websocket connect failed');
|
|
203
|
+
llmService.connect = sinon.stub().rejects(connectError);
|
|
204
|
+
|
|
205
|
+
let caughtError;
|
|
206
|
+
try {
|
|
207
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
caughtError = error;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
assert.equal(caughtError, connectError);
|
|
213
|
+
assert.deepEqual(caughtError.timing, {clientLLMDatachannelResponseTime: 37});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('attaches the measured datachannel timing to the error when the response has no websocket URL', async () => {
|
|
217
|
+
const clock = sinon.useFakeTimers();
|
|
218
|
+
|
|
219
|
+
llmService.register = sinon.stub().callsFake(async () => {
|
|
220
|
+
clock.tick(29);
|
|
221
|
+
// register() resolves but the response leaves the session without a websocket URL.
|
|
222
|
+
llmService.connections.set('llm-default-session', {
|
|
223
|
+
locusUrl,
|
|
224
|
+
datachannelUrl,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
llmService.connect = sinon.stub();
|
|
229
|
+
|
|
230
|
+
let caughtError;
|
|
231
|
+
try {
|
|
232
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
caughtError = error;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
sinon.assert.notCalled(llmService.connect);
|
|
238
|
+
assert.instanceOf(caughtError, Error);
|
|
239
|
+
assert.match(caughtError.message, /returned no websocket URL/);
|
|
240
|
+
assert.deepEqual(caughtError.timing, {clientLLMDatachannelResponseTime: 29});
|
|
241
|
+
});
|
|
175
242
|
});
|
|
176
243
|
|
|
177
244
|
describe('#register', () => {
|
|
@@ -277,23 +344,27 @@ describe('plugin-llm', () => {
|
|
|
277
344
|
instance = {
|
|
278
345
|
disconnect: jest.fn(() => Promise.resolve()),
|
|
279
346
|
connections: new Map([
|
|
280
|
-
['llm-default-session', { foo: 'bar' }],
|
|
347
|
+
['llm-default-session', { foo: 'bar', datachannelToken: 'session-token' }],
|
|
281
348
|
]),
|
|
282
|
-
datachannelTokens: {
|
|
283
|
-
'llm-default-session': 'session-token',
|
|
284
|
-
},
|
|
285
349
|
|
|
286
|
-
disconnectLLM: function (
|
|
350
|
+
disconnectLLM: function (
|
|
351
|
+
options,
|
|
352
|
+
sessionId = 'llm-default-session',
|
|
353
|
+
ownerMeetingId = 'meeting-1'
|
|
354
|
+
) {
|
|
355
|
+
if (!ownerMeetingId) {
|
|
356
|
+
return Promise.reject(new Error('ownerMeetingId is required'));
|
|
357
|
+
}
|
|
358
|
+
|
|
287
359
|
return this.disconnect(options, sessionId).then(() => {
|
|
288
360
|
this.connections.delete(sessionId);
|
|
289
|
-
this.datachannelTokens[sessionId] = undefined;
|
|
290
361
|
});
|
|
291
362
|
},
|
|
292
363
|
};
|
|
293
364
|
});
|
|
294
365
|
|
|
295
|
-
it('calls disconnect and clears session connection
|
|
296
|
-
await instance.disconnectLLM({ code: 3000, reason: 'bye' });
|
|
366
|
+
it('calls disconnect and clears session connection (including token stored in session)', async () => {
|
|
367
|
+
await instance.disconnectLLM({ code: 3000, reason: 'bye' }, 'llm-default-session', 'meeting-1');
|
|
297
368
|
|
|
298
369
|
expect(instance.disconnect).toHaveBeenCalledWith(
|
|
299
370
|
{ code: 3000, reason: 'bye' },
|
|
@@ -301,26 +372,102 @@ describe('plugin-llm', () => {
|
|
|
301
372
|
);
|
|
302
373
|
|
|
303
374
|
expect(instance.connections.has('llm-default-session')).toBe(false);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('disconnectLLM supports legacy call with options only', async () => {
|
|
378
|
+
llmService.disconnect = sinon.stub().resolves(true);
|
|
379
|
+
|
|
380
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 'llm-default-session');
|
|
381
|
+
|
|
382
|
+
const options = {code: 1000, reason: 'legacy'};
|
|
383
|
+
const disconnected = await llmService.disconnectLLM(options);
|
|
384
|
+
|
|
385
|
+
assert.equal(disconnected, true);
|
|
386
|
+
sinon.assert.calledOnceWithExactly(llmService.disconnect, options, 'llm-default-session');
|
|
387
|
+
assert.equal(llmService.getAllConnections().has('llm-default-session'), false);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('disconnectLLM supports legacy call with options and sessionId', async () => {
|
|
391
|
+
llmService.disconnect = sinon.stub().resolves(true);
|
|
392
|
+
|
|
393
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
|
|
394
|
+
|
|
395
|
+
const options = {code: 1000, reason: 'legacy'};
|
|
396
|
+
const disconnected = await llmService.disconnectLLM(options, 's1');
|
|
397
|
+
|
|
398
|
+
assert.equal(disconnected, true);
|
|
399
|
+
sinon.assert.calledOnceWithExactly(llmService.disconnect, options, 's1');
|
|
400
|
+
assert.equal(llmService.getAllConnections().has('s1'), false);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('disconnectLLM treats null sessionId as default session', async () => {
|
|
404
|
+
llmService.disconnect = sinon.stub().resolves(true);
|
|
405
|
+
|
|
406
|
+
await llmService.registerAndConnect(
|
|
407
|
+
locusUrl,
|
|
408
|
+
datachannelUrl,
|
|
409
|
+
undefined,
|
|
410
|
+
'llm-default-session'
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const options = {code: 1000, reason: 'legacy-null-session'};
|
|
414
|
+
const disconnected = await llmService.disconnectLLM(options, null);
|
|
304
415
|
|
|
305
|
-
|
|
416
|
+
assert.equal(disconnected, true);
|
|
417
|
+
sinon.assert.calledOnceWithExactly(llmService.disconnect, options, 'llm-default-session');
|
|
418
|
+
assert.equal(llmService.getAllConnections().has('llm-default-session'), false);
|
|
306
419
|
});
|
|
307
420
|
|
|
308
421
|
it('propagates disconnect errors', async () => {
|
|
309
422
|
instance.disconnect.mockRejectedValue(new Error('disconnect failed'));
|
|
310
423
|
|
|
311
424
|
await expect(
|
|
312
|
-
instance.disconnectLLM({ code: 3000, reason: 'bye' })
|
|
425
|
+
instance.disconnectLLM({ code: 3000, reason: 'bye' }, 'llm-default-session', 'meeting-1')
|
|
313
426
|
).rejects.toThrow('disconnect failed');
|
|
314
427
|
});
|
|
315
428
|
});
|
|
316
429
|
|
|
317
430
|
describe('#setRefreshHandler', () => {
|
|
431
|
+
beforeEach(() => {
|
|
432
|
+
llmService.setRefreshHandler = LLMChannel.prototype.setRefreshHandler.bind(llmService);
|
|
433
|
+
llmService.refreshDataChannelToken = LLMChannel.prototype.refreshDataChannelToken.bind(llmService);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('defaults to llm-default-session when sessionId is omitted', () => {
|
|
437
|
+
const handler = sinon.stub().resolves({ body: { datachannelToken: 'legacyToken' } });
|
|
438
|
+
|
|
439
|
+
llmService.setRefreshHandler(handler);
|
|
440
|
+
|
|
441
|
+
return llmService.refreshDataChannelToken().then((result) => {
|
|
442
|
+
assert.equal(result.body.datachannelToken, 'legacyToken');
|
|
443
|
+
sinon.assert.calledOnce(handler);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
318
447
|
it('stores the provided handler', () => {
|
|
319
448
|
const handler = sinon.stub().resolves({ body: { datachannelToken: 'newToken' } });
|
|
320
|
-
llmService.setRefreshHandler(handler);
|
|
449
|
+
llmService.setRefreshHandler(handler, 'llm-default-session');
|
|
321
450
|
|
|
322
|
-
|
|
323
|
-
|
|
451
|
+
return llmService.refreshDataChannelToken().then((result) => {
|
|
452
|
+
assert.equal(result.body.datachannelToken, 'newToken');
|
|
453
|
+
sinon.assert.calledOnce(handler);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('stores handlers per session id', () => {
|
|
458
|
+
const handlerS1 = sinon.stub().resolves({ body: { datachannelToken: 'token-s1' } });
|
|
459
|
+
const handlerS2 = sinon.stub().resolves({ body: { datachannelToken: 'token-s2' } });
|
|
460
|
+
|
|
461
|
+
llmService.setRefreshHandler(handlerS1, 's1');
|
|
462
|
+
llmService.setRefreshHandler(handlerS2, 's2');
|
|
463
|
+
|
|
464
|
+
return Promise.all([
|
|
465
|
+
llmService.refreshDataChannelToken('s1'),
|
|
466
|
+
llmService.refreshDataChannelToken('s2'),
|
|
467
|
+
]).then(([resultS1, resultS2]) => {
|
|
468
|
+
assert.equal(resultS1.body.datachannelToken, 'token-s1');
|
|
469
|
+
assert.equal(resultS2.body.datachannelToken, 'token-s2');
|
|
470
|
+
});
|
|
324
471
|
});
|
|
325
472
|
});
|
|
326
473
|
|
|
@@ -341,6 +488,11 @@ describe('plugin-llm', () => {
|
|
|
341
488
|
});
|
|
342
489
|
|
|
343
490
|
describe('#refreshDataChannelToken', () => {
|
|
491
|
+
beforeEach(() => {
|
|
492
|
+
llmService.setRefreshHandler = LLMChannel.prototype.setRefreshHandler.bind(llmService);
|
|
493
|
+
llmService.refreshDataChannelToken = LLMChannel.prototype.refreshDataChannelToken.bind(llmService);
|
|
494
|
+
});
|
|
495
|
+
|
|
344
496
|
it('returns null and logs warn if no handler is set', async () => {
|
|
345
497
|
const warnSpy = llmService.logger.warn
|
|
346
498
|
|
|
@@ -351,7 +503,7 @@ describe('plugin-llm', () => {
|
|
|
351
503
|
sinon.assert.calledOnce(warnSpy);
|
|
352
504
|
sinon.assert.calledWithMatch(
|
|
353
505
|
warnSpy,
|
|
354
|
-
sinon.match('LLM refreshHandler is not set')
|
|
506
|
+
sinon.match('LLM refreshHandler is not set for session')
|
|
355
507
|
);
|
|
356
508
|
});
|
|
357
509
|
|
|
@@ -359,7 +511,7 @@ describe('plugin-llm', () => {
|
|
|
359
511
|
const mockToken = { body: { datachannelToken: 'newToken', isPracticeSession: false } };
|
|
360
512
|
const handler = sinon.stub().resolves(mockToken);
|
|
361
513
|
|
|
362
|
-
llmService.setRefreshHandler(handler);
|
|
514
|
+
llmService.setRefreshHandler(handler, 'llm-default-session');
|
|
363
515
|
|
|
364
516
|
const token = await llmService.refreshDataChannelToken();
|
|
365
517
|
|
|
@@ -367,9 +519,21 @@ describe('plugin-llm', () => {
|
|
|
367
519
|
sinon.assert.calledOnce(handler);
|
|
368
520
|
});
|
|
369
521
|
|
|
522
|
+
it('uses the handler for the provided session id', async () => {
|
|
523
|
+
const mockToken = { body: { datachannelToken: 'session-token', isPracticeSession: true } };
|
|
524
|
+
const handler = sinon.stub().resolves(mockToken);
|
|
525
|
+
|
|
526
|
+
llmService.setRefreshHandler(handler, 's2');
|
|
527
|
+
|
|
528
|
+
const token = await llmService.refreshDataChannelToken('s2');
|
|
529
|
+
|
|
530
|
+
assert.equal(token, mockToken);
|
|
531
|
+
sinon.assert.calledOnce(handler);
|
|
532
|
+
});
|
|
533
|
+
|
|
370
534
|
it('logs warn and returns null when handler rejects', async () => {
|
|
371
535
|
const handler = sinon.stub().rejects(new Error('throw error'));
|
|
372
|
-
llmService.setRefreshHandler(handler);
|
|
536
|
+
llmService.setRefreshHandler(handler, 'llm-default-session');
|
|
373
537
|
|
|
374
538
|
const warnSpy = llmService.logger.warn
|
|
375
539
|
|
|
@@ -392,6 +556,73 @@ describe('plugin-llm', () => {
|
|
|
392
556
|
llmService.setDatachannelToken('123abc','llm-practice-session');
|
|
393
557
|
assert.equal(llmService.getDatachannelToken('llm-practice-session'), '123abc');
|
|
394
558
|
});
|
|
559
|
+
|
|
560
|
+
it('supports arbitrary session id token keys', () => {
|
|
561
|
+
llmService.setDatachannelToken('token-s1', 'session-custom-1');
|
|
562
|
+
|
|
563
|
+
assert.equal(llmService.getDatachannelToken('session-custom-1'), 'token-s1');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
describe('#setOwnerMeetingId / #getOwnerMeetingId', () => {
|
|
569
|
+
it('stores and returns the owner meeting id for the default session', () => {
|
|
570
|
+
// beforeEach seeds connections with the default session entry
|
|
571
|
+
llmService.setOwnerMeetingId('meeting-1');
|
|
572
|
+
|
|
573
|
+
assert.equal(llmService.getOwnerMeetingId(), 'meeting-1');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('returns undefined when no owner has been set yet', () => {
|
|
577
|
+
assert.equal(llmService.getOwnerMeetingId(), undefined);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('is a no-op when there is no session data for the given sessionId', () => {
|
|
581
|
+
// Default session exists (seeded in beforeEach), but an arbitrary
|
|
582
|
+
// session id does not — setOwnerMeetingId must not create entries.
|
|
583
|
+
llmService.setOwnerMeetingId('meeting-1', 'unknown-session');
|
|
584
|
+
|
|
585
|
+
assert.equal(llmService.getOwnerMeetingId('unknown-session'), undefined);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('allows clearing ownership by passing undefined', () => {
|
|
589
|
+
llmService.setOwnerMeetingId('meeting-1');
|
|
590
|
+
assert.equal(llmService.getOwnerMeetingId(), 'meeting-1');
|
|
591
|
+
|
|
592
|
+
llmService.setOwnerMeetingId(undefined);
|
|
593
|
+
|
|
594
|
+
assert.equal(llmService.getOwnerMeetingId(), undefined);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('tracks ownership per session id', () => {
|
|
598
|
+
llmService.connections.set('session-A', {webSocketUrl: 'wss://a'});
|
|
599
|
+
llmService.connections.set('session-B', {webSocketUrl: 'wss://b'});
|
|
600
|
+
|
|
601
|
+
llmService.setOwnerMeetingId('meeting-A', 'session-A');
|
|
602
|
+
llmService.setOwnerMeetingId('meeting-B', 'session-B');
|
|
603
|
+
|
|
604
|
+
assert.equal(llmService.getOwnerMeetingId('session-A'), 'meeting-A');
|
|
605
|
+
assert.equal(llmService.getOwnerMeetingId('session-B'), 'meeting-B');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('clears ownerMeetingId naturally when disconnectLLM deletes the session entry', async () => {
|
|
609
|
+
llmService.register = sinon.stub().callsFake(async () => ({
|
|
610
|
+
body: {binding: 'binding', webSocketUrl: 'wss://example.com/socket'},
|
|
611
|
+
}));
|
|
612
|
+
|
|
613
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl);
|
|
614
|
+
llmService.setOwnerMeetingId('meeting-1');
|
|
615
|
+
assert.equal(llmService.getOwnerMeetingId(), 'meeting-1');
|
|
616
|
+
|
|
617
|
+
await llmService.disconnectLLM(
|
|
618
|
+
{code: 3050, reason: 'done (permanent)'},
|
|
619
|
+
'llm-default-session',
|
|
620
|
+
'meeting-1'
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
// Session entry was deleted, so ownerMeetingId is gone.
|
|
624
|
+
assert.equal(llmService.getOwnerMeetingId(), undefined);
|
|
625
|
+
});
|
|
395
626
|
});
|
|
396
627
|
|
|
397
628
|
describe('multi-connection logic', () => {
|
|
@@ -421,20 +652,33 @@ describe('plugin-llm', () => {
|
|
|
421
652
|
await llmService.registerAndConnect(locusUrl2, datachannelUrl2, undefined, 's2');
|
|
422
653
|
|
|
423
654
|
const options = {code: 1000, reason: 'test'};
|
|
424
|
-
await llmService.disconnectLLM(options, 's1');
|
|
655
|
+
const disconnected = await llmService.disconnectLLM(options, 's1', 'meeting-1');
|
|
425
656
|
|
|
657
|
+
assert.equal(disconnected, true);
|
|
426
658
|
sinon.assert.calledOnceWithExactly(llmService.disconnect, options, 's1');
|
|
427
659
|
|
|
428
660
|
const all = llmService.getAllConnections();
|
|
429
661
|
assert.equal(all.has('s1'), false);
|
|
430
662
|
assert.equal(all.has('s2'), true);
|
|
663
|
+
assert.equal(llmService.getDatachannelToken('s1'), undefined);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('disconnectLLM skips disconnect when ownerMeetingId does not match', async () => {
|
|
667
|
+
llmService.disconnect = sinon.stub().resolves(true);
|
|
431
668
|
|
|
432
|
-
|
|
669
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
|
|
670
|
+
llmService.setOwnerMeetingId('meeting-1', 's1');
|
|
671
|
+
|
|
672
|
+
const options = {code: 1000, reason: 'test'};
|
|
673
|
+
const disconnected = await llmService.disconnectLLM(options, 's1', 'meeting-2');
|
|
674
|
+
|
|
675
|
+
assert.equal(disconnected, false);
|
|
676
|
+
sinon.assert.notCalled(llmService.disconnect);
|
|
677
|
+
assert.equal(llmService.getAllConnections().has('s1'), true);
|
|
433
678
|
});
|
|
434
679
|
|
|
435
680
|
it('disconnectAllLLM clears all sessions', async () => {
|
|
436
681
|
llmService.disconnectAll = sinon.stub().resolves(true);
|
|
437
|
-
sinon.spy(llmService, 'resetDatachannelTokens');
|
|
438
682
|
|
|
439
683
|
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
|
|
440
684
|
await llmService.registerAndConnect(locusUrl2, datachannelUrl2, undefined, 's2');
|
|
@@ -446,5 +690,149 @@ describe('plugin-llm', () => {
|
|
|
446
690
|
});
|
|
447
691
|
});
|
|
448
692
|
|
|
693
|
+
describe('#getLocusUrlByDatachannelUrl', () => {
|
|
694
|
+
const locusUrl2 = 'https://locus-b.wbx2.com/locus/api/v1/loci/456';
|
|
695
|
+
const datachannelUrl2 = 'https://board-b.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
|
|
696
|
+
|
|
697
|
+
// Ampersand State.extend() does not always propagate prototype methods
|
|
698
|
+
// added to child classes, so we bind the method from the class prototype
|
|
699
|
+
// directly onto the test instance.
|
|
700
|
+
beforeEach(() => {
|
|
701
|
+
llmService.getLocusUrlByDatachannelUrl = LLMChannel.prototype.getLocusUrlByDatachannelUrl.bind(llmService);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('returns locusUrl when request URL matches a session datachannelUrl', async () => {
|
|
705
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
|
|
706
|
+
await llmService.registerAndConnect(locusUrl2, datachannelUrl2, undefined, 's2');
|
|
707
|
+
|
|
708
|
+
const result = llmService.getLocusUrlByDatachannelUrl(datachannelUrl2 + '/some-path');
|
|
709
|
+
|
|
710
|
+
assert.equal(result, locusUrl2);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('returns undefined when no session matches the request URL', async () => {
|
|
714
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
|
|
715
|
+
|
|
716
|
+
const result = llmService.getLocusUrlByDatachannelUrl('https://unknown.example.com/path');
|
|
717
|
+
|
|
718
|
+
assert.equal(result, undefined);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('matches locusUrl when request host is rewritten but pathname matches', async () => {
|
|
722
|
+
await llmService.registerAndConnect(locusUrl2, datachannelUrl2, undefined, 's2');
|
|
723
|
+
|
|
724
|
+
const rewrittenHostRequestUrl =
|
|
725
|
+
'https://hostmap-rewritten.example.com/datachannel/api/v1/locus/ps-encoded/registrations/events';
|
|
726
|
+
const result = llmService.getLocusUrlByDatachannelUrl(rewrittenHostRequestUrl);
|
|
727
|
+
|
|
728
|
+
assert.equal(result, locusUrl2);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('returns undefined when no connections exist', () => {
|
|
732
|
+
const result = llmService.getLocusUrlByDatachannelUrl(datachannelUrl);
|
|
733
|
+
|
|
734
|
+
assert.equal(result, undefined);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
describe('#getSessionIdByDatachannelUrl', () => {
|
|
739
|
+
const datachannelUrl2 = 'https://board-b.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
|
|
740
|
+
|
|
741
|
+
beforeEach(() => {
|
|
742
|
+
llmService.getSessionIdByDatachannelUrl = LLMChannel.prototype.getSessionIdByDatachannelUrl.bind(llmService);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('returns sessionId when request URL matches a session datachannelUrl', async () => {
|
|
746
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
|
|
747
|
+
await llmService.registerAndConnect('https://locus-b.wbx2.com/locus/api/v1/loci/456', datachannelUrl2, undefined, 's2');
|
|
748
|
+
|
|
749
|
+
const result = llmService.getSessionIdByDatachannelUrl(datachannelUrl2 + '/some-path');
|
|
750
|
+
|
|
751
|
+
assert.equal(result, 's2');
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('returns undefined when no session matches the request URL', async () => {
|
|
755
|
+
await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined, 's1');
|
|
756
|
+
|
|
757
|
+
const result = llmService.getSessionIdByDatachannelUrl('https://unknown.example.com/path');
|
|
758
|
+
|
|
759
|
+
assert.equal(result, undefined);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('matches sessionId when request host is rewritten but pathname matches', async () => {
|
|
763
|
+
await llmService.registerAndConnect(
|
|
764
|
+
'https://locus-b.wbx2.com/locus/api/v1/loci/456',
|
|
765
|
+
datachannelUrl2,
|
|
766
|
+
undefined,
|
|
767
|
+
's2'
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
const rewrittenHostRequestUrl =
|
|
771
|
+
'https://hostmap-rewritten.example.com/datachannel/api/v1/locus/ps-encoded/registrations/messages';
|
|
772
|
+
const result = llmService.getSessionIdByDatachannelUrl(rewrittenHostRequestUrl);
|
|
773
|
+
|
|
774
|
+
assert.equal(result, 's2');
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe('#registerAndConnect timing', () => {
|
|
779
|
+
it('returns timing data on successful connection', async () => {
|
|
780
|
+
const clock = sinon.useFakeTimers();
|
|
781
|
+
|
|
782
|
+
llmService.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
783
|
+
llmService.register = sinon.stub().callsFake(async () => {
|
|
784
|
+
clock.tick(37);
|
|
785
|
+
|
|
786
|
+
const sessionData = llmService.connections.get('llm-default-session') || {};
|
|
787
|
+
|
|
788
|
+
sessionData.webSocketUrl = 'wss://example.com/socket';
|
|
789
|
+
sessionData.binding = 'binding';
|
|
790
|
+
llmService.connections.set('llm-default-session', sessionData);
|
|
791
|
+
});
|
|
792
|
+
llmService.connect = sinon.stub().callsFake(async () => {
|
|
793
|
+
clock.tick(23);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const result = await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined);
|
|
797
|
+
|
|
798
|
+
assert.deepEqual(result, {
|
|
799
|
+
clientLLMDatachannelResponseTime: 37,
|
|
800
|
+
clientLLMWebSocketConnectTime: 23,
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('returns undefined when locusUrl is empty', async () => {
|
|
805
|
+
llmService.register = sinon.stub().resolves();
|
|
806
|
+
|
|
807
|
+
const result = await llmService.registerAndConnect('', datachannelUrl, undefined);
|
|
808
|
+
|
|
809
|
+
assert.isUndefined(result);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
describe('#getWebSocketUrl', () => {
|
|
814
|
+
it('returns the websocket URL for default session', () => {
|
|
815
|
+
llmService.connections.set('llm-default-session', {
|
|
816
|
+
webSocketUrl: 'wss://test.example.com/ws',
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
assert.equal(llmService.getWebSocketUrl(), 'wss://test.example.com/ws');
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('returns undefined when no connection exists', () => {
|
|
823
|
+
llmService.connections.clear();
|
|
824
|
+
|
|
825
|
+
assert.isUndefined(llmService.getWebSocketUrl());
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('returns the websocket URL for custom session', () => {
|
|
829
|
+
llmService.connections.set('custom-session', {
|
|
830
|
+
webSocketUrl: 'wss://custom.example.com/ws',
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
assert.equal(llmService.getWebSocketUrl('custom-session'), 'wss://custom.example.com/ws');
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
449
837
|
});
|
|
450
838
|
});
|