@webex/internal-plugin-llm 3.11.0 → 3.12.0-llmrefactor.1
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 +63 -12
- package/dist/constants.js +9 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +35 -4
- package/dist/index.js.map +1 -1
- package/dist/llm-plugin.js +199 -0
- package/dist/llm-plugin.js.map +1 -0
- package/dist/llm.js +280 -48
- package/dist/llm.js.map +1 -1
- package/dist/llm.types.js +18 -0
- package/dist/llm.types.js.map +1 -1
- package/package.json +6 -6
- package/src/constants.ts +13 -0
- package/src/index.ts +12 -2
- package/src/llm-plugin.ts +115 -0
- package/src/llm.ts +229 -42
- package/src/llm.types.ts +92 -5
- package/test/unit/spec/llm-plugin.js +391 -0
- package/test/unit/spec/llm.js +473 -85
package/test/unit/spec/llm.js
CHANGED
|
@@ -2,152 +2,540 @@ import MockWebex from '@webex/test-helper-mock-webex';
|
|
|
2
2
|
import {assert} from '@webex/test-helper-chai';
|
|
3
3
|
import sinon from 'sinon';
|
|
4
4
|
import Mercury from '@webex/internal-plugin-mercury';
|
|
5
|
-
import
|
|
5
|
+
import LLMChannel from '@webex/internal-plugin-llm/src/llm';
|
|
6
6
|
|
|
7
7
|
describe('plugin-llm', () => {
|
|
8
8
|
const locusUrl = 'locusUrl';
|
|
9
9
|
const datachannelUrl = 'datachannelUrl';
|
|
10
10
|
|
|
11
|
-
describe('
|
|
12
|
-
let webex,
|
|
11
|
+
describe('LLMChannel', () => {
|
|
12
|
+
let webex, llmChannel;
|
|
13
13
|
|
|
14
14
|
beforeEach(() => {
|
|
15
15
|
webex = new MockWebex({
|
|
16
16
|
children: {
|
|
17
17
|
mercury: Mercury,
|
|
18
|
-
llm:
|
|
18
|
+
llm: LLMChannel,
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
webex.internal.feature = {
|
|
23
|
+
setFeature: sinon.stub().resolves({value: true}),
|
|
24
|
+
getFeature: sinon.stub().resolves(true),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
llmChannel = webex.internal.llm;
|
|
28
|
+
// Stub Mercury's prototype disconnect so super.disconnect() works in tests
|
|
29
|
+
sinon.stub(Object.getPrototypeOf(Object.getPrototypeOf(llmChannel)), 'disconnect').resolves();
|
|
30
|
+
llmChannel.request = sinon.stub().resolves({
|
|
28
31
|
headers: {},
|
|
29
32
|
body: {
|
|
30
33
|
binding: 'binding',
|
|
31
|
-
webSocketUrl: '
|
|
34
|
+
webSocketUrl: 'wss://example.com/socket',
|
|
32
35
|
},
|
|
33
36
|
});
|
|
37
|
+
llmChannel.connect = sinon.stub().callsFake(() => {
|
|
38
|
+
llmChannel.connected = true;
|
|
39
|
+
});
|
|
34
40
|
});
|
|
35
41
|
|
|
42
|
+
afterEach(() => sinon.restore());
|
|
43
|
+
|
|
36
44
|
describe('#registerAndConnect', () => {
|
|
37
|
-
it('registers
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
it('registers and connects', async () => {
|
|
46
|
+
assert.equal(llmChannel.isConnected(), false);
|
|
47
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
48
|
+
assert.equal(llmChannel.isConnected(), true);
|
|
49
|
+
assert.equal(llmChannel.getLocusUrl(), locusUrl);
|
|
50
|
+
assert.equal(llmChannel.getDatachannelUrl(), datachannelUrl);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('connects with subscriptionAwareSubchannels when token enabled', async () => {
|
|
54
|
+
llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
|
|
55
|
+
|
|
56
|
+
const buildSpy = sinon.spy(LLMChannel, 'buildUrlWithAwareSubchannels');
|
|
57
|
+
|
|
58
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl, 'abc123');
|
|
59
|
+
|
|
60
|
+
sinon.assert.calledOnce(buildSpy);
|
|
61
|
+
sinon.assert.calledOnce(llmChannel.connect);
|
|
62
|
+
|
|
63
|
+
const calledUrl = llmChannel.connect.getCall(0).args[0];
|
|
64
|
+
assert.include(calledUrl, 'subscriptionAwareSubchannels=');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('connects without subscriptionAwareSubchannels when token disabled', async () => {
|
|
68
|
+
llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
69
|
+
|
|
70
|
+
const buildSpy = sinon.spy(LLMChannel, 'buildUrlWithAwareSubchannels');
|
|
71
|
+
|
|
72
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
73
|
+
|
|
74
|
+
sinon.assert.notCalled(buildSpy);
|
|
75
|
+
sinon.assert.calledOnce(llmChannel.connect);
|
|
76
|
+
|
|
77
|
+
const calledUrl = llmChannel.connect.getCall(0).args[0];
|
|
78
|
+
assert.equal(calledUrl, 'wss://example.com/socket');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('connects with subscriptionAwareSubchannels when token enabled but token missing', async () => {
|
|
82
|
+
llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
|
|
83
|
+
|
|
84
|
+
const buildSpy = sinon.spy(LLMChannel, 'buildUrlWithAwareSubchannels');
|
|
85
|
+
|
|
86
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl, undefined);
|
|
87
|
+
|
|
88
|
+
sinon.assert.calledOnce(buildSpy);
|
|
89
|
+
sinon.assert.calledOnce(llmChannel.connect);
|
|
90
|
+
|
|
91
|
+
const calledUrl = llmChannel.connect.getCall(0).args[0];
|
|
92
|
+
assert.include(calledUrl, 'subscriptionAwareSubchannels=');
|
|
93
|
+
|
|
94
|
+
buildSpy.restore();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('deduplicates concurrent calls', async () => {
|
|
98
|
+
const promise1 = llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
99
|
+
const promise2 = llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
100
|
+
|
|
101
|
+
assert.strictEqual(promise1, promise2);
|
|
102
|
+
await promise1;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('avoids reconnect if already connected to same URLs', async () => {
|
|
106
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
107
|
+
sinon.assert.calledOnce(llmChannel.connect);
|
|
108
|
+
|
|
109
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
110
|
+
sinon.assert.calledOnce(llmChannel.connect);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('propagates error when registration request fails', async () => {
|
|
114
|
+
const error = new Error('Network failure');
|
|
115
|
+
llmChannel.request = sinon.stub().rejects(error);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
119
|
+
assert.fail('should have thrown');
|
|
120
|
+
} catch (e) {
|
|
121
|
+
assert.equal(e.message, 'Network failure');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
sinon.assert.notCalled(llmChannel.connect);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns timing data on successful connection', async () => {
|
|
128
|
+
const clock = sinon.useFakeTimers();
|
|
129
|
+
|
|
130
|
+
llmChannel.request = sinon.stub().callsFake(async () => {
|
|
131
|
+
clock.tick(37);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
headers: {},
|
|
135
|
+
body: {
|
|
136
|
+
binding: 'binding',
|
|
137
|
+
webSocketUrl: 'wss://example.com/socket',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
llmChannel.connect = sinon.stub().callsFake(async () => {
|
|
142
|
+
clock.tick(23);
|
|
143
|
+
llmChannel.connected = true;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(result, {
|
|
149
|
+
clientLLMDatachannelResponseTime: 37,
|
|
150
|
+
clientLLMWebSocketConnectTime: 23,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
clock.restore();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('attaches timing to error when connect fails', async () => {
|
|
157
|
+
const clock = sinon.useFakeTimers();
|
|
158
|
+
|
|
159
|
+
llmChannel.request = sinon.stub().callsFake(async () => {
|
|
160
|
+
clock.tick(37);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
headers: {},
|
|
164
|
+
body: {
|
|
165
|
+
binding: 'binding',
|
|
166
|
+
webSocketUrl: 'wss://example.com/socket',
|
|
167
|
+
},
|
|
168
|
+
};
|
|
43
169
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
170
|
+
|
|
171
|
+
const connectError = new Error('websocket connect failed');
|
|
172
|
+
llmChannel.connect = sinon.stub().rejects(connectError);
|
|
173
|
+
|
|
174
|
+
let caughtError;
|
|
175
|
+
try {
|
|
176
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
caughtError = error;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
assert.equal(caughtError, connectError);
|
|
182
|
+
assert.deepEqual(caughtError.timing, {clientLLMDatachannelResponseTime: 37});
|
|
183
|
+
|
|
184
|
+
clock.restore();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('clears connectingPromise after successful connection', async () => {
|
|
188
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
189
|
+
|
|
190
|
+
// After completion, a new call should create a fresh promise (not deduplicated)
|
|
191
|
+
llmChannel.connect.resetHistory();
|
|
192
|
+
llmChannel.request.resetHistory();
|
|
193
|
+
|
|
194
|
+
// Simulate different URLs to force new connection
|
|
195
|
+
await llmChannel.registerAndConnect('newLocusUrl', 'newDatachannelUrl');
|
|
196
|
+
|
|
197
|
+
// Should have called request again since connectingPromise was cleared
|
|
198
|
+
sinon.assert.calledOnce(llmChannel.request);
|
|
47
199
|
});
|
|
48
200
|
|
|
49
|
-
it(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
201
|
+
it('clears connectingPromise after failed connection', async () => {
|
|
202
|
+
llmChannel.connect = sinon.stub().rejects(new Error('connect failed'));
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// expected
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reset and try again - should not deduplicate since promise was cleared
|
|
211
|
+
llmChannel.connect = sinon.stub().callsFake(() => {
|
|
212
|
+
llmChannel.connected = true;
|
|
55
213
|
});
|
|
56
|
-
|
|
57
|
-
|
|
214
|
+
llmChannel.request.resetHistory();
|
|
215
|
+
|
|
216
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
217
|
+
|
|
218
|
+
// Should have called request again
|
|
219
|
+
sinon.assert.calledOnce(llmChannel.request);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns undefined when already connected to same URLs', async () => {
|
|
223
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
224
|
+
|
|
225
|
+
const result = await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
226
|
+
|
|
227
|
+
assert.equal(result, undefined);
|
|
58
228
|
});
|
|
59
229
|
});
|
|
60
230
|
|
|
61
|
-
describe('#register', () => {
|
|
62
|
-
it('
|
|
63
|
-
|
|
231
|
+
describe('#register (via registerAndConnect)', () => {
|
|
232
|
+
it('sends request with token header when token enabled and provided', async () => {
|
|
233
|
+
llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
|
|
234
|
+
|
|
235
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl, 'abc123');
|
|
236
|
+
|
|
237
|
+
sinon.assert.calledOnceWithExactly(
|
|
238
|
+
llmChannel.request,
|
|
239
|
+
sinon.match({
|
|
240
|
+
method: 'POST',
|
|
241
|
+
url: datachannelUrl,
|
|
242
|
+
body: {deviceUrl: webex.internal.device.url},
|
|
243
|
+
headers: {'Data-Channel-Auth-Token': 'abc123'},
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('sends request without token header when none provided', async () => {
|
|
249
|
+
llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(true);
|
|
250
|
+
|
|
251
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
64
252
|
|
|
65
253
|
sinon.assert.calledOnceWithExactly(
|
|
66
|
-
|
|
254
|
+
llmChannel.request,
|
|
67
255
|
sinon.match({
|
|
68
256
|
method: 'POST',
|
|
69
|
-
url:
|
|
257
|
+
url: datachannelUrl,
|
|
70
258
|
body: {deviceUrl: webex.internal.device.url},
|
|
259
|
+
headers: {},
|
|
71
260
|
})
|
|
72
261
|
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('sends request without token header when toggle disabled', async () => {
|
|
265
|
+
llmChannel.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
73
266
|
|
|
74
|
-
|
|
267
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl, 'abc123');
|
|
268
|
+
|
|
269
|
+
sinon.assert.calledOnceWithExactly(
|
|
270
|
+
llmChannel.request,
|
|
271
|
+
sinon.match({
|
|
272
|
+
method: 'POST',
|
|
273
|
+
url: datachannelUrl,
|
|
274
|
+
body: {deviceUrl: webex.internal.device.url},
|
|
275
|
+
headers: {},
|
|
276
|
+
})
|
|
277
|
+
);
|
|
75
278
|
});
|
|
76
279
|
});
|
|
77
280
|
|
|
78
281
|
describe('#getLocusUrl', () => {
|
|
79
282
|
it('gets LocusUrl', async () => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
await llmService.registerAndConnect(locusUrl, datachannelUrl);
|
|
87
|
-
assert.equal(llmService.getLocusUrl(), locusUrl);
|
|
283
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
284
|
+
assert.equal(llmChannel.getLocusUrl(), locusUrl);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('returns undefined before connection', () => {
|
|
288
|
+
assert.equal(llmChannel.getLocusUrl(), undefined);
|
|
88
289
|
});
|
|
89
290
|
});
|
|
90
291
|
|
|
91
292
|
describe('#getDatachannelUrl', () => {
|
|
92
293
|
it('gets dataChannel Url', async () => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
294
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
295
|
+
assert.equal(llmChannel.getDatachannelUrl(), datachannelUrl);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('returns undefined before connection', () => {
|
|
299
|
+
assert.equal(llmChannel.getDatachannelUrl(), undefined);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('#getBinding', () => {
|
|
304
|
+
it('gets binding after connection', async () => {
|
|
305
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
306
|
+
assert.equal(llmChannel.getBinding(), 'binding');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('returns undefined before connection', () => {
|
|
310
|
+
assert.equal(llmChannel.getBinding(), undefined);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('#isConnecting', () => {
|
|
315
|
+
it('returns true while connection is in progress', () => {
|
|
316
|
+
// Start connection but don't await
|
|
317
|
+
llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
318
|
+
|
|
319
|
+
assert.equal(llmChannel.isConnecting(), true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('returns false after connection completes', async () => {
|
|
323
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
324
|
+
|
|
325
|
+
assert.equal(llmChannel.isConnecting(), false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('returns false before any connection attempt', () => {
|
|
329
|
+
assert.equal(llmChannel.isConnecting(), false);
|
|
101
330
|
});
|
|
102
331
|
});
|
|
103
332
|
|
|
104
333
|
describe('#disconnect', () => {
|
|
105
|
-
it('
|
|
106
|
-
await
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
assert.equal(
|
|
110
|
-
assert.equal(
|
|
111
|
-
assert.equal(
|
|
334
|
+
it('calls super.disconnect and clears all connection state', async () => {
|
|
335
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
336
|
+
llmChannel.setDatachannelToken('token123');
|
|
337
|
+
|
|
338
|
+
assert.equal(llmChannel.isConnected(), true);
|
|
339
|
+
assert.equal(llmChannel.getLocusUrl(), locusUrl);
|
|
340
|
+
assert.equal(llmChannel.getDatachannelUrl(), datachannelUrl);
|
|
341
|
+
assert.equal(llmChannel.getBinding(), 'binding');
|
|
342
|
+
assert.equal(llmChannel.getDatachannelToken(), 'token123');
|
|
343
|
+
|
|
344
|
+
await llmChannel.disconnect({code: 1000, reason: 'test'});
|
|
345
|
+
|
|
346
|
+
assert.equal(llmChannel.getLocusUrl(), undefined);
|
|
347
|
+
assert.equal(llmChannel.getDatachannelUrl(), undefined);
|
|
348
|
+
assert.equal(llmChannel.getBinding(), undefined);
|
|
349
|
+
assert.equal(llmChannel.getDatachannelToken(), undefined);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('works without options', async () => {
|
|
353
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
354
|
+
|
|
355
|
+
await llmChannel.disconnect();
|
|
356
|
+
|
|
357
|
+
assert.equal(llmChannel.getLocusUrl(), undefined);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('propagates disconnect errors', async () => {
|
|
361
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
362
|
+
// Change the existing stub to reject
|
|
363
|
+
Object.getPrototypeOf(Object.getPrototypeOf(llmChannel)).disconnect.rejects(
|
|
364
|
+
new Error('disconnect failed')
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
await llmChannel.disconnect({code: 1000, reason: 'test'});
|
|
369
|
+
assert.fail('should have thrown');
|
|
370
|
+
} catch (error) {
|
|
371
|
+
assert.equal(error.message, 'disconnect failed');
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('emits disconnected event', async () => {
|
|
376
|
+
await llmChannel.registerAndConnect(locusUrl, datachannelUrl);
|
|
377
|
+
const disconnectedSpy = sinon.spy();
|
|
378
|
+
|
|
379
|
+
llmChannel.on('disconnected', disconnectedSpy);
|
|
380
|
+
|
|
381
|
+
await llmChannel.disconnect({code: 1000, reason: 'test'});
|
|
382
|
+
|
|
383
|
+
sinon.assert.calledOnce(disconnectedSpy);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('#setRefreshHandler', () => {
|
|
388
|
+
it('stores the provided handler', async () => {
|
|
389
|
+
const handler = sinon.stub().resolves({body: {datachannelToken: 'newToken'}});
|
|
390
|
+
llmChannel.setRefreshHandler(handler);
|
|
391
|
+
|
|
392
|
+
const result = await llmChannel.refreshDataChannelToken();
|
|
393
|
+
|
|
394
|
+
assert.equal(result.body.datachannelToken, 'newToken');
|
|
395
|
+
sinon.assert.calledOnce(handler);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('#isDataChannelTokenEnabled', () => {
|
|
400
|
+
it('returns value from feature toggle', async () => {
|
|
401
|
+
webex.internal.feature.getFeature.resolves(true);
|
|
402
|
+
|
|
403
|
+
const result = await llmChannel.isDataChannelTokenEnabled();
|
|
404
|
+
|
|
405
|
+
sinon.assert.calledOnceWithExactly(
|
|
406
|
+
webex.internal.feature.getFeature,
|
|
407
|
+
'developer',
|
|
408
|
+
'data-channel-with-jwt-token'
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
assert.equal(result, true);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('#refreshDataChannelToken', () => {
|
|
416
|
+
it('returns null and logs warn if no handler is set', async () => {
|
|
417
|
+
const warnSpy = llmChannel.logger.warn;
|
|
418
|
+
|
|
419
|
+
const result = await llmChannel.refreshDataChannelToken();
|
|
420
|
+
|
|
421
|
+
assert.equal(result, null);
|
|
422
|
+
|
|
423
|
+
sinon.assert.calledOnce(warnSpy);
|
|
424
|
+
sinon.assert.calledWithMatch(warnSpy, sinon.match('LLM refreshHandler is not set'));
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('returns token when handler resolves', async () => {
|
|
428
|
+
const mockToken = {body: {datachannelToken: 'newToken', datachannelTokenType: 'default'}};
|
|
429
|
+
const handler = sinon.stub().resolves(mockToken);
|
|
430
|
+
|
|
431
|
+
llmChannel.setRefreshHandler(handler);
|
|
432
|
+
|
|
433
|
+
const token = await llmChannel.refreshDataChannelToken();
|
|
434
|
+
|
|
435
|
+
assert.equal(token, mockToken);
|
|
436
|
+
sinon.assert.calledOnce(handler);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('logs warn and returns null when handler rejects', async () => {
|
|
440
|
+
const handler = sinon.stub().rejects(new Error('throw error'));
|
|
441
|
+
llmChannel.setRefreshHandler(handler);
|
|
442
|
+
|
|
443
|
+
const warnSpy = llmChannel.logger.warn;
|
|
444
|
+
|
|
445
|
+
const result = await llmChannel.refreshDataChannelToken();
|
|
446
|
+
|
|
447
|
+
assert.equal(result, null);
|
|
448
|
+
|
|
449
|
+
sinon.assert.calledOnce(warnSpy);
|
|
450
|
+
sinon.assert.calledWithMatch(warnSpy, sinon.match('DataChannel token refresh failed'));
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('#getDatachannelToken / #setDatachannelToken / #clearDatachannelToken', () => {
|
|
455
|
+
it('sets and gets datachannel token', () => {
|
|
456
|
+
llmChannel.setDatachannelToken('abc123');
|
|
457
|
+
assert.equal(llmChannel.getDatachannelToken(), 'abc123');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('returns undefined when no token is set', () => {
|
|
461
|
+
assert.equal(llmChannel.getDatachannelToken(), undefined);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('clears datachannel token', () => {
|
|
465
|
+
llmChannel.setDatachannelToken('abc123');
|
|
466
|
+
assert.equal(llmChannel.getDatachannelToken(), 'abc123');
|
|
467
|
+
|
|
468
|
+
llmChannel.clearDatachannelToken();
|
|
469
|
+
assert.equal(llmChannel.getDatachannelToken(), undefined);
|
|
112
470
|
});
|
|
113
471
|
});
|
|
114
472
|
|
|
115
|
-
describe('
|
|
116
|
-
|
|
473
|
+
describe('.matchesDatachannelRequestUrl', () => {
|
|
474
|
+
it('returns true when request URL starts with registration URL', () => {
|
|
475
|
+
const registrationUrl =
|
|
476
|
+
'https://board.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
|
|
477
|
+
const requestUrl = registrationUrl + '/some-path';
|
|
478
|
+
|
|
479
|
+
const result = LLMChannel.matchesDatachannelRequestUrl(requestUrl, registrationUrl);
|
|
480
|
+
|
|
481
|
+
assert.equal(result, true);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('returns false when URLs do not match', () => {
|
|
485
|
+
const registrationUrl =
|
|
486
|
+
'https://board.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
|
|
487
|
+
const requestUrl = 'https://unknown.example.com/path';
|
|
488
|
+
|
|
489
|
+
const result = LLMChannel.matchesDatachannelRequestUrl(requestUrl, registrationUrl);
|
|
490
|
+
|
|
491
|
+
assert.equal(result, false);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('returns true when request host is rewritten but pathname matches', () => {
|
|
495
|
+
const registrationUrl =
|
|
496
|
+
'https://board-b.wbx2.com/datachannel/api/v1/locus/ps-encoded/registrations';
|
|
497
|
+
const requestUrl =
|
|
498
|
+
'https://hostmap-rewritten.example.com/datachannel/api/v1/locus/ps-encoded/registrations/events';
|
|
499
|
+
|
|
500
|
+
const result = LLMChannel.matchesDatachannelRequestUrl(requestUrl, registrationUrl);
|
|
501
|
+
|
|
502
|
+
assert.equal(result, true);
|
|
503
|
+
});
|
|
117
504
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
binding: {},
|
|
124
|
-
webSocketUrl: 'someUrl',
|
|
125
|
-
disconnectLLM: function (options) {
|
|
126
|
-
return this.disconnect(options).then(() => {
|
|
127
|
-
this.locusUrl = undefined;
|
|
128
|
-
this.datachannelUrl = undefined;
|
|
129
|
-
this.binding = undefined;
|
|
130
|
-
this.webSocketUrl = undefined;
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
};
|
|
505
|
+
it('returns false for empty or null URLs', () => {
|
|
506
|
+
assert.equal(LLMChannel.matchesDatachannelRequestUrl('', 'https://example.com'), false);
|
|
507
|
+
assert.equal(LLMChannel.matchesDatachannelRequestUrl('https://example.com', ''), false);
|
|
508
|
+
assert.equal(LLMChannel.matchesDatachannelRequestUrl(null, 'https://example.com'), false);
|
|
509
|
+
assert.equal(LLMChannel.matchesDatachannelRequestUrl('https://example.com', null), false);
|
|
134
510
|
});
|
|
135
511
|
|
|
136
|
-
it('
|
|
137
|
-
|
|
512
|
+
it('returns false for invalid URLs', () => {
|
|
513
|
+
const result = LLMChannel.matchesDatachannelRequestUrl('not-a-url', 'also-not-a-url');
|
|
138
514
|
|
|
139
|
-
|
|
140
|
-
expect(instance.locusUrl).toBeUndefined();
|
|
141
|
-
expect(instance.datachannelUrl).toBeUndefined();
|
|
142
|
-
expect(instance.binding).toBeUndefined();
|
|
143
|
-
expect(instance.webSocketUrl).toBeUndefined();
|
|
515
|
+
assert.equal(result, false);
|
|
144
516
|
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('.buildUrlWithAwareSubchannels', () => {
|
|
520
|
+
it('adds subscriptionAwareSubchannels query param', () => {
|
|
521
|
+
const baseUrl = 'wss://example.com/socket';
|
|
522
|
+
const subchannels = ['channel1', 'channel2'];
|
|
145
523
|
|
|
146
|
-
|
|
147
|
-
instance.disconnect.mockRejectedValue(new Error('Disconnect failed'));
|
|
524
|
+
const result = LLMChannel.buildUrlWithAwareSubchannels(baseUrl, subchannels);
|
|
148
525
|
|
|
149
|
-
|
|
526
|
+
assert.include(result, 'subscriptionAwareSubchannels=channel1%2Cchannel2');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('preserves existing query params', () => {
|
|
530
|
+
const baseUrl = 'wss://example.com/socket?existing=param';
|
|
531
|
+
const subchannels = ['channel1'];
|
|
532
|
+
|
|
533
|
+
const result = LLMChannel.buildUrlWithAwareSubchannels(baseUrl, subchannels);
|
|
534
|
+
|
|
535
|
+
assert.include(result, 'existing=param');
|
|
536
|
+
assert.include(result, 'subscriptionAwareSubchannels=channel1');
|
|
150
537
|
});
|
|
151
538
|
});
|
|
539
|
+
|
|
152
540
|
});
|
|
153
541
|
});
|