flock-core 0.5.0b70__py3-none-any.whl → 0.5.0b75__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/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 +28 -9
- 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 +24 -6
- 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.0b70.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b70.dist-info → flock_core-0.5.0b75.dist-info}/RECORD +56 -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.0b70.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b70.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b70.dist-info → flock_core-0.5.0b75.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,205 +1,570 @@
|
|
|
1
|
-
|
|
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 {
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
514
|
+
vi.mocked(fetchGraphSnapshot).mockResolvedValue(mockSnapshot);
|
|
515
|
+
vi.mocked(mergeNodePositions).mockReturnValue([]);
|
|
516
|
+
vi.mocked(overlayWebSocketState).mockReturnValue([]);
|
|
127
517
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
518
|
+
await useGraphStore.getState().generateAgentViewGraph();
|
|
519
|
+
|
|
520
|
+
const state = useGraphStore.getState();
|
|
521
|
+
expect(state.statistics).toEqual(mockStatistics);
|
|
522
|
+
});
|
|
131
523
|
});
|
|
132
524
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
id: '
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
producedBy: 'movie',
|
|
173
|
-
},
|
|
174
|
-
];
|
|
534
|
+
};
|
|
175
535
|
|
|
176
|
-
|
|
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
|
-
|
|
182
|
-
|
|
538
|
+
const state = useGraphStore.getState();
|
|
539
|
+
expect(state.events).toHaveLength(1);
|
|
540
|
+
expect(state.events[0]).toEqual(event);
|
|
541
|
+
});
|
|
183
542
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
});
|