@lobehub/lobehub 2.0.0-next.38 → 2.0.0-next.39

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 (103) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/apps/desktop/src/main/modules/networkProxy/__tests__/dispatcher.test.ts +401 -0
  3. package/apps/desktop/src/main/modules/networkProxy/__tests__/tester.test.ts +531 -0
  4. package/apps/desktop/src/main/modules/networkProxy/__tests__/urlBuilder.test.ts +349 -0
  5. package/apps/desktop/src/main/modules/networkProxy/__tests__/validator.test.ts +492 -0
  6. package/changelog/v1.json +5 -0
  7. package/locales/ar/auth.json +45 -1
  8. package/locales/bg-BG/auth.json +45 -1
  9. package/locales/de-DE/auth.json +45 -1
  10. package/locales/en-US/auth.json +45 -1
  11. package/locales/es-ES/auth.json +45 -1
  12. package/locales/fa-IR/auth.json +45 -1
  13. package/locales/fr-FR/auth.json +45 -1
  14. package/locales/it-IT/auth.json +45 -1
  15. package/locales/ja-JP/auth.json +45 -1
  16. package/locales/ko-KR/auth.json +45 -1
  17. package/locales/nl-NL/auth.json +45 -1
  18. package/locales/pl-PL/auth.json +45 -1
  19. package/locales/pt-BR/auth.json +45 -1
  20. package/locales/ru-RU/auth.json +45 -1
  21. package/locales/tr-TR/auth.json +45 -1
  22. package/locales/vi-VN/auth.json +45 -1
  23. package/locales/zh-CN/auth.json +45 -1
  24. package/locales/zh-TW/auth.json +45 -1
  25. package/package.json +1 -1
  26. package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
  27. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
  28. package/packages/obervability-otel/package.json +3 -1
  29. package/packages/obervability-otel/src/api.ts +2 -0
  30. package/packages/obervability-otel/src/trpc/convention.ts +16 -0
  31. package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
  32. package/packages/obervability-otel/src/trpc/index.ts +62 -0
  33. package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
  34. package/packages/types/src/usage/usageRecord.ts +54 -0
  35. package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
  36. package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
  37. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
  38. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
  39. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
  40. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
  41. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
  42. package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
  43. package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
  44. package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
  45. package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
  46. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
  47. package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
  48. package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
  49. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
  50. package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
  51. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
  52. package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
  53. package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
  54. package/src/features/Conversation/Messages/Group/index.tsx +7 -2
  55. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
  56. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  57. package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
  58. package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
  59. package/src/features/PluginsUI/Render/index.tsx +17 -0
  60. package/src/libs/mcp/client.ts +3 -2
  61. package/src/libs/mcp/types.ts +71 -0
  62. package/src/libs/trpc/lambda/index.ts +5 -2
  63. package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
  64. package/src/locales/default/auth.ts +44 -0
  65. package/src/locales/default/chat.ts +1 -0
  66. package/src/server/routers/desktop/mcp.ts +1 -3
  67. package/src/server/routers/lambda/index.ts +2 -0
  68. package/src/server/routers/lambda/usage.ts +36 -0
  69. package/src/server/routers/tools/mcp.ts +1 -3
  70. package/src/server/services/mcp/index.test.ts +28 -15
  71. package/src/server/services/mcp/index.ts +29 -18
  72. package/src/server/services/usage/index.test.ts +310 -0
  73. package/src/server/services/usage/index.ts +164 -0
  74. package/src/services/chat/contextEngineering.test.ts +4 -0
  75. package/src/services/mcp.test.ts +7 -1
  76. package/src/services/mcp.ts +13 -12
  77. package/src/services/usage.ts +13 -0
  78. package/src/store/chat/agents/createAgentExecutors.ts +2 -3
  79. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
  80. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
  81. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
  82. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
  83. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
  84. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  85. package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
  86. package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
  87. package/src/store/chat/slices/message/initialState.ts +5 -0
  88. package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
  89. package/src/store/chat/slices/message/selectors/chat.ts +0 -2
  90. package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
  91. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
  92. package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
  93. package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
  94. package/src/store/chat/slices/plugin/action.test.ts +4 -4
  95. package/src/store/chat/slices/plugin/actions/index.ts +39 -0
  96. package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
  97. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
  98. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
  99. package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
  100. package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
  101. package/src/store/chat/store.ts +1 -1
  102. package/src/store/global/initialState.ts +1 -0
  103. package/src/store/chat/slices/plugin/action.ts +0 -539
@@ -0,0 +1,531 @@
1
+ import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { ProxyConnectionTester } from '../tester';
5
+
6
+ // Mock logger
7
+ vi.mock('@/utils/logger', () => ({
8
+ createLogger: () => ({
9
+ debug: vi.fn(),
10
+ info: vi.fn(),
11
+ warn: vi.fn(),
12
+ error: vi.fn(),
13
+ }),
14
+ }));
15
+
16
+ // Mock undici
17
+ vi.mock('undici', () => ({
18
+ fetch: vi.fn(),
19
+ getGlobalDispatcher: vi.fn(),
20
+ setGlobalDispatcher: vi.fn(),
21
+ }));
22
+
23
+ // Mock ProxyConfigValidator
24
+ vi.mock('../validator', () => ({
25
+ ProxyConfigValidator: {
26
+ validate: vi.fn(),
27
+ },
28
+ }));
29
+
30
+ // Mock ProxyUrlBuilder
31
+ vi.mock('../urlBuilder', () => ({
32
+ ProxyUrlBuilder: {
33
+ build: vi.fn(),
34
+ },
35
+ }));
36
+
37
+ // Mock ProxyDispatcherManager
38
+ vi.mock('../dispatcher', () => ({
39
+ ProxyDispatcherManager: {
40
+ createProxyAgent: vi.fn(),
41
+ },
42
+ }));
43
+
44
+ describe('ProxyConnectionTester', () => {
45
+ let mockAgent: any;
46
+ let mockOriginalDispatcher: any;
47
+ let mockFetch: any;
48
+ let mockGetGlobalDispatcher: any;
49
+ let mockSetGlobalDispatcher: any;
50
+ let mockProxyDispatcherManager: any;
51
+ let mockProxyConfigValidator: any;
52
+ let mockProxyUrlBuilder: any;
53
+
54
+ beforeEach(async () => {
55
+ vi.clearAllMocks();
56
+
57
+ // Import mocked modules
58
+ const undici = await import('undici');
59
+ const dispatcher = await import('../dispatcher');
60
+ const validator = await import('../validator');
61
+ const urlBuilder = await import('../urlBuilder');
62
+
63
+ mockFetch = vi.mocked(undici.fetch);
64
+ mockGetGlobalDispatcher = vi.mocked(undici.getGlobalDispatcher);
65
+ mockSetGlobalDispatcher = vi.mocked(undici.setGlobalDispatcher);
66
+ mockProxyDispatcherManager = vi.mocked(dispatcher.ProxyDispatcherManager);
67
+ mockProxyConfigValidator = vi.mocked(validator.ProxyConfigValidator);
68
+ mockProxyUrlBuilder = vi.mocked(urlBuilder.ProxyUrlBuilder.build);
69
+
70
+ // Setup mock agent
71
+ mockAgent = {
72
+ destroy: vi.fn().mockResolvedValue(undefined),
73
+ };
74
+
75
+ mockOriginalDispatcher = {
76
+ destroy: vi.fn().mockResolvedValue(undefined),
77
+ };
78
+
79
+ mockGetGlobalDispatcher.mockReturnValue(mockOriginalDispatcher);
80
+ mockProxyDispatcherManager.createProxyAgent.mockReturnValue(mockAgent);
81
+ mockProxyConfigValidator.validate.mockReturnValue({ isValid: true, errors: [] });
82
+ mockProxyUrlBuilder.mockImplementation((config: NetworkProxySettings) => {
83
+ return `${config.proxyType}://${config.proxyServer}:${config.proxyPort}`;
84
+ });
85
+ });
86
+
87
+ describe('testConnection', () => {
88
+ describe('successful connection', () => {
89
+ it('should return success for successful HTTP request', async () => {
90
+ mockFetch.mockResolvedValueOnce({
91
+ ok: true,
92
+ status: 200,
93
+ statusText: 'OK',
94
+ });
95
+
96
+ const result = await ProxyConnectionTester.testConnection();
97
+
98
+ expect(result.success).toBe(true);
99
+ expect(result.responseTime).toBeGreaterThanOrEqual(0);
100
+ expect(result.message).toBeUndefined();
101
+ expect(mockFetch).toHaveBeenCalledWith(
102
+ 'https://www.google.com',
103
+ expect.objectContaining({
104
+ headers: expect.objectContaining({
105
+ 'User-Agent': 'LobeChat-Desktop/1.0.0',
106
+ }),
107
+ signal: expect.any(AbortSignal),
108
+ }),
109
+ );
110
+ });
111
+
112
+ it('should return success with custom URL', async () => {
113
+ mockFetch.mockResolvedValueOnce({
114
+ ok: true,
115
+ status: 200,
116
+ statusText: 'OK',
117
+ });
118
+
119
+ const customUrl = 'https://api.example.com';
120
+ const result = await ProxyConnectionTester.testConnection(customUrl);
121
+
122
+ expect(result.success).toBe(true);
123
+ expect(mockFetch).toHaveBeenCalledWith(customUrl, expect.any(Object));
124
+ });
125
+
126
+ it('should return success with custom timeout', async () => {
127
+ mockFetch.mockResolvedValueOnce({
128
+ ok: true,
129
+ status: 200,
130
+ statusText: 'OK',
131
+ });
132
+
133
+ const result = await ProxyConnectionTester.testConnection('https://www.google.com', 5000);
134
+
135
+ expect(result.success).toBe(true);
136
+ });
137
+
138
+ it('should include response time in result', async () => {
139
+ mockFetch.mockResolvedValueOnce({
140
+ ok: true,
141
+ status: 200,
142
+ statusText: 'OK',
143
+ });
144
+
145
+ const result = await ProxyConnectionTester.testConnection();
146
+
147
+ expect(result.responseTime).toBeDefined();
148
+ expect(typeof result.responseTime).toBe('number');
149
+ expect(result.responseTime).toBeGreaterThanOrEqual(0);
150
+ });
151
+ });
152
+
153
+ describe('connection failures', () => {
154
+ it('should return failure for HTTP error status', async () => {
155
+ mockFetch.mockResolvedValueOnce({
156
+ ok: false,
157
+ status: 404,
158
+ statusText: 'Not Found',
159
+ });
160
+
161
+ const result = await ProxyConnectionTester.testConnection();
162
+
163
+ expect(result.success).toBe(false);
164
+ expect(result.message).toContain('HTTP 404');
165
+ expect(result.message).toContain('Not Found');
166
+ expect(result.responseTime).toBeGreaterThanOrEqual(0);
167
+ });
168
+
169
+ it('should return failure for HTTP 500 error', async () => {
170
+ mockFetch.mockResolvedValueOnce({
171
+ ok: false,
172
+ status: 500,
173
+ statusText: 'Internal Server Error',
174
+ });
175
+
176
+ const result = await ProxyConnectionTester.testConnection();
177
+
178
+ expect(result.success).toBe(false);
179
+ expect(result.message).toContain('HTTP 500');
180
+ });
181
+
182
+ it('should return failure for network error', async () => {
183
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
184
+
185
+ const result = await ProxyConnectionTester.testConnection();
186
+
187
+ expect(result.success).toBe(false);
188
+ expect(result.message).toBe('Network error');
189
+ expect(result.responseTime).toBeGreaterThanOrEqual(0);
190
+ });
191
+
192
+ it('should return failure for timeout', async () => {
193
+ mockFetch.mockImplementationOnce(() => {
194
+ return new Promise((_, reject) => {
195
+ const error = new Error('Request aborted');
196
+ error.name = 'AbortError';
197
+ setTimeout(() => reject(error), 50);
198
+ });
199
+ });
200
+
201
+ const result = await ProxyConnectionTester.testConnection('https://www.google.com', 100);
202
+
203
+ expect(result.success).toBe(false);
204
+ expect(result.message).toBeTruthy();
205
+ });
206
+
207
+ it('should return failure for connection refused', async () => {
208
+ mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
209
+
210
+ const result = await ProxyConnectionTester.testConnection();
211
+
212
+ expect(result.success).toBe(false);
213
+ expect(result.message).toBe('ECONNREFUSED');
214
+ });
215
+
216
+ it('should handle unknown error type', async () => {
217
+ mockFetch.mockRejectedValueOnce('String error');
218
+
219
+ const result = await ProxyConnectionTester.testConnection();
220
+
221
+ expect(result.success).toBe(false);
222
+ expect(result.message).toBe('Unknown error');
223
+ });
224
+ });
225
+ });
226
+
227
+ describe('testProxyConfig', () => {
228
+ const validConfig: NetworkProxySettings = {
229
+ enableProxy: true,
230
+ proxyType: 'http',
231
+ proxyServer: 'proxy.example.com',
232
+ proxyPort: '8080',
233
+ proxyRequireAuth: false,
234
+ proxyBypass: 'localhost,127.0.0.1,::1',
235
+ };
236
+
237
+ describe('config validation', () => {
238
+ it('should return failure for invalid config', async () => {
239
+ mockProxyConfigValidator.validate.mockReturnValueOnce({
240
+ isValid: false,
241
+ errors: ['Proxy server is required', 'Invalid port'],
242
+ });
243
+
244
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
245
+
246
+ expect(result.success).toBe(false);
247
+ expect(result.message).toContain('Invalid proxy configuration');
248
+ expect(result.message).toContain('Proxy server is required');
249
+ expect(result.message).toContain('Invalid port');
250
+ });
251
+
252
+ it('should validate config before testing', async () => {
253
+ mockFetch.mockResolvedValueOnce({
254
+ ok: true,
255
+ status: 200,
256
+ statusText: 'OK',
257
+ });
258
+
259
+ await ProxyConnectionTester.testProxyConfig(validConfig);
260
+
261
+ expect(mockProxyConfigValidator.validate).toHaveBeenCalledWith(validConfig);
262
+ });
263
+ });
264
+
265
+ describe('disabled proxy', () => {
266
+ it('should test direct connection when proxy is disabled', async () => {
267
+ const disabledConfig: NetworkProxySettings = {
268
+ ...validConfig,
269
+ enableProxy: false,
270
+ };
271
+
272
+ mockFetch.mockResolvedValueOnce({
273
+ ok: true,
274
+ status: 200,
275
+ statusText: 'OK',
276
+ });
277
+
278
+ const result = await ProxyConnectionTester.testProxyConfig(disabledConfig);
279
+
280
+ expect(result.success).toBe(true);
281
+ expect(mockFetch).toHaveBeenCalled();
282
+ });
283
+
284
+ it('should use custom test URL for disabled proxy', async () => {
285
+ const disabledConfig: NetworkProxySettings = {
286
+ ...validConfig,
287
+ enableProxy: false,
288
+ };
289
+
290
+ mockFetch.mockResolvedValueOnce({
291
+ ok: true,
292
+ status: 200,
293
+ statusText: 'OK',
294
+ });
295
+
296
+ const customUrl = 'https://api.example.com';
297
+ await ProxyConnectionTester.testProxyConfig(disabledConfig, customUrl);
298
+
299
+ expect(mockFetch).toHaveBeenCalledWith(customUrl, expect.any(Object));
300
+ });
301
+ });
302
+
303
+ describe('enabled proxy', () => {
304
+ it('should test proxy connection successfully', async () => {
305
+ mockFetch.mockResolvedValueOnce({
306
+ ok: true,
307
+ status: 200,
308
+ statusText: 'OK',
309
+ });
310
+
311
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
312
+
313
+ expect(result.success).toBe(true);
314
+ expect(result.responseTime).toBeGreaterThanOrEqual(0);
315
+ });
316
+
317
+ it('should create temporary proxy agent for testing', async () => {
318
+ mockFetch.mockResolvedValueOnce({
319
+ ok: true,
320
+ status: 200,
321
+ statusText: 'OK',
322
+ });
323
+
324
+ await ProxyConnectionTester.testProxyConfig(validConfig);
325
+
326
+ expect(mockProxyDispatcherManager.createProxyAgent).toHaveBeenCalledWith(
327
+ 'http',
328
+ 'http://proxy.example.com:8080',
329
+ );
330
+ });
331
+
332
+ it('should restore original dispatcher after test', async () => {
333
+ mockFetch.mockResolvedValueOnce({
334
+ ok: true,
335
+ status: 200,
336
+ statusText: 'OK',
337
+ });
338
+
339
+ await ProxyConnectionTester.testProxyConfig(validConfig);
340
+
341
+ expect(mockSetGlobalDispatcher).toHaveBeenCalledWith(mockOriginalDispatcher);
342
+ });
343
+
344
+ it('should destroy temporary agent after test', async () => {
345
+ mockFetch.mockResolvedValueOnce({
346
+ ok: true,
347
+ status: 200,
348
+ statusText: 'OK',
349
+ });
350
+
351
+ await ProxyConnectionTester.testProxyConfig(validConfig);
352
+
353
+ expect(mockAgent.destroy).toHaveBeenCalled();
354
+ });
355
+
356
+ it('should restore dispatcher even if test fails', async () => {
357
+ mockFetch.mockRejectedValueOnce(new Error('Connection failed'));
358
+
359
+ await ProxyConnectionTester.testProxyConfig(validConfig);
360
+
361
+ expect(mockSetGlobalDispatcher).toHaveBeenCalledWith(mockOriginalDispatcher);
362
+ });
363
+
364
+ it('should destroy agent even if test fails', async () => {
365
+ mockFetch.mockRejectedValueOnce(new Error('Connection failed'));
366
+
367
+ await ProxyConnectionTester.testProxyConfig(validConfig);
368
+
369
+ expect(mockAgent.destroy).toHaveBeenCalled();
370
+ });
371
+
372
+ it('should handle agent destroy failure gracefully', async () => {
373
+ mockAgent.destroy.mockRejectedValueOnce(new Error('Destroy failed'));
374
+ mockFetch.mockResolvedValueOnce({
375
+ ok: true,
376
+ status: 200,
377
+ statusText: 'OK',
378
+ });
379
+
380
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
381
+
382
+ expect(result.success).toBe(true);
383
+ });
384
+
385
+ it('should test with custom URL', async () => {
386
+ mockFetch.mockResolvedValueOnce({
387
+ ok: true,
388
+ status: 200,
389
+ statusText: 'OK',
390
+ });
391
+
392
+ const customUrl = 'https://httpbin.org/ip';
393
+ await ProxyConnectionTester.testProxyConfig(validConfig, customUrl);
394
+
395
+ expect(mockFetch).toHaveBeenCalledWith(
396
+ customUrl,
397
+ expect.objectContaining({
398
+ dispatcher: mockAgent,
399
+ }),
400
+ );
401
+ });
402
+
403
+ it('should test socks5 proxy', async () => {
404
+ const socks5Config: NetworkProxySettings = {
405
+ ...validConfig,
406
+ proxyType: 'socks5',
407
+ };
408
+
409
+ mockFetch.mockResolvedValueOnce({
410
+ ok: true,
411
+ status: 200,
412
+ statusText: 'OK',
413
+ });
414
+
415
+ await ProxyConnectionTester.testProxyConfig(socks5Config);
416
+
417
+ expect(mockProxyDispatcherManager.createProxyAgent).toHaveBeenCalledWith(
418
+ 'socks5',
419
+ expect.any(String),
420
+ );
421
+ });
422
+
423
+ it('should test proxy with authentication', async () => {
424
+ const authConfig: NetworkProxySettings = {
425
+ ...validConfig,
426
+ proxyRequireAuth: true,
427
+ proxyUsername: 'user',
428
+ proxyPassword: 'pass',
429
+ };
430
+
431
+ mockFetch.mockResolvedValueOnce({
432
+ ok: true,
433
+ status: 200,
434
+ statusText: 'OK',
435
+ });
436
+
437
+ await ProxyConnectionTester.testProxyConfig(authConfig);
438
+
439
+ expect(mockProxyUrlBuilder).toHaveBeenCalledWith(authConfig);
440
+ });
441
+ });
442
+
443
+ describe('error handling', () => {
444
+ it('should return failure when agent creation fails', async () => {
445
+ mockProxyDispatcherManager.createProxyAgent.mockImplementationOnce(() => {
446
+ throw new Error('Agent creation failed');
447
+ });
448
+
449
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
450
+
451
+ expect(result.success).toBe(false);
452
+ expect(result.message).toContain('Proxy test failed');
453
+ expect(result.message).toContain('Agent creation failed');
454
+ });
455
+
456
+ it('should return failure when fetch fails', async () => {
457
+ mockFetch.mockRejectedValueOnce(new Error('Connection timeout'));
458
+
459
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
460
+
461
+ expect(result.success).toBe(false);
462
+ expect(result.message).toContain('Connection timeout');
463
+ });
464
+
465
+ it('should return failure for HTTP error response', async () => {
466
+ mockFetch.mockResolvedValueOnce({
467
+ ok: false,
468
+ status: 407,
469
+ statusText: 'Proxy Authentication Required',
470
+ });
471
+
472
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
473
+
474
+ expect(result.success).toBe(false);
475
+ expect(result.message).toContain('HTTP 407');
476
+ });
477
+
478
+ it('should handle timeout correctly', async () => {
479
+ mockFetch.mockImplementationOnce(() => {
480
+ return new Promise((_, reject) => {
481
+ setTimeout(() => reject(new Error('Timeout')), 50);
482
+ });
483
+ });
484
+
485
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
486
+
487
+ expect(result.success).toBe(false);
488
+ });
489
+
490
+ it('should handle unknown error type', async () => {
491
+ mockProxyDispatcherManager.createProxyAgent.mockImplementationOnce(() => {
492
+ throw 'String error';
493
+ });
494
+
495
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
496
+
497
+ expect(result.success).toBe(false);
498
+ expect(result.message).toContain('Unknown error');
499
+ });
500
+
501
+ it('should handle null agent', async () => {
502
+ mockProxyDispatcherManager.createProxyAgent.mockReturnValueOnce(null);
503
+
504
+ mockFetch.mockResolvedValueOnce({
505
+ ok: true,
506
+ status: 200,
507
+ statusText: 'OK',
508
+ });
509
+
510
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
511
+
512
+ // Should handle gracefully
513
+ expect(result).toBeDefined();
514
+ });
515
+
516
+ it('should handle agent without destroy method', async () => {
517
+ mockProxyDispatcherManager.createProxyAgent.mockReturnValueOnce({});
518
+
519
+ mockFetch.mockResolvedValueOnce({
520
+ ok: true,
521
+ status: 200,
522
+ statusText: 'OK',
523
+ });
524
+
525
+ const result = await ProxyConnectionTester.testProxyConfig(validConfig);
526
+
527
+ expect(result.success).toBe(true);
528
+ });
529
+ });
530
+ });
531
+ });