flock-core 0.5.0b71__py3-none-any.whl → 0.5.1__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (65) hide show
  1. flock/agent.py +39 -1
  2. flock/artifacts.py +17 -10
  3. flock/cli.py +1 -1
  4. flock/dashboard/__init__.py +2 -0
  5. flock/dashboard/collector.py +282 -6
  6. flock/dashboard/events.py +6 -0
  7. flock/dashboard/graph_builder.py +563 -0
  8. flock/dashboard/launcher.py +11 -6
  9. flock/dashboard/models/__init__.py +1 -0
  10. flock/dashboard/models/graph.py +156 -0
  11. flock/dashboard/service.py +175 -14
  12. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
  13. flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
  14. flock/dashboard/static_v2/index.html +13 -0
  15. flock/dashboard/websocket.py +2 -2
  16. flock/engines/dspy_engine.py +294 -20
  17. flock/frontend/README.md +6 -6
  18. flock/frontend/src/App.tsx +23 -31
  19. flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
  20. flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
  21. flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
  22. flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
  23. flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
  24. flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
  25. flock/frontend/src/components/graph/AgentNode.tsx +8 -6
  26. flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
  27. flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
  28. flock/frontend/src/components/graph/MessageNode.tsx +16 -3
  29. flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
  30. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
  31. flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
  32. flock/frontend/src/hooks/useModules.ts +12 -4
  33. flock/frontend/src/hooks/usePersistence.ts +5 -3
  34. flock/frontend/src/services/api.ts +3 -19
  35. flock/frontend/src/services/graphService.test.ts +330 -0
  36. flock/frontend/src/services/graphService.ts +75 -0
  37. flock/frontend/src/services/websocket.ts +104 -268
  38. flock/frontend/src/store/filterStore.test.ts +89 -1
  39. flock/frontend/src/store/filterStore.ts +38 -16
  40. flock/frontend/src/store/graphStore.test.ts +538 -173
  41. flock/frontend/src/store/graphStore.ts +374 -465
  42. flock/frontend/src/store/moduleStore.ts +51 -33
  43. flock/frontend/src/store/uiStore.ts +23 -11
  44. flock/frontend/src/types/graph.ts +77 -44
  45. flock/frontend/src/utils/mockData.ts +16 -3
  46. flock/frontend/vite.config.ts +2 -2
  47. flock/orchestrator.py +27 -7
  48. flock/patches/__init__.py +5 -0
  49. flock/patches/dspy_streaming_patch.py +82 -0
  50. flock/service.py +2 -2
  51. flock/store.py +169 -4
  52. flock/themes/darkmatrix.toml +2 -2
  53. flock/themes/deep.toml +2 -2
  54. flock/themes/neopolitan.toml +4 -4
  55. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/METADATA +20 -13
  56. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/RECORD +59 -53
  57. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
  58. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
  59. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
  60. flock/frontend/src/services/websocket.test.ts +0 -595
  61. flock/frontend/src/utils/transforms.test.ts +0 -860
  62. flock/frontend/src/utils/transforms.ts +0 -323
  63. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/WHEEL +0 -0
  64. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/entry_points.txt +0 -0
  65. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,595 +0,0 @@
1
- /**
2
- * Unit tests for WebSocket client service.
3
- *
4
- * Tests verify connection management, reconnection with exponential backoff,
5
- * message buffering, event handling, and heartbeat/pong handling.
6
- */
7
-
8
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
-
10
- // Mock WebSocket
11
- class MockWebSocket {
12
- url: string;
13
- readyState: number;
14
- onopen: ((event: Event) => void) | null = null;
15
- onclose: ((event: CloseEvent) => void) | null = null;
16
- onerror: ((event: Event) => void) | null = null;
17
- onmessage: ((event: MessageEvent) => void) | null = null;
18
-
19
- static CONNECTING = 0;
20
- static OPEN = 1;
21
- static CLOSING = 2;
22
- static CLOSED = 3;
23
-
24
- sentMessages: string[] = [];
25
- private autoConnectTimerId: any = null;
26
- private errorSimulated = false;
27
- static skipAutoConnect = false; // Global flag for controlling auto-connect
28
-
29
- constructor(url: string) {
30
- this.url = url;
31
- this.readyState = MockWebSocket.CONNECTING;
32
-
33
- // Simulate connection opening (can be prevented by simulateError or global flag)
34
- if (!MockWebSocket.skipAutoConnect) {
35
- this.autoConnectTimerId = setTimeout(() => {
36
- if (!this.errorSimulated && this.readyState === MockWebSocket.CONNECTING) {
37
- this.readyState = MockWebSocket.OPEN;
38
- if (this.onopen) {
39
- this.onopen(new Event('open'));
40
- }
41
- }
42
- }, 0);
43
- }
44
- }
45
-
46
- // Method to manually trigger connection success (for timeout tests)
47
- manuallyConnect(): void {
48
- if (this.readyState === MockWebSocket.CONNECTING) {
49
- this.readyState = MockWebSocket.OPEN;
50
- if (this.onopen) {
51
- this.onopen(new Event('open'));
52
- }
53
- }
54
- }
55
-
56
- send(data: string): void {
57
- if (this.readyState !== MockWebSocket.OPEN) {
58
- throw new Error('WebSocket is not open');
59
- }
60
- this.sentMessages.push(data);
61
- }
62
-
63
- close(code?: number, reason?: string): void {
64
- this.readyState = MockWebSocket.CLOSING;
65
- setTimeout(() => {
66
- this.readyState = MockWebSocket.CLOSED;
67
- if (this.onclose) {
68
- this.onclose(new CloseEvent('close', { code: code || 1000, reason }));
69
- }
70
- }, 0);
71
- }
72
-
73
- // Test helpers
74
- simulateMessage(data: any): void {
75
- if (this.onmessage) {
76
- this.onmessage(new MessageEvent('message', { data: JSON.stringify(data) }));
77
- }
78
- }
79
-
80
- simulateError(): void {
81
- this.errorSimulated = true;
82
- // Clear auto-connect timer if still pending
83
- if (this.autoConnectTimerId) {
84
- clearTimeout(this.autoConnectTimerId);
85
- this.autoConnectTimerId = null;
86
- }
87
- // Transition to closed state
88
- this.readyState = MockWebSocket.CLOSED;
89
- if (this.onerror) {
90
- this.onerror(new Event('error'));
91
- }
92
- // Also trigger onclose after error
93
- setTimeout(() => {
94
- if (this.onclose) {
95
- this.onclose(new CloseEvent('close', { code: 1006, reason: 'Connection error' }));
96
- }
97
- }, 0);
98
- }
99
-
100
- simulateClose(code: number = 1000): void {
101
- this.readyState = MockWebSocket.CLOSED;
102
- if (this.onclose) {
103
- this.onclose(new CloseEvent('close', { code }));
104
- }
105
- }
106
- }
107
-
108
- // Replace global WebSocket with mock
109
- (globalThis as any).WebSocket = MockWebSocket;
110
-
111
- describe('WebSocketClient', () => {
112
- let WebSocketClient: any;
113
- let client: any;
114
- let mockStore: any;
115
-
116
- beforeEach(async () => {
117
- // Reset timers
118
- vi.useFakeTimers();
119
-
120
- // Mock store for event dispatching
121
- mockStore = {
122
- addAgent: vi.fn(),
123
- updateAgent: vi.fn(),
124
- addMessage: vi.fn(),
125
- batchUpdate: vi.fn(),
126
- };
127
-
128
- // Dynamic import to avoid module-level errors before implementation
129
- try {
130
- const module = await import('./websocket');
131
- WebSocketClient = module.WebSocketClient;
132
- client = new WebSocketClient('ws://localhost:8080/ws', mockStore);
133
- } catch (error) {
134
- // Skip tests if WebSocketClient not implemented yet (TDD approach)
135
- throw new Error('WebSocketClient not implemented yet - skipping tests');
136
- }
137
- });
138
-
139
- afterEach(() => {
140
- if (client) {
141
- client.disconnect();
142
- }
143
- vi.clearAllTimers();
144
- vi.useRealTimers();
145
- });
146
-
147
- it('should connect to WebSocket server', async () => {
148
- // Connect
149
- client.connect();
150
-
151
- // Wait for connection
152
- await vi.runAllTimersAsync();
153
-
154
- // Verify connection status
155
- expect(client.isConnected()).toBe(true);
156
- expect(client.getConnectionStatus()).toBe('connected');
157
- });
158
-
159
- it('should handle connection failure', async () => {
160
- // Prevent auto-reconnect for this test
161
- MockWebSocket.skipAutoConnect = true;
162
-
163
- // Connect
164
- client.connect();
165
-
166
- // Simulate connection error before connection succeeds
167
- const ws = client.ws as MockWebSocket;
168
- ws.simulateError();
169
-
170
- // Wait for error handling (just the error, not reconnection)
171
- await vi.advanceTimersByTimeAsync(100);
172
-
173
- // Verify connection status
174
- expect(client.isConnected()).toBe(false);
175
- expect(client.getConnectionStatus()).toBe('error');
176
-
177
- // Reset flag
178
- MockWebSocket.skipAutoConnect = false;
179
- });
180
-
181
- it('should reconnect with exponential backoff (1s, 2s, 4s, 8s, max 30s)', async () => {
182
- // Connect
183
- client.connect();
184
- await vi.runAllTimersAsync();
185
-
186
- // Simulate disconnection
187
- (client.ws as MockWebSocket).simulateClose(1006); // Abnormal closure
188
-
189
- // Track reconnection attempts
190
- const reconnectTimes: number[] = [];
191
-
192
- // Override connect to track timing
193
- const originalConnect = client.connect.bind(client);
194
- client.connect = vi.fn(() => {
195
- reconnectTimes.push(Date.now());
196
- originalConnect();
197
- });
198
-
199
- // Wait for first reconnection attempt (1s)
200
- await vi.advanceTimersByTimeAsync(1000);
201
- expect(reconnectTimes.length).toBe(1);
202
-
203
- // Simulate failure, wait for second attempt (2s)
204
- (client.ws as MockWebSocket)?.simulateClose(1006);
205
- await vi.advanceTimersByTimeAsync(2000);
206
- expect(reconnectTimes.length).toBe(2);
207
-
208
- // Simulate failure, wait for third attempt (4s)
209
- (client.ws as MockWebSocket)?.simulateClose(1006);
210
- await vi.advanceTimersByTimeAsync(4000);
211
- expect(reconnectTimes.length).toBe(3);
212
-
213
- // Simulate failure, wait for fourth attempt (8s)
214
- (client.ws as MockWebSocket)?.simulateClose(1006);
215
- await vi.advanceTimersByTimeAsync(8000);
216
- expect(reconnectTimes.length).toBe(4);
217
-
218
- // Verify max backoff (30s)
219
- (client.ws as MockWebSocket)?.simulateClose(1006);
220
- await vi.advanceTimersByTimeAsync(16000); // Would be 16s, but capped at 30s
221
- await vi.advanceTimersByTimeAsync(30000);
222
- expect(reconnectTimes.length).toBe(5);
223
- });
224
-
225
- it('should buffer messages during disconnection (max 100 messages)', async () => {
226
- // Connect
227
- client.connect();
228
- await vi.runAllTimersAsync();
229
-
230
- // Disconnect
231
- client.disconnect();
232
-
233
- // Try to send messages while disconnected
234
- for (let i = 0; i < 120; i++) {
235
- client.send({ type: 'test', index: i });
236
- }
237
-
238
- // Verify messages are buffered (max 100)
239
- expect(client.getBufferedMessageCount()).toBe(100);
240
-
241
- // Reconnect
242
- client.connect();
243
- await vi.runAllTimersAsync();
244
-
245
- // Verify buffered messages are sent
246
- const ws = client.ws as MockWebSocket;
247
- expect(ws.sentMessages.length).toBe(100);
248
-
249
- // Verify oldest messages were kept (FIFO)
250
- const firstMessage = JSON.parse(ws.sentMessages[0]!);
251
- expect(firstMessage.index).toBe(20); // 0-19 were dropped
252
- });
253
-
254
- it('should dispatch received messages to store handlers', async () => {
255
- // Connect
256
- client.connect();
257
- await vi.runAllTimersAsync();
258
-
259
- // Simulate receiving agent_activated event
260
- const ws = client.ws as MockWebSocket;
261
- ws.simulateMessage({
262
- agent_name: 'test_agent',
263
- agent_id: 'test_agent',
264
- consumed_types: ['Input'],
265
- consumed_artifacts: ['artifact-1'],
266
- subscription_info: { from_agents: [], channels: [], mode: 'both' },
267
- labels: ['test'],
268
- tenant_id: null,
269
- max_concurrency: 1,
270
- correlation_id: 'corr-123',
271
- timestamp: '2025-10-03T12:00:00Z',
272
- });
273
-
274
- // Verify store was updated
275
- // Implementation will determine exact handler
276
- // This could be addAgent, updateAgent, or custom event handler
277
- expect(mockStore.addAgent.mock.calls.length + mockStore.updateAgent.mock.calls.length).toBeGreaterThan(0);
278
- });
279
-
280
- it('should update connection status state', async () => {
281
- // Initial state
282
- expect(client.getConnectionStatus()).toBe('disconnected');
283
-
284
- // Connecting
285
- client.connect();
286
- expect(client.getConnectionStatus()).toBe('connecting');
287
-
288
- // Connected
289
- await vi.runAllTimersAsync();
290
- expect(client.getConnectionStatus()).toBe('connected');
291
-
292
- // Disconnecting
293
- client.disconnect();
294
- expect(client.getConnectionStatus()).toBe('disconnecting');
295
-
296
- // Disconnected
297
- await vi.runAllTimersAsync();
298
- expect(client.getConnectionStatus()).toBe('disconnected');
299
-
300
- // Error state
301
- client.connect();
302
- await vi.runAllTimersAsync();
303
- (client.ws as MockWebSocket).simulateError();
304
- expect(client.getConnectionStatus()).toBe('error');
305
- });
306
-
307
- it('should handle heartbeat/pong messages', async () => {
308
- // Connect
309
- client.connect();
310
- await vi.runAllTimersAsync();
311
-
312
- const ws = client.ws as MockWebSocket;
313
-
314
- // Simulate receiving heartbeat/ping from server
315
- ws.simulateMessage({ type: 'ping', timestamp: Date.now() });
316
-
317
- // Verify pong response was sent
318
- const pongMessages = ws.sentMessages.filter(msg => {
319
- try {
320
- const data = JSON.parse(msg);
321
- return data.type === 'pong';
322
- } catch {
323
- return false;
324
- }
325
- });
326
-
327
- expect(pongMessages.length).toBeGreaterThan(0);
328
- });
329
-
330
- it('should handle message_published events', async () => {
331
- // Connect
332
- client.connect();
333
- await vi.runAllTimersAsync();
334
-
335
- // Simulate receiving message_published event
336
- const ws = client.ws as MockWebSocket;
337
- ws.simulateMessage({
338
- artifact_id: 'artifact-123',
339
- artifact_type: 'Movie',
340
- produced_by: 'movie_agent',
341
- payload: { title: 'Inception', year: 2010 },
342
- visibility: { kind: 'Public' },
343
- tags: ['scifi'],
344
- version: 1,
345
- consumers: ['tagline_agent'],
346
- correlation_id: 'corr-456',
347
- timestamp: '2025-10-03T12:01:00Z',
348
- });
349
-
350
- // Verify message was added to store
351
- expect(mockStore.addMessage).toHaveBeenCalledWith(
352
- expect.objectContaining({
353
- id: 'artifact-123',
354
- type: 'Movie',
355
- payload: expect.objectContaining({ title: 'Inception' }),
356
- })
357
- );
358
- });
359
-
360
- it('should handle agent_completed events', async () => {
361
- // Connect
362
- client.connect();
363
- await vi.runAllTimersAsync();
364
-
365
- // Simulate receiving agent_completed event
366
- const ws = client.ws as MockWebSocket;
367
- ws.simulateMessage({
368
- agent_name: 'test_agent',
369
- run_id: 'task-123',
370
- duration_ms: 1234.56,
371
- artifacts_produced: ['artifact-1', 'artifact-2'],
372
- metrics: { tokens_used: 500, cost_usd: 0.01 },
373
- final_state: {},
374
- correlation_id: 'corr-789',
375
- timestamp: '2025-10-03T12:02:00Z',
376
- });
377
-
378
- // Verify agent status was updated
379
- expect(mockStore.updateAgent).toHaveBeenCalledWith(
380
- 'test_agent',
381
- expect.objectContaining({
382
- status: 'idle', // or 'completed'
383
- })
384
- );
385
- });
386
-
387
- it('should handle agent_error events', async () => {
388
- // Connect
389
- client.connect();
390
- await vi.runAllTimersAsync();
391
-
392
- // Simulate receiving agent_error event
393
- const ws = client.ws as MockWebSocket;
394
- ws.simulateMessage({
395
- agent_name: 'test_agent',
396
- run_id: 'task-456',
397
- error_type: 'ValueError',
398
- error_message: 'Invalid input',
399
- traceback: 'Traceback...',
400
- failed_at: '2025-10-03T12:03:00Z',
401
- correlation_id: 'corr-999',
402
- timestamp: '2025-10-03T12:03:00Z',
403
- });
404
-
405
- // Verify agent status was updated to error
406
- expect(mockStore.updateAgent).toHaveBeenCalledWith(
407
- 'test_agent',
408
- expect.objectContaining({
409
- status: 'error',
410
- })
411
- );
412
- });
413
-
414
- it('should handle streaming_output events', async () => {
415
- // Connect
416
- client.connect();
417
- await vi.runAllTimersAsync();
418
-
419
- // Simulate receiving streaming_output event
420
- const ws = client.ws as MockWebSocket;
421
- ws.simulateMessage({
422
- agent_name: 'llm_agent',
423
- run_id: 'task-789',
424
- output_type: 'llm_token',
425
- content: 'Generated text...',
426
- sequence: 1,
427
- is_final: false,
428
- correlation_id: 'corr-111',
429
- timestamp: '2025-10-03T12:04:00Z',
430
- });
431
-
432
- // Verify streaming output was handled
433
- // Implementation may use custom handler or update agent state
434
- // This test verifies the event was processed without error
435
- expect(mockStore.updateAgent).toHaveBeenCalled();
436
- });
437
-
438
- it('should clean up on disconnect', async () => {
439
- // Connect
440
- client.connect();
441
- await vi.runAllTimersAsync();
442
-
443
- const ws = client.ws as MockWebSocket;
444
-
445
- // Disconnect
446
- client.disconnect();
447
- await vi.runAllTimersAsync();
448
-
449
- // Verify WebSocket is closed
450
- expect(ws.readyState).toBe(MockWebSocket.CLOSED);
451
-
452
- // Verify no reconnection attempts
453
- await vi.advanceTimersByTimeAsync(5000);
454
- expect(client.isConnected()).toBe(false);
455
- });
456
-
457
- it('should handle malformed JSON messages gracefully', async () => {
458
- // Connect
459
- client.connect();
460
- await vi.runAllTimersAsync();
461
-
462
- const ws = client.ws as MockWebSocket;
463
-
464
- // Simulate receiving malformed JSON
465
- if (ws.onmessage) {
466
- ws.onmessage(new MessageEvent('message', { data: 'invalid json {{{' }));
467
- }
468
-
469
- // Verify client is still connected (error was handled)
470
- expect(client.isConnected()).toBe(true);
471
- });
472
-
473
- it('should support manual reconnection', async () => {
474
- // Connect and disconnect
475
- client.connect();
476
- await vi.runAllTimersAsync();
477
- client.disconnect();
478
- await vi.runAllTimersAsync();
479
-
480
- expect(client.isConnected()).toBe(false);
481
-
482
- // Manual reconnect
483
- client.connect();
484
- await vi.runAllTimersAsync();
485
-
486
- expect(client.isConnected()).toBe(true);
487
- });
488
-
489
- it('should prevent multiple simultaneous connections', async () => {
490
- // Connect
491
- client.connect();
492
- await vi.runAllTimersAsync();
493
-
494
- const firstWs = client.ws;
495
-
496
- // Try to connect again (should use same WebSocket)
497
- client.connect();
498
- await vi.runAllTimersAsync();
499
-
500
- expect(client.ws).toBe(firstWs); // Should reuse existing connection
501
-
502
- // Verify same WebSocket instance (or old one was closed)
503
- // Implementation should either reuse or close old connection
504
- expect(client.ws).toBeDefined();
505
- expect(client.isConnected()).toBe(true);
506
- });
507
- });
508
-
509
- describe('WebSocketClient - Edge Cases', () => {
510
- let WebSocketClient: any;
511
- let client: any;
512
- let mockStore: any;
513
-
514
- beforeEach(async () => {
515
- vi.useFakeTimers();
516
-
517
- mockStore = {
518
- addAgent: vi.fn(),
519
- updateAgent: vi.fn(),
520
- addMessage: vi.fn(),
521
- batchUpdate: vi.fn(),
522
- };
523
-
524
- try {
525
- const module = await import('./websocket');
526
- WebSocketClient = module.WebSocketClient;
527
- client = new WebSocketClient('ws://localhost:8080/ws', mockStore);
528
- } catch (error) {
529
- throw new Error('WebSocketClient not implemented yet - skipping tests');
530
- }
531
- });
532
-
533
- afterEach(() => {
534
- if (client) {
535
- client.disconnect();
536
- }
537
- vi.clearAllTimers();
538
- vi.useRealTimers();
539
- });
540
-
541
- it('should handle rapid connect/disconnect cycles', async () => {
542
- for (let i = 0; i < 5; i++) {
543
- client.connect();
544
- await vi.runAllTimersAsync();
545
- expect(client.isConnected()).toBe(true);
546
-
547
- client.disconnect();
548
- await vi.runAllTimersAsync();
549
- expect(client.isConnected()).toBe(false);
550
- }
551
- });
552
-
553
- it('should handle connection timeout', async () => {
554
- // Prevent auto-connect in mock
555
- MockWebSocket.skipAutoConnect = true;
556
-
557
- // Connect
558
- client.connect();
559
-
560
- // Don't simulate open event - let connection timeout
561
- await vi.advanceTimersByTimeAsync(10000); // 10 second timeout
562
-
563
- // Verify timeout was handled
564
- // Implementation should either retry or emit error
565
- expect(client.getConnectionStatus()).toMatch(/error|connecting/);
566
-
567
- // Reset flag
568
- MockWebSocket.skipAutoConnect = false;
569
- });
570
-
571
- it('should reset backoff on successful connection', async () => {
572
- // Initial connection
573
- client.connect();
574
- await vi.runAllTimersAsync();
575
-
576
- // Disconnect and trigger exponential backoff
577
- (client.ws as MockWebSocket).simulateClose(1006);
578
- await vi.advanceTimersByTimeAsync(1000);
579
- (client.ws as MockWebSocket)?.simulateClose(1006);
580
- await vi.advanceTimersByTimeAsync(2000);
581
-
582
- // Successful connection
583
- client.connect();
584
- await vi.runAllTimersAsync();
585
- expect(client.isConnected()).toBe(true);
586
-
587
- // Next disconnection should start at 1s again (backoff reset)
588
- (client.ws as MockWebSocket).simulateClose(1006);
589
- const startTime = Date.now();
590
- await vi.advanceTimersByTimeAsync(1000);
591
-
592
- // Verify reconnection happened at 1s (not continuing exponential backoff)
593
- expect(Date.now() - startTime).toBeLessThan(1500);
594
- });
595
- });