flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b52__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 (117) hide show
  1. flock/dashboard/launcher.py +1 -1
  2. flock/frontend/README.md +678 -0
  3. flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
  4. flock/frontend/index.html +12 -0
  5. flock/frontend/package-lock.json +4347 -0
  6. flock/frontend/package.json +48 -0
  7. flock/frontend/src/App.tsx +79 -0
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
  10. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
  11. flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
  12. flock/frontend/src/components/common/BuildInfo.tsx +39 -0
  13. flock/frontend/src/components/common/EmptyState.module.css +115 -0
  14. flock/frontend/src/components/common/EmptyState.tsx +128 -0
  15. flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
  16. flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
  17. flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
  18. flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
  19. flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
  20. flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
  21. flock/frontend/src/components/controls/PublishControl.css +547 -0
  22. flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
  23. flock/frontend/src/components/controls/PublishControl.tsx +432 -0
  24. flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
  25. flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
  26. flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
  27. flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
  28. flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
  29. flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
  30. flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
  31. flock/frontend/src/components/details/tabs.test.tsx +1015 -0
  32. flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
  33. flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
  34. flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
  35. flock/frontend/src/components/filters/FilterBar.module.css +29 -0
  36. flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
  37. flock/frontend/src/components/filters/FilterBar.tsx +33 -0
  38. flock/frontend/src/components/filters/FilterPills.module.css +79 -0
  39. flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
  40. flock/frontend/src/components/filters/FilterPills.tsx +67 -0
  41. flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
  42. flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
  43. flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
  44. flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
  45. flock/frontend/src/components/graph/AgentNode.tsx +322 -0
  46. flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
  47. flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
  48. flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
  49. flock/frontend/src/components/graph/MessageNode.tsx +116 -0
  50. flock/frontend/src/components/graph/MiniMap.tsx +47 -0
  51. flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
  52. flock/frontend/src/components/layout/DashboardLayout.css +407 -0
  53. flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
  54. flock/frontend/src/components/layout/Header.module.css +88 -0
  55. flock/frontend/src/components/layout/Header.tsx +52 -0
  56. flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
  57. flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
  58. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
  59. flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
  60. flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
  61. flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
  62. flock/frontend/src/components/modules/registerModules.ts +20 -0
  63. flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
  64. flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
  65. flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
  66. flock/frontend/src/components/settings/SettingsPanel.css +327 -0
  67. flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
  68. flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
  69. flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
  70. flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
  71. flock/frontend/src/hooks/useModulePersistence.ts +154 -0
  72. flock/frontend/src/hooks/useModules.ts +139 -0
  73. flock/frontend/src/hooks/usePersistence.ts +139 -0
  74. flock/frontend/src/main.tsx +13 -0
  75. flock/frontend/src/services/api.ts +213 -0
  76. flock/frontend/src/services/indexeddb.test.ts +793 -0
  77. flock/frontend/src/services/indexeddb.ts +794 -0
  78. flock/frontend/src/services/layout.test.ts +437 -0
  79. flock/frontend/src/services/layout.ts +146 -0
  80. flock/frontend/src/services/themeApplicator.ts +140 -0
  81. flock/frontend/src/services/themeService.ts +77 -0
  82. flock/frontend/src/services/websocket.test.ts +595 -0
  83. flock/frontend/src/services/websocket.ts +685 -0
  84. flock/frontend/src/store/filterStore.test.ts +242 -0
  85. flock/frontend/src/store/filterStore.ts +103 -0
  86. flock/frontend/src/store/graphStore.test.ts +186 -0
  87. flock/frontend/src/store/graphStore.ts +414 -0
  88. flock/frontend/src/store/moduleStore.test.ts +253 -0
  89. flock/frontend/src/store/moduleStore.ts +57 -0
  90. flock/frontend/src/store/settingsStore.ts +188 -0
  91. flock/frontend/src/store/streamStore.ts +68 -0
  92. flock/frontend/src/store/uiStore.test.ts +54 -0
  93. flock/frontend/src/store/uiStore.ts +110 -0
  94. flock/frontend/src/store/wsStore.ts +34 -0
  95. flock/frontend/src/styles/index.css +15 -0
  96. flock/frontend/src/styles/scrollbar.css +47 -0
  97. flock/frontend/src/styles/variables.css +488 -0
  98. flock/frontend/src/test/setup.ts +1 -0
  99. flock/frontend/src/types/filters.ts +14 -0
  100. flock/frontend/src/types/graph.ts +55 -0
  101. flock/frontend/src/types/modules.ts +7 -0
  102. flock/frontend/src/types/theme.ts +55 -0
  103. flock/frontend/src/utils/mockData.ts +85 -0
  104. flock/frontend/src/utils/performance.ts +16 -0
  105. flock/frontend/src/utils/transforms.test.ts +860 -0
  106. flock/frontend/src/utils/transforms.ts +323 -0
  107. flock/frontend/src/vite-env.d.ts +17 -0
  108. flock/frontend/tsconfig.json +27 -0
  109. flock/frontend/tsconfig.node.json +11 -0
  110. flock/frontend/vite.config.ts +25 -0
  111. flock/frontend/vitest.config.ts +11 -0
  112. flock/helper/cli_helper.py +1 -1
  113. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/METADATA +1 -1
  114. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/RECORD +117 -7
  115. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/WHEEL +0 -0
  116. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/entry_points.txt +0 -0
  117. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,414 @@
1
+ import { create } from 'zustand';
2
+ import { devtools } from 'zustand/middleware';
3
+ import { Node, Edge } from '@xyflow/react';
4
+ import { Agent, Message, AgentNodeData, MessageNodeData } from '../types/graph';
5
+ import { deriveAgentViewEdges, deriveBlackboardViewEdges, Artifact, Run, DashboardState } from '../utils/transforms';
6
+ import { useFilterStore } from './filterStore';
7
+
8
+ interface GraphState {
9
+ // Core data
10
+ agents: Map<string, Agent>;
11
+ messages: Map<string, Message>;
12
+ events: Message[];
13
+ runs: Map<string, Run>;
14
+
15
+ // Phase 11 Bug Fix: Track actual consumption (artifact_id -> consumer_ids[])
16
+ // Updated by agent_activated events to reflect filtering and actual consumption
17
+ consumptions: Map<string, string[]>;
18
+
19
+ // Message node positions (message_id -> {x, y})
20
+ // Messages don't have position in their data model, so we track it separately
21
+ messagePositions: Map<string, { x: number; y: number }>;
22
+
23
+ // Graph representation
24
+ nodes: Node[];
25
+ edges: Edge[];
26
+
27
+ // Actions
28
+ addAgent: (agent: Agent) => void;
29
+ updateAgent: (id: string, updates: Partial<Agent>) => void;
30
+ removeAgent: (id: string) => void;
31
+
32
+ addMessage: (message: Message) => void;
33
+ updateMessage: (id: string, updates: Partial<Message>) => void;
34
+ addRun: (run: Run) => void;
35
+
36
+ // Phase 11 Bug Fix: Track actual consumption from agent_activated events
37
+ recordConsumption: (artifactIds: string[], consumerId: string) => void;
38
+
39
+ // Transform streaming message to final message (changes ID)
40
+ finalizeStreamingMessage: (oldId: string, newMessage: Message) => void;
41
+
42
+ updateNodePosition: (nodeId: string, position: { x: number; y: number }) => void;
43
+
44
+ // Mode-specific graph generation
45
+ generateAgentViewGraph: () => void;
46
+ generateBlackboardViewGraph: () => void;
47
+
48
+ // Filter application
49
+ applyFilters: () => void;
50
+
51
+ // Bulk updates
52
+ batchUpdate: (update: { agents?: Agent[]; messages?: Message[]; runs?: Run[] }) => void;
53
+ }
54
+
55
+ // Helper function to convert Message to Artifact
56
+ function messageToArtifact(message: Message, consumptions: Map<string, string[]>): Artifact {
57
+ // BUG FIX: Use ACTUAL consumption data from consumptions Map, not inferred from subscriptions!
58
+ // This ensures edges reflect what actually happened, not what "should" happen based on current subscriptions.
59
+ const actualConsumers = consumptions.get(message.id) || [];
60
+
61
+ return {
62
+ artifact_id: message.id,
63
+ artifact_type: message.type,
64
+ produced_by: message.producedBy,
65
+ consumed_by: actualConsumers, // Use actual consumption data
66
+ published_at: new Date(message.timestamp).toISOString(),
67
+ payload: message.payload,
68
+ correlation_id: message.correlationId,
69
+ };
70
+ }
71
+
72
+ // Helper function to convert store state to DashboardState
73
+ function toDashboardState(
74
+ messages: Map<string, Message>,
75
+ runs: Map<string, Run>,
76
+ consumptions: Map<string, string[]>
77
+ ): DashboardState {
78
+ const artifacts = new Map<string, Artifact>();
79
+
80
+ messages.forEach((message) => {
81
+ artifacts.set(message.id, messageToArtifact(message, consumptions));
82
+ });
83
+
84
+ return {
85
+ artifacts,
86
+ runs,
87
+ consumptions, // Phase 11: Pass actual consumption data for filtered count calculation
88
+ };
89
+ }
90
+
91
+ export const useGraphStore = create<GraphState>()(
92
+ devtools(
93
+ (set, get) => ({
94
+ agents: new Map(),
95
+ messages: new Map(),
96
+ events: [],
97
+ runs: new Map(),
98
+ consumptions: new Map(), // Phase 11: Track actual artifact consumption
99
+ messagePositions: new Map(), // Track message node positions
100
+ nodes: [],
101
+ edges: [],
102
+
103
+ addAgent: (agent) =>
104
+ set((state) => {
105
+ const agents = new Map(state.agents);
106
+ agents.set(agent.id, agent);
107
+ return { agents };
108
+ }),
109
+
110
+ updateAgent: (id, updates) =>
111
+ set((state) => {
112
+ const agents = new Map(state.agents);
113
+ const agent = agents.get(id);
114
+ if (agent) {
115
+ agents.set(id, { ...agent, ...updates });
116
+ }
117
+ return { agents };
118
+ }),
119
+
120
+ removeAgent: (id) =>
121
+ set((state) => {
122
+ const agents = new Map(state.agents);
123
+ agents.delete(id);
124
+ return { agents };
125
+ }),
126
+
127
+ addMessage: (message) =>
128
+ set((state) => {
129
+ const messages = new Map(state.messages);
130
+ messages.set(message.id, message);
131
+
132
+ // Only add to events if this is a NEW message (not already in the array)
133
+ // This prevents streaming token updates from flooding the Event Log
134
+ const isDuplicate = state.events.some(e => e.id === message.id);
135
+ const events = isDuplicate
136
+ ? state.events // Skip if already in events array
137
+ : [message, ...state.events].slice(0, 100); // Add new message
138
+
139
+ return { messages, events };
140
+ }),
141
+
142
+ updateMessage: (id, updates) =>
143
+ set((state) => {
144
+ const messages = new Map(state.messages);
145
+ const message = messages.get(id);
146
+ if (message) {
147
+ messages.set(id, { ...message, ...updates });
148
+ }
149
+ // Note: updateMessage does NOT touch the events array
150
+ // This allows streaming updates without flooding the Event Log
151
+ return { messages };
152
+ }),
153
+
154
+ addRun: (run) =>
155
+ set((state) => {
156
+ const runs = new Map(state.runs);
157
+ runs.set(run.run_id, run);
158
+ return { runs };
159
+ }),
160
+
161
+ // Phase 11 Bug Fix: Record actual consumption from agent_activated events
162
+ recordConsumption: (artifactIds, consumerId) =>
163
+ set((state) => {
164
+ const consumptions = new Map(state.consumptions);
165
+ artifactIds.forEach((artifactId) => {
166
+ const existing = consumptions.get(artifactId) || [];
167
+ if (!existing.includes(consumerId)) {
168
+ consumptions.set(artifactId, [...existing, consumerId]);
169
+ }
170
+ });
171
+ return { consumptions };
172
+ }),
173
+
174
+ finalizeStreamingMessage: (oldId, newMessage) =>
175
+ set((state) => {
176
+ // Remove old streaming message, add final message with new ID
177
+ const messages = new Map(state.messages);
178
+ messages.delete(oldId);
179
+ messages.set(newMessage.id, newMessage);
180
+
181
+ // Transfer position from old ID to new ID
182
+ const messagePositions = new Map(state.messagePositions);
183
+ const oldPos = messagePositions.get(oldId);
184
+ if (oldPos) {
185
+ messagePositions.delete(oldId);
186
+ messagePositions.set(newMessage.id, oldPos);
187
+ }
188
+
189
+ // Update events array: replace streaming ID with final message ID
190
+ const events = state.events.map(e =>
191
+ e.id === oldId ? newMessage : e
192
+ );
193
+
194
+ return { messages, messagePositions, events };
195
+ }),
196
+
197
+ updateNodePosition: (nodeId, position) =>
198
+ set((state) => {
199
+ const agents = new Map(state.agents);
200
+ const agent = agents.get(nodeId);
201
+ if (agent) {
202
+ // Update agent position
203
+ agents.set(nodeId, { ...agent, position });
204
+ return { agents };
205
+ } else {
206
+ // Must be a message node - update message position
207
+ const messagePositions = new Map(state.messagePositions);
208
+ messagePositions.set(nodeId, position);
209
+ return { messagePositions };
210
+ }
211
+ }),
212
+
213
+ generateAgentViewGraph: () => {
214
+ const { agents, messages, runs, consumptions, nodes: currentNodes } = get();
215
+
216
+ // Create a map of current node positions to preserve them during regeneration
217
+ const currentPositions = new Map<string, { x: number; y: number }>();
218
+ currentNodes.forEach(node => {
219
+ currentPositions.set(node.id, node.position);
220
+ });
221
+
222
+ const nodes: Node<AgentNodeData>[] = [];
223
+
224
+ // Create nodes from agents
225
+ agents.forEach((agent) => {
226
+ // Preserve position priority: saved position > current React Flow position > default
227
+ const position = agent.position
228
+ || currentPositions.get(agent.id)
229
+ || { x: 400 + Math.random() * 200, y: 300 + Math.random() * 200 };
230
+
231
+ nodes.push({
232
+ id: agent.id,
233
+ type: 'agent',
234
+ position,
235
+ data: {
236
+ name: agent.name,
237
+ status: agent.status,
238
+ subscriptions: agent.subscriptions,
239
+ outputTypes: agent.outputTypes,
240
+ sentCount: agent.sentCount,
241
+ recvCount: agent.recvCount,
242
+ receivedByType: agent.receivedByType,
243
+ sentByType: agent.sentByType,
244
+ streamingTokens: agent.streamingTokens,
245
+ },
246
+ });
247
+ });
248
+
249
+ // Derive edges using transform algorithm
250
+ const dashboardState = toDashboardState(messages, runs, consumptions);
251
+ const edges = deriveAgentViewEdges(dashboardState);
252
+
253
+ set({ nodes, edges });
254
+ },
255
+
256
+ generateBlackboardViewGraph: () => {
257
+ const { messages, runs, consumptions, messagePositions, nodes: currentNodes } = get();
258
+
259
+ // Create a map of current node positions to preserve them during regeneration
260
+ const currentPositions = new Map<string, { x: number; y: number }>();
261
+ currentNodes.forEach(node => {
262
+ currentPositions.set(node.id, node.position);
263
+ });
264
+
265
+ const nodes: Node<MessageNodeData>[] = [];
266
+
267
+ // Create nodes from messages
268
+ messages.forEach((message) => {
269
+ const payloadStr = JSON.stringify(message.payload);
270
+
271
+ // BUG FIX: Use ACTUAL consumption data from consumptions Map, not inferred from subscriptions!
272
+ const consumedBy = consumptions.get(message.id) || [];
273
+
274
+ // Preserve position priority: saved position > current React Flow position > default
275
+ const position = messagePositions.get(message.id)
276
+ || currentPositions.get(message.id)
277
+ || { x: 400 + Math.random() * 200, y: 300 + Math.random() * 200 };
278
+
279
+ nodes.push({
280
+ id: message.id,
281
+ type: 'message',
282
+ position,
283
+ data: {
284
+ artifactType: message.type,
285
+ payloadPreview: payloadStr.slice(0, 100),
286
+ payload: message.payload, // Full payload for display
287
+ producedBy: message.producedBy,
288
+ consumedBy, // Use actual consumption data
289
+ timestamp: message.timestamp,
290
+ isStreaming: message.isStreaming || false,
291
+ streamingText: message.streamingText || '',
292
+ },
293
+ });
294
+ });
295
+
296
+ // Derive edges using transform algorithm
297
+ const dashboardState = toDashboardState(messages, runs, consumptions);
298
+ const edges = deriveBlackboardViewEdges(dashboardState);
299
+
300
+ set({ nodes, edges });
301
+ },
302
+
303
+ batchUpdate: (update) =>
304
+ set((state) => {
305
+ const newState: Partial<GraphState> = {};
306
+
307
+ if (update.agents) {
308
+ const agents = new Map(state.agents);
309
+ update.agents.forEach((a) => agents.set(a.id, a));
310
+ newState.agents = agents;
311
+ }
312
+
313
+ if (update.messages) {
314
+ const messages = new Map(state.messages);
315
+ update.messages.forEach((m) => messages.set(m.id, m));
316
+ newState.messages = messages;
317
+ newState.events = [...update.messages, ...state.events].slice(0, 100);
318
+ }
319
+
320
+ if (update.runs) {
321
+ const runs = new Map(state.runs);
322
+ update.runs.forEach((r) => runs.set(r.run_id, r));
323
+ newState.runs = runs;
324
+ }
325
+
326
+ return newState;
327
+ }),
328
+
329
+ applyFilters: () => {
330
+ const { nodes, edges, messages } = get();
331
+ const { correlationId, timeRange } = useFilterStore.getState();
332
+
333
+ // Helper to calculate time range boundaries
334
+ const getTimeRangeBoundaries = (): { start: number; end: number } => {
335
+ const now = Date.now();
336
+ if (timeRange.preset === 'last5min') {
337
+ return { start: now - 5 * 60 * 1000, end: now };
338
+ } else if (timeRange.preset === 'last10min') {
339
+ return { start: now - 10 * 60 * 1000, end: now };
340
+ } else if (timeRange.preset === 'last1hour') {
341
+ return { start: now - 60 * 60 * 1000, end: now };
342
+ } else if (timeRange.preset === 'custom' && timeRange.start && timeRange.end) {
343
+ return { start: timeRange.start, end: timeRange.end };
344
+ }
345
+ return { start: now - 10 * 60 * 1000, end: now };
346
+ };
347
+
348
+ const { start: timeStart, end: timeEnd } = getTimeRangeBoundaries();
349
+
350
+ // Filter messages based on correlation ID and time range
351
+ const visibleMessageIds = new Set<string>();
352
+ messages.forEach((message) => {
353
+ let visible = true;
354
+
355
+ // Apply correlation ID filter (selective)
356
+ if (correlationId && message.correlationId !== correlationId) {
357
+ visible = false;
358
+ }
359
+
360
+ // Apply time range filter (in-memory)
361
+ if (visible && (message.timestamp < timeStart || message.timestamp > timeEnd)) {
362
+ visible = false;
363
+ }
364
+
365
+ if (visible) {
366
+ visibleMessageIds.add(message.id);
367
+ }
368
+ });
369
+
370
+ // Update nodes visibility
371
+ const updatedNodes = nodes.map((node) => {
372
+ if (node.type === 'message') {
373
+ // For message nodes, check if message is visible
374
+ return {
375
+ ...node,
376
+ hidden: !visibleMessageIds.has(node.id),
377
+ };
378
+ } else if (node.type === 'agent') {
379
+ // For agent nodes, show if any visible messages involve this agent
380
+ let hasVisibleMessages = false;
381
+ messages.forEach((message) => {
382
+ if (visibleMessageIds.has(message.id)) {
383
+ if (message.producedBy === node.id) {
384
+ hasVisibleMessages = true;
385
+ }
386
+ }
387
+ });
388
+ return {
389
+ ...node,
390
+ hidden: !hasVisibleMessages,
391
+ };
392
+ }
393
+ return node;
394
+ });
395
+
396
+ // Update edges visibility
397
+ const updatedEdges = edges.map((edge) => {
398
+ // Hide edge if either source or target node is hidden
399
+ const sourceNode = updatedNodes.find((n) => n.id === edge.source);
400
+ const targetNode = updatedNodes.find((n) => n.id === edge.target);
401
+ const hidden = sourceNode?.hidden || targetNode?.hidden || false;
402
+
403
+ return {
404
+ ...edge,
405
+ hidden,
406
+ };
407
+ });
408
+
409
+ set({ nodes: updatedNodes, edges: updatedEdges });
410
+ },
411
+ }),
412
+ { name: 'graphStore' }
413
+ )
414
+ );
@@ -0,0 +1,253 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { useModuleStore } from './moduleStore';
3
+ import type { ModuleInstance } from '../types/modules';
4
+
5
+ describe('moduleStore', () => {
6
+ beforeEach(() => {
7
+ // Reset store before each test
8
+ useModuleStore.setState({
9
+ instances: new Map(),
10
+ });
11
+ });
12
+
13
+ it('should have an empty Map as initial state', () => {
14
+ const instances = useModuleStore.getState().instances;
15
+ expect(instances.size).toBe(0);
16
+ expect(instances instanceof Map).toBe(true);
17
+ });
18
+
19
+ it('should add a new module instance', () => {
20
+ const module: ModuleInstance = {
21
+ id: 'module-1',
22
+ type: 'eventlog',
23
+ position: { x: 100, y: 100 },
24
+ size: { width: 600, height: 400 },
25
+ visible: true,
26
+ };
27
+
28
+ useModuleStore.getState().addModule(module);
29
+
30
+ const instances = useModuleStore.getState().instances;
31
+ expect(instances.size).toBe(1);
32
+ expect(instances.get('module-1')).toEqual(module);
33
+ });
34
+
35
+ it('should add multiple module instances', () => {
36
+ const module1: ModuleInstance = {
37
+ id: 'module-1',
38
+ type: 'eventlog',
39
+ position: { x: 100, y: 100 },
40
+ size: { width: 600, height: 400 },
41
+ visible: true,
42
+ };
43
+
44
+ const module2: ModuleInstance = {
45
+ id: 'module-2',
46
+ type: 'eventlog',
47
+ position: { x: 200, y: 200 },
48
+ size: { width: 600, height: 400 },
49
+ visible: true,
50
+ };
51
+
52
+ useModuleStore.getState().addModule(module1);
53
+ useModuleStore.getState().addModule(module2);
54
+
55
+ const instances = useModuleStore.getState().instances;
56
+ expect(instances.size).toBe(2);
57
+ expect(instances.get('module-1')).toEqual(module1);
58
+ expect(instances.get('module-2')).toEqual(module2);
59
+ });
60
+
61
+ it('should update module position', () => {
62
+ const module: ModuleInstance = {
63
+ id: 'module-1',
64
+ type: 'eventlog',
65
+ position: { x: 100, y: 100 },
66
+ size: { width: 600, height: 400 },
67
+ visible: true,
68
+ };
69
+
70
+ useModuleStore.getState().addModule(module);
71
+ useModuleStore.getState().updateModule('module-1', {
72
+ position: { x: 300, y: 300 },
73
+ });
74
+
75
+ const updated = useModuleStore.getState().instances.get('module-1');
76
+ expect(updated?.position).toEqual({ x: 300, y: 300 });
77
+ expect(updated?.size).toEqual({ width: 600, height: 400 }); // Other properties unchanged
78
+ expect(updated?.visible).toBe(true);
79
+ });
80
+
81
+ it('should update module size', () => {
82
+ const module: ModuleInstance = {
83
+ id: 'module-1',
84
+ type: 'eventlog',
85
+ position: { x: 100, y: 100 },
86
+ size: { width: 600, height: 400 },
87
+ visible: true,
88
+ };
89
+
90
+ useModuleStore.getState().addModule(module);
91
+ useModuleStore.getState().updateModule('module-1', {
92
+ size: { width: 800, height: 600 },
93
+ });
94
+
95
+ const updated = useModuleStore.getState().instances.get('module-1');
96
+ expect(updated?.size).toEqual({ width: 800, height: 600 });
97
+ expect(updated?.position).toEqual({ x: 100, y: 100 }); // Other properties unchanged
98
+ });
99
+
100
+ it('should update multiple module properties at once', () => {
101
+ const module: ModuleInstance = {
102
+ id: 'module-1',
103
+ type: 'eventlog',
104
+ position: { x: 100, y: 100 },
105
+ size: { width: 600, height: 400 },
106
+ visible: true,
107
+ };
108
+
109
+ useModuleStore.getState().addModule(module);
110
+ useModuleStore.getState().updateModule('module-1', {
111
+ position: { x: 300, y: 300 },
112
+ size: { width: 800, height: 600 },
113
+ visible: false,
114
+ });
115
+
116
+ const updated = useModuleStore.getState().instances.get('module-1');
117
+ expect(updated?.position).toEqual({ x: 300, y: 300 });
118
+ expect(updated?.size).toEqual({ width: 800, height: 600 });
119
+ expect(updated?.visible).toBe(false);
120
+ });
121
+
122
+ it('should toggle module visibility', () => {
123
+ const module: ModuleInstance = {
124
+ id: 'module-1',
125
+ type: 'eventlog',
126
+ position: { x: 100, y: 100 },
127
+ size: { width: 600, height: 400 },
128
+ visible: true,
129
+ };
130
+
131
+ useModuleStore.getState().addModule(module);
132
+
133
+ // Toggle visibility to false
134
+ useModuleStore.getState().toggleVisibility('module-1');
135
+ expect(useModuleStore.getState().instances.get('module-1')?.visible).toBe(false);
136
+
137
+ // Toggle visibility back to true
138
+ useModuleStore.getState().toggleVisibility('module-1');
139
+ expect(useModuleStore.getState().instances.get('module-1')?.visible).toBe(true);
140
+ });
141
+
142
+ it('should remove a module instance', () => {
143
+ const module1: ModuleInstance = {
144
+ id: 'module-1',
145
+ type: 'eventlog',
146
+ position: { x: 100, y: 100 },
147
+ size: { width: 600, height: 400 },
148
+ visible: true,
149
+ };
150
+
151
+ const module2: ModuleInstance = {
152
+ id: 'module-2',
153
+ type: 'eventlog',
154
+ position: { x: 200, y: 200 },
155
+ size: { width: 600, height: 400 },
156
+ visible: true,
157
+ };
158
+
159
+ useModuleStore.getState().addModule(module1);
160
+ useModuleStore.getState().addModule(module2);
161
+
162
+ expect(useModuleStore.getState().instances.size).toBe(2);
163
+
164
+ useModuleStore.getState().removeModule('module-1');
165
+
166
+ const instances = useModuleStore.getState().instances;
167
+ expect(instances.size).toBe(1);
168
+ expect(instances.get('module-1')).toBeUndefined();
169
+ expect(instances.get('module-2')).toEqual(module2);
170
+ });
171
+
172
+ it('should get all module instances', () => {
173
+ const module1: ModuleInstance = {
174
+ id: 'module-1',
175
+ type: 'eventlog',
176
+ position: { x: 100, y: 100 },
177
+ size: { width: 600, height: 400 },
178
+ visible: true,
179
+ };
180
+
181
+ const module2: ModuleInstance = {
182
+ id: 'module-2',
183
+ type: 'eventlog',
184
+ position: { x: 200, y: 200 },
185
+ size: { width: 600, height: 400 },
186
+ visible: false,
187
+ };
188
+
189
+ useModuleStore.getState().addModule(module1);
190
+ useModuleStore.getState().addModule(module2);
191
+
192
+ const instances = useModuleStore.getState().instances;
193
+ const allInstances = Array.from(instances.values());
194
+
195
+ expect(allInstances.length).toBe(2);
196
+ expect(allInstances).toContainEqual(module1);
197
+ expect(allInstances).toContainEqual(module2);
198
+ });
199
+
200
+ it('should handle updating non-existent module gracefully', () => {
201
+ useModuleStore.getState().updateModule('non-existent', {
202
+ position: { x: 100, y: 100 },
203
+ });
204
+
205
+ // Should not throw error and instances should remain empty
206
+ expect(useModuleStore.getState().instances.size).toBe(0);
207
+ });
208
+
209
+ it('should handle removing non-existent module gracefully', () => {
210
+ const module: ModuleInstance = {
211
+ id: 'module-1',
212
+ type: 'eventlog',
213
+ position: { x: 100, y: 100 },
214
+ size: { width: 600, height: 400 },
215
+ visible: true,
216
+ };
217
+
218
+ useModuleStore.getState().addModule(module);
219
+ useModuleStore.getState().removeModule('non-existent');
220
+
221
+ // Should not affect existing modules
222
+ expect(useModuleStore.getState().instances.size).toBe(1);
223
+ expect(useModuleStore.getState().instances.get('module-1')).toEqual(module);
224
+ });
225
+
226
+ it('should handle toggling visibility of non-existent module gracefully', () => {
227
+ useModuleStore.getState().toggleVisibility('non-existent');
228
+
229
+ // Should not throw error
230
+ expect(useModuleStore.getState().instances.size).toBe(0);
231
+ });
232
+
233
+ it('should maintain Map state immutability', () => {
234
+ const module: ModuleInstance = {
235
+ id: 'module-1',
236
+ type: 'eventlog',
237
+ position: { x: 100, y: 100 },
238
+ size: { width: 600, height: 400 },
239
+ visible: true,
240
+ };
241
+
242
+ useModuleStore.getState().addModule(module);
243
+ const instancesBefore = useModuleStore.getState().instances;
244
+
245
+ useModuleStore.getState().updateModule('module-1', {
246
+ position: { x: 200, y: 200 },
247
+ });
248
+ const instancesAfter = useModuleStore.getState().instances;
249
+
250
+ // New Map instance should be created (immutability)
251
+ expect(instancesBefore).not.toBe(instancesAfter);
252
+ });
253
+ });