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.
- flock/agent.py +39 -1
- flock/artifacts.py +17 -10
- flock/cli.py +1 -1
- flock/dashboard/__init__.py +2 -0
- flock/dashboard/collector.py +282 -6
- flock/dashboard/events.py +6 -0
- flock/dashboard/graph_builder.py +563 -0
- flock/dashboard/launcher.py +11 -6
- flock/dashboard/models/__init__.py +1 -0
- flock/dashboard/models/graph.py +156 -0
- flock/dashboard/service.py +175 -14
- flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
- flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
- flock/dashboard/static_v2/index.html +13 -0
- flock/dashboard/websocket.py +2 -2
- flock/engines/dspy_engine.py +294 -20
- flock/frontend/README.md +6 -6
- flock/frontend/src/App.tsx +23 -31
- flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
- flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
- flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
- flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
- flock/frontend/src/components/graph/AgentNode.tsx +8 -6
- flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
- flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
- flock/frontend/src/components/graph/MessageNode.tsx +16 -3
- flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
- flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
- flock/frontend/src/hooks/useModules.ts +12 -4
- flock/frontend/src/hooks/usePersistence.ts +5 -3
- flock/frontend/src/services/api.ts +3 -19
- flock/frontend/src/services/graphService.test.ts +330 -0
- flock/frontend/src/services/graphService.ts +75 -0
- flock/frontend/src/services/websocket.ts +104 -268
- flock/frontend/src/store/filterStore.test.ts +89 -1
- flock/frontend/src/store/filterStore.ts +38 -16
- flock/frontend/src/store/graphStore.test.ts +538 -173
- flock/frontend/src/store/graphStore.ts +374 -465
- flock/frontend/src/store/moduleStore.ts +51 -33
- flock/frontend/src/store/uiStore.ts +23 -11
- flock/frontend/src/types/graph.ts +77 -44
- flock/frontend/src/utils/mockData.ts +16 -3
- flock/frontend/vite.config.ts +2 -2
- flock/orchestrator.py +27 -7
- flock/patches/__init__.py +5 -0
- flock/patches/dspy_streaming_patch.py +82 -0
- flock/service.py +2 -2
- flock/store.py +169 -4
- flock/themes/darkmatrix.toml +2 -2
- flock/themes/deep.toml +2 -2
- flock/themes/neopolitan.toml +4 -4
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/METADATA +20 -13
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/RECORD +59 -53
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
- flock/frontend/src/services/websocket.test.ts +0 -595
- flock/frontend/src/utils/transforms.test.ts +0 -860
- flock/frontend/src/utils/transforms.ts +0 -323
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/entry_points.txt +0 -0
- {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
|
-
});
|