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,205 +1,570 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
1
+ /**
2
+ * GraphStore Tests - NEW Simplified Architecture
3
+ *
4
+ * Tests for Phase 2: Backend snapshot consumption replacing client-side graph construction
5
+ *
6
+ * FOCUS: Backend integration, position merging, real-time WebSocket overlays
7
+ * NOT: Edge derivation algorithms (now handled by backend)
8
+ *
9
+ * Specification: docs/specs/002-ui-optimization-migration/
10
+ */
11
+
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
13
  import { useGraphStore } from './graphStore';
3
- import { Agent, Message } from '../types/graph';
14
+ import { fetchGraphSnapshot, mergeNodePositions, overlayWebSocketState } from '../services/graphService';
15
+ import { useFilterStore } from './filterStore';
16
+ import { GraphSnapshot, GraphNode } from '../types/graph';
17
+ import { Node } from '@xyflow/react';
18
+
19
+ // Mock dependencies
20
+ vi.mock('../services/graphService', () => ({
21
+ fetchGraphSnapshot: vi.fn(),
22
+ mergeNodePositions: vi.fn(),
23
+ overlayWebSocketState: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('./filterStore', () => ({
27
+ useFilterStore: {
28
+ getState: vi.fn(),
29
+ },
30
+ }));
31
+
32
+ vi.mock('../hooks/usePersistence', () => ({
33
+ usePersistence: vi.fn(() => ({
34
+ loadPositions: vi.fn().mockResolvedValue(new Map()),
35
+ savePositions: vi.fn(),
36
+ })),
37
+ }));
4
38
 
5
- describe('graphStore', () => {
39
+ describe('graphStore - NEW Simplified Architecture', () => {
6
40
  beforeEach(() => {
7
- // Reset store before each test
41
+ // Reset store state before each test
8
42
  useGraphStore.setState({
9
- agents: new Map(),
10
- messages: new Map(),
11
- events: [],
12
- runs: new Map(),
13
- consumptions: new Map(),
43
+ agentStatus: new Map(),
44
+ streamingTokens: new Map(),
14
45
  nodes: [],
15
46
  edges: [],
47
+ statistics: null,
48
+ events: [],
49
+ viewMode: 'agent',
16
50
  });
51
+
52
+ // Clear all mocks
53
+ vi.clearAllMocks();
54
+
55
+ // Setup default filter store mock
56
+ vi.mocked(useFilterStore.getState).mockReturnValue({
57
+ correlationId: null,
58
+ timeRange: { preset: 'last10min' },
59
+ selectedArtifactTypes: [],
60
+ selectedProducers: [],
61
+ selectedTags: [],
62
+ selectedVisibility: [],
63
+ updateFacets: vi.fn(),
64
+ } as any);
17
65
  });
18
66
 
19
- it('should add an agent', () => {
20
- const agent: Agent = {
21
- id: 'test-agent',
22
- name: 'test-agent',
23
- status: 'idle',
24
- subscriptions: ['Movie'],
25
- lastActive: Date.now(),
26
- sentCount: 0,
27
- recvCount: 0,
28
- };
29
-
30
- useGraphStore.getState().addAgent(agent);
31
-
32
- const agents = useGraphStore.getState().agents;
33
- expect(agents.size).toBe(1);
34
- expect(agents.get('test-agent')).toEqual(agent);
67
+ afterEach(() => {
68
+ vi.restoreAllMocks();
35
69
  });
36
70
 
37
- it('should update an agent', () => {
38
- const agent: Agent = {
39
- id: 'test-agent',
40
- name: 'test-agent',
41
- status: 'idle',
42
- subscriptions: [],
43
- lastActive: Date.now(),
44
- sentCount: 0,
45
- recvCount: 0,
46
- };
47
-
48
- useGraphStore.getState().addAgent(agent);
49
- useGraphStore.getState().updateAgent('test-agent', { status: 'running', sentCount: 5 });
50
-
51
- const updatedAgent = useGraphStore.getState().agents.get('test-agent');
52
- expect(updatedAgent?.status).toBe('running');
53
- expect(updatedAgent?.sentCount).toBe(5);
71
+ describe('generateAgentViewGraph()', () => {
72
+ it('should fetch agent view graph from backend', async () => {
73
+ const mockSnapshot: GraphSnapshot = {
74
+ nodes: [
75
+ { id: 'agent1', type: 'agent', data: { name: 'pizza_master' }, position: { x: 0, y: 0 }, hidden: false },
76
+ ],
77
+ edges: [
78
+ { id: 'edge1', source: 'agent1', target: 'agent2', type: 'message_flow', data: {}, hidden: false },
79
+ ],
80
+ statistics: null,
81
+ viewMode: 'agent',
82
+ filters: {
83
+ correlation_id: null,
84
+ time_range: { preset: 'last10min' },
85
+ artifactTypes: [],
86
+ producers: [],
87
+ tags: [],
88
+ visibility: [],
89
+ },
90
+ generatedAt: '2025-10-11T00:00:00Z',
91
+ totalArtifacts: 1,
92
+ truncated: false,
93
+ };
94
+
95
+ const mockMergedNodes: Node[] = [
96
+ { id: 'agent1', type: 'agent', data: { name: 'pizza_master' }, position: { x: 100, y: 100 } } as Node,
97
+ ];
98
+
99
+ vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
100
+ vi.mocked(mergeNodePositions).mockReturnValue(mockMergedNodes);
101
+ vi.mocked(overlayWebSocketState).mockReturnValue(mockMergedNodes);
102
+
103
+ await useGraphStore.getState().generateAgentViewGraph();
104
+
105
+ // Verify backend API call
106
+ expect(fetchGraphSnapshot).toHaveBeenCalledWith({
107
+ viewMode: 'agent',
108
+ filters: {
109
+ correlation_id: null,
110
+ time_range: { preset: 'last10min' },
111
+ artifactTypes: [],
112
+ producers: [],
113
+ tags: [],
114
+ visibility: [],
115
+ },
116
+ options: { include_statistics: true },
117
+ });
118
+
119
+ // Verify state updated
120
+ const state = useGraphStore.getState();
121
+ expect(state.nodes).toHaveLength(1);
122
+ expect(state.edges).toHaveLength(1);
123
+ expect(state.viewMode).toBe('agent');
124
+ });
125
+
126
+ it('should pass filters from filterStore to backend', async () => {
127
+ const mockFilters = {
128
+ correlationId: 'test-correlation-id',
129
+ timeRange: { preset: 'last1hour' as const },
130
+ selectedArtifactTypes: ['Pizza', 'Order'],
131
+ selectedProducers: ['pizza_master', 'waiter'],
132
+ selectedTags: ['urgent'],
133
+ selectedVisibility: ['public'],
134
+ updateFacets: vi.fn(),
135
+ };
136
+
137
+ vi.mocked(useFilterStore.getState).mockReturnValue(mockFilters as any);
138
+
139
+ const mockSnapshot: GraphSnapshot = {
140
+ nodes: [],
141
+ edges: [],
142
+ statistics: null,
143
+ viewMode: 'agent',
144
+ filters: {
145
+ correlation_id: 'test-correlation-id',
146
+ time_range: { preset: 'last1hour' },
147
+ artifactTypes: ['Pizza', 'Order'],
148
+ producers: ['pizza_master', 'waiter'],
149
+ tags: ['urgent'],
150
+ visibility: ['public'],
151
+ },
152
+ generatedAt: '2025-10-11T00:00:00Z',
153
+ totalArtifacts: 0,
154
+ truncated: false,
155
+ };
156
+
157
+ vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
158
+ vi.mocked(mergeNodePositions).mockReturnValue([]);
159
+ vi.mocked(overlayWebSocketState).mockReturnValue([]);
160
+
161
+ await useGraphStore.getState().generateAgentViewGraph();
162
+
163
+ expect(fetchGraphSnapshot).toHaveBeenCalledWith({
164
+ viewMode: 'agent',
165
+ filters: {
166
+ correlation_id: 'test-correlation-id',
167
+ time_range: { preset: 'last1hour' },
168
+ artifactTypes: ['Pizza', 'Order'],
169
+ producers: ['pizza_master', 'waiter'],
170
+ tags: ['urgent'],
171
+ visibility: ['public'],
172
+ },
173
+ options: { include_statistics: true },
174
+ });
175
+ });
176
+
177
+ it('should update filter facets from backend statistics', async () => {
178
+ const mockUpdateFacets = vi.fn();
179
+ vi.mocked(useFilterStore.getState).mockReturnValue({
180
+ correlationId: null,
181
+ timeRange: { preset: 'last10min' },
182
+ selectedArtifactTypes: [],
183
+ selectedProducers: [],
184
+ selectedTags: [],
185
+ selectedVisibility: [],
186
+ updateFacets: mockUpdateFacets,
187
+ } as any);
188
+
189
+ const mockSnapshot: GraphSnapshot = {
190
+ nodes: [],
191
+ edges: [],
192
+ statistics: {
193
+ producedByAgent: {},
194
+ consumedByAgent: {},
195
+ artifactSummary: {
196
+ total: 100,
197
+ by_type: { Pizza: 50, Order: 50 },
198
+ by_producer: { pizza_master: 75, waiter: 25 },
199
+ by_visibility: { public: 100 },
200
+ tag_counts: { urgent: 10 },
201
+ earliest_created_at: '2025-10-11T00:00:00Z',
202
+ latest_created_at: '2025-10-11T01:00:00Z',
203
+ },
204
+ },
205
+ viewMode: 'agent',
206
+ filters: {
207
+ correlation_id: null,
208
+ time_range: { preset: 'last10min' },
209
+ artifactTypes: [],
210
+ producers: [],
211
+ tags: [],
212
+ visibility: [],
213
+ },
214
+ generatedAt: '2025-10-11T00:00:00Z',
215
+ totalArtifacts: 100,
216
+ truncated: false,
217
+ };
218
+
219
+ vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
220
+ vi.mocked(mergeNodePositions).mockReturnValue([]);
221
+ vi.mocked(overlayWebSocketState).mockReturnValue([]);
222
+
223
+ await useGraphStore.getState().generateAgentViewGraph();
224
+
225
+ // Verify facets are transformed from artifactSummary
226
+ expect(mockUpdateFacets).toHaveBeenCalledWith({
227
+ artifactTypes: ['Pizza', 'Order'],
228
+ producers: ['pizza_master', 'waiter'],
229
+ tags: ['urgent'],
230
+ visibilities: ['public'],
231
+ });
232
+ });
233
+
234
+ it('should handle API errors gracefully', async () => {
235
+ vi.mocked(fetchGraphSnapshot).mockRejectedValue(new Error('API error'));
236
+
237
+ await expect(useGraphStore.getState().generateAgentViewGraph()).rejects.toThrow('API error');
238
+ });
54
239
  });
55
240
 
56
- it('should add a message', () => {
57
- const message: Message = {
58
- id: 'msg-1',
59
- type: 'Movie',
60
- payload: { title: 'Test Movie' },
61
- timestamp: Date.now(),
62
- correlationId: 'corr-1',
63
- producedBy: 'movie',
64
- };
65
-
66
- useGraphStore.getState().addMessage(message);
67
-
68
- const messages = useGraphStore.getState().messages;
69
- const events = useGraphStore.getState().events;
70
- expect(messages.size).toBe(1);
71
- expect(messages.get('msg-1')).toEqual(message);
72
- expect(events.length).toBe(1);
73
- expect(events[0]).toEqual(message);
241
+ describe('generateBlackboardViewGraph()', () => {
242
+ it('should fetch blackboard view graph from backend', async () => {
243
+ const mockSnapshot: GraphSnapshot = {
244
+ nodes: [
245
+ { id: 'msg1', type: 'message', data: { artifact_type: 'Pizza' }, position: { x: 0, y: 0 }, hidden: false },
246
+ ],
247
+ edges: [
248
+ { id: 'edge1', source: 'msg1', target: 'msg2', type: 'transformation', data: {}, hidden: false },
249
+ ],
250
+ statistics: null,
251
+ viewMode: 'blackboard',
252
+ filters: {
253
+ correlation_id: null,
254
+ time_range: { preset: 'last10min' },
255
+ artifactTypes: [],
256
+ producers: [],
257
+ tags: [],
258
+ visibility: [],
259
+ },
260
+ generatedAt: '2025-10-11T00:00:00Z',
261
+ totalArtifacts: 1,
262
+ truncated: false,
263
+ };
264
+
265
+ const mockMergedNodes: Node[] = [
266
+ { id: 'msg1', type: 'message', data: { artifact_type: 'Pizza' }, position: { x: 100, y: 100 } } as Node,
267
+ ];
268
+
269
+ vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
270
+ vi.mocked(mergeNodePositions).mockReturnValue(mockMergedNodes);
271
+ vi.mocked(overlayWebSocketState).mockReturnValue(mockMergedNodes);
272
+
273
+ await useGraphStore.getState().generateBlackboardViewGraph();
274
+
275
+ // Verify backend API call with blackboard mode
276
+ expect(fetchGraphSnapshot).toHaveBeenCalledWith({
277
+ viewMode: 'blackboard',
278
+ filters: expect.any(Object),
279
+ options: { include_statistics: true },
280
+ });
281
+
282
+ // Verify state updated
283
+ const state = useGraphStore.getState();
284
+ expect(state.nodes).toHaveLength(1);
285
+ expect(state.edges).toHaveLength(1);
286
+ expect(state.viewMode).toBe('blackboard');
287
+ });
74
288
  });
75
289
 
76
- it('should limit events to 100', () => {
77
- for (let i = 0; i < 120; i++) {
78
- const message: Message = {
79
- id: `msg-${i}`,
80
- type: 'Movie',
81
- payload: { index: i },
82
- timestamp: Date.now(),
83
- correlationId: 'corr-1',
84
- producedBy: 'movie',
290
+ describe('updateAgentStatus() - Real-time WebSocket updates', () => {
291
+ it('should update agent status immediately (FAST path)', () => {
292
+ // Setup initial state with agent node
293
+ useGraphStore.setState({
294
+ nodes: [
295
+ { id: 'agent1', type: 'agent', data: { name: 'pizza_master', status: 'idle' }, position: { x: 0, y: 0 } } as Node,
296
+ ],
297
+ });
298
+
299
+ // Update status
300
+ useGraphStore.getState().updateAgentStatus('agent1', 'running');
301
+
302
+ // Verify immediate update
303
+ const state = useGraphStore.getState();
304
+ const node = state.nodes.find((n) => n.id === 'agent1');
305
+ expect(node?.data.status).toBe('running');
306
+
307
+ // Verify status stored in agentStatus map
308
+ expect(state.agentStatus.get('agent1')).toBe('running');
309
+ });
310
+
311
+ it('should only update matching agent node', () => {
312
+ useGraphStore.setState({
313
+ nodes: [
314
+ { id: 'agent1', type: 'agent', data: { status: 'idle' }, position: { x: 0, y: 0 } } as Node,
315
+ { id: 'agent2', type: 'agent', data: { status: 'idle' }, position: { x: 100, y: 0 } } as Node,
316
+ ],
317
+ });
318
+
319
+ useGraphStore.getState().updateAgentStatus('agent1', 'running');
320
+
321
+ const state = useGraphStore.getState();
322
+ expect(state.nodes.find((n) => n.id === 'agent1')?.data.status).toBe('running');
323
+ expect(state.nodes.find((n) => n.id === 'agent2')?.data.status).toBe('idle');
324
+ });
325
+ });
326
+
327
+ describe('updateStreamingTokens() - Real-time token display', () => {
328
+ it('should update streaming tokens and keep last 6 only', () => {
329
+ useGraphStore.setState({
330
+ nodes: [
331
+ { id: 'agent1', type: 'agent', data: { streamingTokens: [] }, position: { x: 0, y: 0 } } as Node,
332
+ ],
333
+ });
334
+
335
+ // Send 8 tokens
336
+ const tokens = ['token1', 'token2', 'token3', 'token4', 'token5', 'token6', 'token7', 'token8'];
337
+ useGraphStore.getState().updateStreamingTokens('agent1', tokens);
338
+
339
+ const state = useGraphStore.getState();
340
+ const node = state.nodes.find((n) => n.id === 'agent1');
341
+
342
+ // Verify only last 6 tokens kept
343
+ expect(node?.data.streamingTokens).toEqual(['token3', 'token4', 'token5', 'token6', 'token7', 'token8']);
344
+ expect(node?.data.streamingTokens).toHaveLength(6);
345
+ });
346
+
347
+ it('should handle tokens less than 6', () => {
348
+ useGraphStore.setState({
349
+ nodes: [
350
+ { id: 'agent1', type: 'agent', data: { streamingTokens: [] }, position: { x: 0, y: 0 } } as Node,
351
+ ],
352
+ });
353
+
354
+ const tokens = ['token1', 'token2', 'token3'];
355
+ useGraphStore.getState().updateStreamingTokens('agent1', tokens);
356
+
357
+ const state = useGraphStore.getState();
358
+ const node = state.nodes.find((n) => n.id === 'agent1');
359
+ expect(node?.data.streamingTokens).toEqual(['token1', 'token2', 'token3']);
360
+ });
361
+
362
+ it('should store tokens in streamingTokens map', () => {
363
+ useGraphStore.setState({
364
+ nodes: [
365
+ { id: 'agent1', type: 'agent', data: { streamingTokens: [] }, position: { x: 0, y: 0 } } as Node,
366
+ ],
367
+ });
368
+
369
+ const tokens = ['token1', 'token2'];
370
+ useGraphStore.getState().updateStreamingTokens('agent1', tokens);
371
+
372
+ const state = useGraphStore.getState();
373
+ expect(state.streamingTokens.get('agent1')).toEqual(['token1', 'token2']);
374
+ });
375
+ });
376
+
377
+ describe('Position persistence integration', () => {
378
+ it('should merge saved positions with backend nodes', async () => {
379
+ const mockBackendNodes: GraphNode[] = [
380
+ { id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 0, y: 0 }, hidden: false },
381
+ ];
382
+
383
+ // Mock saved positions (200, 300) should be used by mergeNodePositions
384
+ const mockMergedNodes: Node[] = [
385
+ { id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 200, y: 300 } } as Node,
386
+ ];
387
+
388
+ const mockSnapshot: GraphSnapshot = {
389
+ nodes: mockBackendNodes,
390
+ edges: [],
391
+ statistics: null,
392
+ viewMode: 'agent',
393
+ filters: {
394
+ correlation_id: null,
395
+ time_range: { preset: 'last10min' },
396
+ artifactTypes: [],
397
+ producers: [],
398
+ tags: [],
399
+ visibility: [],
400
+ },
401
+ generatedAt: '2025-10-11T00:00:00Z',
402
+ totalArtifacts: 0,
403
+ truncated: false,
404
+ };
405
+
406
+ vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
407
+ vi.mocked(mergeNodePositions).mockReturnValue(mockMergedNodes);
408
+ vi.mocked(overlayWebSocketState).mockReturnValue(mockMergedNodes);
409
+
410
+ await useGraphStore.getState().generateAgentViewGraph();
411
+
412
+ // Verify mergeNodePositions was called with correct arguments
413
+ expect(mergeNodePositions).toHaveBeenCalledWith(
414
+ mockBackendNodes,
415
+ expect.any(Map), // savedPositions from IndexedDB
416
+ [] // currentNodes (empty on first load)
417
+ );
418
+
419
+ // Verify merged positions applied
420
+ const state = useGraphStore.getState();
421
+ expect(state.nodes[0]!.position).toEqual({ x: 200, y: 300 });
422
+ });
423
+
424
+ it('should overlay WebSocket state on merged nodes', async () => {
425
+ const mockBackendNodes: GraphNode[] = [
426
+ { id: 'agent1', type: 'agent', data: { name: 'agent1', status: 'idle' }, position: { x: 0, y: 0 }, hidden: false },
427
+ ];
428
+
429
+ const mockMergedNodes: Node[] = [
430
+ { id: 'agent1', type: 'agent', data: { name: 'agent1', status: 'idle' }, position: { x: 100, y: 100 } } as Node,
431
+ ];
432
+
433
+ const mockOverlayedNodes: Node[] = [
434
+ { id: 'agent1', type: 'agent', data: { name: 'agent1', status: 'running', streamingTokens: ['token1'] }, position: { x: 100, y: 100 } } as Node,
435
+ ];
436
+
437
+ const mockSnapshot: GraphSnapshot = {
438
+ nodes: mockBackendNodes,
439
+ edges: [],
440
+ statistics: null,
441
+ viewMode: 'agent',
442
+ filters: {
443
+ correlation_id: null,
444
+ time_range: { preset: 'last10min' },
445
+ artifactTypes: [],
446
+ producers: [],
447
+ tags: [],
448
+ visibility: [],
449
+ },
450
+ generatedAt: '2025-10-11T00:00:00Z',
451
+ totalArtifacts: 0,
452
+ truncated: false,
85
453
  };
86
- useGraphStore.getState().addMessage(message);
87
- }
88
454
 
89
- const events = useGraphStore.getState().events;
90
- expect(events.length).toBe(100);
455
+ // Setup WebSocket state
456
+ useGraphStore.setState({
457
+ agentStatus: new Map([['agent1', 'running']]),
458
+ streamingTokens: new Map([['agent1', ['token1']]]),
459
+ });
460
+
461
+ vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
462
+ vi.mocked(mergeNodePositions).mockReturnValue(mockMergedNodes);
463
+ vi.mocked(overlayWebSocketState).mockReturnValue(mockOverlayedNodes);
464
+
465
+ await useGraphStore.getState().generateAgentViewGraph();
466
+
467
+ // Verify overlayWebSocketState called with WebSocket state
468
+ expect(overlayWebSocketState).toHaveBeenCalledWith(
469
+ mockMergedNodes,
470
+ expect.any(Map), // agentStatus
471
+ expect.any(Map) // streamingTokens
472
+ );
473
+ });
91
474
  });
92
475
 
93
- it('should batch update agents and messages', () => {
94
- const agents: Agent[] = [
95
- {
96
- id: 'agent-1',
97
- name: 'agent-1',
98
- status: 'idle',
99
- subscriptions: [],
100
- lastActive: Date.now(),
101
- sentCount: 0,
102
- recvCount: 0,
103
- },
104
- {
105
- id: 'agent-2',
106
- name: 'agent-2',
107
- status: 'running',
108
- subscriptions: [],
109
- lastActive: Date.now(),
110
- sentCount: 0,
111
- recvCount: 0,
112
- },
113
- ];
114
-
115
- const messages: Message[] = [
116
- {
117
- id: 'msg-1',
118
- type: 'Movie',
119
- payload: {},
120
- timestamp: Date.now(),
121
- correlationId: 'corr-1',
122
- producedBy: 'agent-1',
123
- },
124
- ];
476
+ describe('Statistics from backend snapshot', () => {
477
+ it('should store statistics from backend', async () => {
478
+ const mockStatistics = {
479
+ producedByAgent: {
480
+ pizza_master: { total: 50, byType: { Pizza: 50 } },
481
+ },
482
+ consumedByAgent: {
483
+ waiter: { total: 50, byType: { Pizza: 50 } },
484
+ },
485
+ artifactSummary: {
486
+ total: 100,
487
+ by_type: { Pizza: 100 },
488
+ by_producer: { pizza_master: 100 },
489
+ by_visibility: { public: 100 },
490
+ tag_counts: {},
491
+ earliest_created_at: '2025-10-11T00:00:00Z',
492
+ latest_created_at: '2025-10-11T01:00:00Z',
493
+ },
494
+ };
495
+
496
+ const mockSnapshot: GraphSnapshot = {
497
+ nodes: [],
498
+ edges: [],
499
+ statistics: mockStatistics,
500
+ viewMode: 'agent',
501
+ filters: {
502
+ correlation_id: null,
503
+ time_range: { preset: 'last10min' },
504
+ artifactTypes: [],
505
+ producers: [],
506
+ tags: [],
507
+ visibility: [],
508
+ },
509
+ generatedAt: '2025-10-11T00:00:00Z',
510
+ totalArtifacts: 100,
511
+ truncated: false,
512
+ };
125
513
 
126
- useGraphStore.getState().batchUpdate({ agents, messages });
514
+ vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
515
+ vi.mocked(mergeNodePositions).mockReturnValue([]);
516
+ vi.mocked(overlayWebSocketState).mockReturnValue([]);
127
517
 
128
- expect(useGraphStore.getState().agents.size).toBe(2);
129
- expect(useGraphStore.getState().messages.size).toBe(1);
130
- expect(useGraphStore.getState().events.length).toBe(1);
518
+ await useGraphStore.getState().generateAgentViewGraph();
519
+
520
+ const state = useGraphStore.getState();
521
+ expect(state.statistics).toEqual(mockStatistics);
522
+ });
131
523
  });
132
524
 
133
- it('should generate agent view graph', () => {
134
- const agents: Agent[] = [
135
- {
136
- id: 'movie',
137
- name: 'movie',
138
- status: 'idle',
139
- subscriptions: [],
140
- lastActive: Date.now(),
141
- sentCount: 2,
142
- recvCount: 0,
143
- position: { x: 0, y: 0 },
144
- },
145
- {
146
- id: 'tagline',
147
- name: 'tagline',
148
- status: 'idle',
149
- subscriptions: ['Movie'],
150
- lastActive: Date.now(),
151
- sentCount: 0,
152
- recvCount: 2,
153
- position: { x: 200, y: 0 },
154
- },
155
- ];
156
-
157
- const messages: Message[] = [
158
- {
159
- id: 'msg-1',
160
- type: 'Movie',
525
+ describe('UI state management', () => {
526
+ it('should add events to event log', () => {
527
+ const event = {
528
+ id: 'msg1',
529
+ type: 'Pizza',
161
530
  payload: {},
162
- timestamp: Date.now(),
531
+ producedBy: 'pizza_master',
163
532
  correlationId: 'corr-1',
164
- producedBy: 'movie',
165
- },
166
- {
167
- id: 'msg-2',
168
- type: 'Movie',
169
- payload: {},
170
533
  timestamp: Date.now(),
171
- correlationId: 'corr-1',
172
- producedBy: 'movie',
173
- },
174
- ];
534
+ };
175
535
 
176
- useGraphStore.getState().batchUpdate({ agents, messages });
177
- // Phase 11 fix: Record consumption to populate consumed_by field
178
- useGraphStore.getState().recordConsumption(['msg-1', 'msg-2'], 'tagline');
179
- useGraphStore.getState().generateAgentViewGraph();
536
+ useGraphStore.getState().addEvent(event as any);
180
537
 
181
- const nodes = useGraphStore.getState().nodes;
182
- const edges = useGraphStore.getState().edges;
538
+ const state = useGraphStore.getState();
539
+ expect(state.events).toHaveLength(1);
540
+ expect(state.events[0]).toEqual(event);
541
+ });
183
542
 
184
- expect(nodes.length).toBe(2);
185
- expect(nodes[0]?.type).toBe('agent');
186
- expect(edges.length).toBeGreaterThan(0);
187
- });
543
+ it('should limit events to 100 entries', () => {
544
+ // Add 150 events
545
+ for (let i = 0; i < 150; i++) {
546
+ useGraphStore.getState().addEvent({
547
+ id: `msg${i}`,
548
+ type: 'Pizza',
549
+ payload: {},
550
+ producedBy: 'pizza_master',
551
+ correlationId: 'corr-1',
552
+ timestamp: Date.now() + i,
553
+ } as any);
554
+ }
555
+
556
+ const state = useGraphStore.getState();
557
+ expect(state.events).toHaveLength(100);
558
+ // Most recent should be first
559
+ expect(state.events[0]!.id).toBe('msg149');
560
+ });
561
+
562
+ it('should update view mode', () => {
563
+ useGraphStore.getState().setViewMode('blackboard');
564
+ expect(useGraphStore.getState().viewMode).toBe('blackboard');
188
565
 
189
- it('should hydrate consumptions from message payload', () => {
190
- const message: Message = {
191
- id: 'msg-embed',
192
- type: 'Recipe',
193
- payload: {},
194
- timestamp: Date.now(),
195
- correlationId: 'corr-embed',
196
- producedBy: 'chef',
197
- consumedBy: ['critic'],
198
- };
199
-
200
- useGraphStore.getState().batchUpdate({ messages: [message] });
201
-
202
- const state = useGraphStore.getState();
203
- expect(state.consumptions.get('msg-embed')).toEqual(['critic']);
566
+ useGraphStore.getState().setViewMode('agent');
567
+ expect(useGraphStore.getState().viewMode).toBe('agent');
568
+ });
204
569
  });
205
570
  });