flock-core 0.5.0b65__py3-none-any.whl → 0.5.0b70__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 (56) hide show
  1. flock/cli.py +74 -2
  2. flock/engines/dspy_engine.py +40 -4
  3. flock/examples.py +4 -1
  4. flock/frontend/README.md +15 -1
  5. flock/frontend/package-lock.json +2 -2
  6. flock/frontend/package.json +1 -1
  7. flock/frontend/src/App.tsx +74 -6
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +4 -5
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +7 -3
  10. flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
  11. flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
  12. flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
  13. flock/frontend/src/components/filters/FilterPills.module.css +186 -45
  14. flock/frontend/src/components/filters/FilterPills.test.tsx +115 -99
  15. flock/frontend/src/components/filters/FilterPills.tsx +120 -44
  16. flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
  17. flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
  18. flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
  19. flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
  20. flock/frontend/src/components/filters/TagFilter.tsx +21 -0
  21. flock/frontend/src/components/filters/TimeRangeFilter.module.css +24 -0
  22. flock/frontend/src/components/filters/TimeRangeFilter.tsx +6 -1
  23. flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
  24. flock/frontend/src/components/graph/GraphCanvas.tsx +24 -0
  25. flock/frontend/src/components/layout/DashboardLayout.css +13 -0
  26. flock/frontend/src/components/layout/DashboardLayout.tsx +8 -24
  27. flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
  28. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +460 -0
  29. flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
  30. flock/frontend/src/components/modules/ModuleRegistry.ts +7 -1
  31. flock/frontend/src/components/modules/registerModules.ts +9 -10
  32. flock/frontend/src/hooks/useModules.ts +11 -1
  33. flock/frontend/src/services/api.ts +140 -0
  34. flock/frontend/src/services/indexeddb.ts +56 -2
  35. flock/frontend/src/services/websocket.ts +129 -0
  36. flock/frontend/src/store/filterStore.test.ts +105 -185
  37. flock/frontend/src/store/filterStore.ts +173 -26
  38. flock/frontend/src/store/graphStore.test.ts +19 -0
  39. flock/frontend/src/store/graphStore.ts +166 -27
  40. flock/frontend/src/types/filters.ts +34 -1
  41. flock/frontend/src/types/graph.ts +7 -0
  42. flock/frontend/src/utils/artifacts.ts +24 -0
  43. flock/orchestrator.py +23 -1
  44. flock/service.py +146 -9
  45. flock/store.py +971 -24
  46. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/METADATA +26 -1
  47. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/RECORD +50 -43
  48. flock/frontend/src/components/filters/FilterBar.module.css +0 -29
  49. flock/frontend/src/components/filters/FilterBar.test.tsx +0 -133
  50. flock/frontend/src/components/filters/FilterBar.tsx +0 -33
  51. flock/frontend/src/components/modules/EventLogModule.test.tsx +0 -401
  52. flock/frontend/src/components/modules/EventLogModule.tsx +0 -396
  53. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +0 -17
  54. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/WHEEL +0 -0
  55. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/entry_points.txt +0 -0
  56. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/licenses/LICENSE +0 -0
@@ -1,79 +1,220 @@
1
- .container {
1
+ .wrapper {
2
2
  display: flex;
3
- gap: var(--spacing-2);
4
3
  align-items: center;
5
- flex-wrap: wrap;
4
+ width: 100%;
5
+ gap: var(--gap-md);
6
6
  }
7
7
 
8
- .pill {
9
- display: flex;
8
+ .container {
9
+ display: inline-flex;
10
10
  align-items: center;
11
+ flex-wrap: wrap;
11
12
  gap: var(--gap-md);
12
- padding: var(--spacing-1-5) var(--spacing-3);
13
- background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
14
- border: 1px solid var(--color-primary-500);
13
+ pointer-events: auto;
14
+ margin-left: 0;
15
+ padding: 0;
16
+ max-width: 100%;
17
+ }
18
+
19
+ .toggleButton {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ gap: var(--gap-sm);
23
+ padding: var(--spacing-1) var(--spacing-3);
15
24
  border-radius: var(--radius-full);
16
- font-size: var(--font-size-body-xs);
25
+ border: var(--border-subtle);
26
+ background: color-mix(in srgb, var(--color-bg-surface) 80%, transparent);
27
+ color: var(--color-text-secondary);
28
+ font-size: var(--font-size-body-sm);
17
29
  font-weight: var(--font-weight-medium);
30
+ letter-spacing: var(--letter-spacing-wide);
31
+ text-transform: uppercase;
32
+ cursor: pointer;
33
+ transition: var(--transition-colors), var(--transition-transform), var(--transition-shadow);
34
+ }
35
+
36
+ .toggleButton:hover {
37
+ color: var(--color-text-primary);
38
+ border-color: color-mix(in srgb, var(--color-border-focus) 60%, transparent);
39
+ background: color-mix(in srgb, var(--color-bg-overlay) 75%, transparent);
40
+ }
41
+
42
+ .toggleButton:focus-visible {
43
+ outline: none;
44
+ box-shadow: var(--shadow-glow-primary);
45
+ color: var(--color-text-primary);
46
+ }
47
+
48
+ .toggleButton:active {
49
+ transform: scale(0.98);
50
+ }
51
+
52
+ .toggleButtonActive {
53
+ color: var(--color-text-on-primary);
54
+ background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-primary-600) 100%);
55
+ border-color: color-mix(in srgb, var(--color-primary-400) 70%, transparent);
56
+ box-shadow: var(--shadow-sm);
57
+ }
58
+
59
+ .toggleButtonActive:hover {
60
+ box-shadow: var(--shadow-md);
61
+ }
62
+
63
+ .toggleIcon {
64
+ display: inline-flex;
65
+ width: 1rem;
66
+ height: 1rem;
67
+ }
68
+
69
+ .toggleIcon svg {
70
+ width: 100%;
71
+ height: 100%;
72
+ }
73
+
74
+ .toggleButtonActive .toggleIcon {
18
75
  color: var(--color-text-on-primary);
19
- box-shadow: var(--shadow-xs);
20
- transition: var(--transition-all);
21
- animation: slideIn var(--duration-normal) var(--ease-out);
76
+ }
77
+
78
+ .toggleLabel {
79
+ letter-spacing: var(--letter-spacing-wide);
80
+ }
81
+
82
+ .pill {
83
+ --pill-accent: var(--color-primary-500);
84
+ --pill-accent-soft: color-mix(in srgb, var(--color-bg-overlay) 88%, var(--pill-accent) 12%);
85
+ --pill-border: color-mix(in srgb, var(--pill-accent) 45%, transparent);
86
+ --pill-shadow: 0 12px 28px -18px color-mix(in srgb, var(--pill-accent) 40%, transparent);
87
+
88
+ display: inline-flex;
89
+ align-items: center;
90
+ gap: var(--gap-sm);
91
+ padding: var(--spacing-1-5) var(--spacing-3);
92
+ border-radius: var(--radius-full);
93
+ background: var(--color-bg-overlay);
94
+ background: linear-gradient(135deg, var(--pill-accent-soft) 0%, color-mix(in srgb, var(--color-bg-surface) 92%, transparent) 100%);
95
+ border: 1px solid var(--pill-border);
96
+ color: var(--color-text-primary);
97
+ box-shadow: var(--shadow-xs), var(--pill-shadow);
98
+ transition: var(--transition-transform), var(--transition-shadow), var(--transition-colors);
99
+ cursor: default;
100
+ animation: fadeIn var(--duration-normal) var(--ease-out);
22
101
  }
23
102
 
24
103
  .pill:hover {
25
- transform: translateY(-1px);
26
- box-shadow: var(--shadow-sm);
104
+ transform: translateY(-2px);
105
+ box-shadow: 0 16px 32px -18px color-mix(in srgb, var(--pill-accent) 55%, transparent);
106
+ }
107
+
108
+ .pill:focus-within {
109
+ box-shadow:
110
+ 0 16px 32px -18px color-mix(in srgb, var(--pill-accent) 55%, transparent),
111
+ 0 0 0 2px color-mix(in srgb, var(--pill-accent) 35%, transparent);
27
112
  }
28
113
 
29
- .pillSecondary {
30
- background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
31
- border-color: var(--color-secondary-500);
114
+ .pillAccent {
115
+ width: 0.75rem;
116
+ height: 0.75rem;
117
+ border-radius: var(--radius-full);
118
+ flex-shrink: 0;
119
+ background: var(--pill-accent);
120
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--pill-accent) 25%, transparent);
121
+ }
122
+
123
+ .textGroup {
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 2px;
127
+ line-height: 1.1;
32
128
  }
33
129
 
34
- .pillLabel {
35
- user-select: none;
130
+ .pillTitle {
131
+ font-size: var(--font-size-tiny);
132
+ font-weight: var(--font-weight-medium);
133
+ letter-spacing: var(--letter-spacing-wide);
134
+ text-transform: uppercase;
135
+ color: var(--color-text-tertiary);
136
+ }
137
+
138
+ .pillValue {
139
+ font-size: var(--font-size-body-sm);
140
+ font-weight: var(--font-weight-semibold);
141
+ color: var(--color-text-primary);
142
+ line-height: 1.3;
36
143
  }
37
144
 
38
145
  .removeButton {
39
146
  background: none;
40
147
  border: none;
41
- cursor: pointer;
42
- padding: 0 var(--spacing-0-5);
43
- font-size: 16px;
44
- color: var(--color-text-on-primary);
45
- line-height: 1;
46
- display: flex;
148
+ margin-left: var(--gap-sm);
149
+ display: inline-flex;
47
150
  align-items: center;
48
151
  justify-content: center;
49
- transition: var(--transition-transform);
50
- opacity: 0.8;
152
+ width: 1.75rem;
153
+ height: 1.75rem;
154
+ border-radius: var(--radius-full);
155
+ color: var(--color-text-tertiary);
156
+ transition: var(--transition-transform), var(--transition-colors), var(--transition-shadow);
157
+ cursor: pointer;
51
158
  }
52
159
 
53
- .removeButton:hover {
54
- opacity: 1;
55
- transform: scale(1.2);
160
+ .removeButton:hover,
161
+ .removeButton:focus-visible {
162
+ color: var(--color-text-primary);
163
+ background: color-mix(in srgb, var(--pill-accent) 18%, transparent);
56
164
  }
57
165
 
58
- @keyframes slideIn {
59
- from {
60
- opacity: 0;
61
- transform: translateX(-10px);
62
- }
63
- to {
64
- opacity: 1;
65
- transform: translateX(0);
66
- }
166
+ .removeButton:focus-visible {
167
+ outline: none;
168
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--pill-accent) 35%, transparent);
67
169
  }
68
170
 
69
- /* Animation when pill is removed */
70
- .pill.removing {
71
- animation: slideOut var(--duration-fast) var(--ease-in) forwards;
171
+ .removeButton:active {
172
+ transform: scale(0.92);
72
173
  }
73
174
 
74
- @keyframes slideOut {
75
- to {
175
+ .removeIcon {
176
+ font-size: var(--font-size-body);
177
+ line-height: 1;
178
+ }
179
+
180
+ .pill[data-filter-type='timeRange'] {
181
+ --pill-accent: var(--color-tertiary-500);
182
+ }
183
+
184
+ .pill[data-filter-type='artifactTypes'] {
185
+ --pill-accent: var(--color-secondary-500);
186
+ }
187
+
188
+ .pill[data-filter-type='producers'] {
189
+ --pill-accent: var(--color-success);
190
+ }
191
+
192
+ .pill[data-filter-type='tags'] {
193
+ --pill-accent: var(--color-secondary-400);
194
+ }
195
+
196
+ .pill[data-filter-type='visibility'] {
197
+ --pill-accent: var(--color-info);
198
+ }
199
+
200
+ @media (max-width: 768px) {
201
+ .wrapper {
202
+ width: 100%;
203
+ }
204
+
205
+ .container {
206
+ border-radius: var(--radius-xl);
207
+ width: 100%;
208
+ }
209
+ }
210
+
211
+ @keyframes fadeIn {
212
+ from {
76
213
  opacity: 0;
77
- transform: scale(0.8) translateX(-10px);
214
+ transform: translateY(6px);
215
+ }
216
+ to {
217
+ opacity: 1;
218
+ transform: translateY(0);
78
219
  }
79
220
  }
@@ -1,83 +1,125 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { render, screen, fireEvent, within } from '@testing-library/react';
3
3
  import FilterPills from './FilterPills';
4
- import { useFilterStore } from '../../store/filterStore';
4
+ import { useFilterStore, formatTimeRange } from '../../store/filterStore';
5
+ import { useSettingsStore } from '../../store/settingsStore';
6
+ import type { TimeRange } from '../../types/filters';
7
+
8
+ vi.mock('../../store/filterStore', async () => {
9
+ const actual = await vi.importActual<typeof import('../../store/filterStore')>('../../store/filterStore');
10
+ return {
11
+ ...actual,
12
+ useFilterStore: vi.fn(),
13
+ };
14
+ });
5
15
 
6
- vi.mock('../../store/filterStore');
16
+ vi.mock('../../store/settingsStore', async () => {
17
+ const actual = await vi.importActual<typeof import('../../store/settingsStore')>('../../store/settingsStore');
18
+ return {
19
+ ...actual,
20
+ useSettingsStore: vi.fn(),
21
+ };
22
+ });
7
23
 
8
24
  describe('FilterPills', () => {
9
25
  const mockRemoveFilter = vi.fn();
10
-
11
- beforeEach(() => {
12
- vi.clearAllMocks();
13
- });
14
-
15
- it('should not render anything when no filters are active', () => {
26
+ const mockSetShowFilters = vi.fn();
27
+
28
+ type MockFilterState = {
29
+ correlationId: string | null;
30
+ timeRange: TimeRange;
31
+ selectedArtifactTypes: string[];
32
+ selectedProducers: string[];
33
+ selectedTags: string[];
34
+ selectedVisibility: string[];
35
+ removeFilter: typeof mockRemoveFilter;
36
+ };
37
+
38
+ const setupStore = (override: Partial<MockFilterState> = {}) => {
16
39
  vi.mocked(useFilterStore).mockImplementation((selector: any) => {
17
- const state = {
40
+ const state: MockFilterState = {
18
41
  correlationId: null,
19
- timeRange: { preset: 'last10min' as const },
42
+ timeRange: { preset: 'last10min' },
43
+ selectedArtifactTypes: [],
44
+ selectedProducers: [],
45
+ selectedTags: [],
46
+ selectedVisibility: [],
20
47
  removeFilter: mockRemoveFilter,
48
+ ...override,
21
49
  };
22
50
  return selector(state);
23
51
  });
24
52
 
25
- const { container } = render(<FilterPills />);
26
- expect(container.firstChild).toBeNull();
53
+ vi.mocked(useSettingsStore).mockImplementation((selector: any) =>
54
+ selector({
55
+ ui: { showFilters: false },
56
+ setShowFilters: mockSetShowFilters,
57
+ })
58
+ );
59
+ };
60
+
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ });
64
+
65
+ it('should render the default time range filter when no other filters are active', () => {
66
+ setupStore();
67
+
68
+ render(<FilterPills />);
69
+ expect(screen.getByRole('button', { name: /filter panel/i })).toBeInTheDocument();
70
+ expect(screen.getByText('Time')).toBeInTheDocument();
71
+ expect(screen.getByText('Last 10 min')).toBeInTheDocument();
27
72
  });
28
73
 
29
74
  it('should render filter pill for active correlation ID', () => {
30
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
31
- const state = {
32
- correlationId: 'test-123',
33
- timeRange: { preset: 'last10min' as const },
34
- removeFilter: mockRemoveFilter,
35
- };
36
- return selector(state);
37
- });
75
+ setupStore({ correlationId: 'test-123' });
38
76
 
39
77
  render(<FilterPills />);
40
- expect(screen.getByText('Correlation ID: test-123')).toBeInTheDocument();
78
+ const pill = screen.getAllByRole('listitem').find((item) => within(item).queryByText('Correlation ID'));
79
+ expect(pill).toBeDefined();
80
+ if (!pill) {
81
+ throw new Error('Correlation ID pill not found');
82
+ }
83
+ expect(within(pill).getByText('Correlation ID')).toBeInTheDocument();
84
+ expect(within(pill).getByText('test-123')).toBeInTheDocument();
41
85
  });
42
86
 
43
87
  it('should render filter pill for active time range', () => {
44
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
45
- const state = {
46
- correlationId: null,
47
- timeRange: { preset: 'last5min' as const },
48
- removeFilter: mockRemoveFilter,
49
- };
50
- return selector(state);
51
- });
88
+ setupStore({ timeRange: { preset: 'last5min' } as TimeRange });
52
89
 
53
90
  render(<FilterPills />);
54
- expect(screen.getByText('Time: Last 5 min')).toBeInTheDocument();
91
+ const pill = screen.getByRole('listitem');
92
+ expect(within(pill).getByText('Time')).toBeInTheDocument();
93
+ expect(within(pill).getByText('Last 5 min')).toBeInTheDocument();
94
+ });
95
+
96
+ it('should toggle filters when toggle button is clicked', () => {
97
+ setupStore();
98
+
99
+ render(<FilterPills />);
100
+ fireEvent.click(screen.getByRole('button', { name: /filter panel/i }));
101
+
102
+ expect(mockSetShowFilters).toHaveBeenCalledWith(true);
55
103
  });
56
104
 
57
105
  it('should render multiple filter pills', () => {
58
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
59
- const state = {
60
- correlationId: 'test-123',
61
- timeRange: { preset: 'last1hour' as const },
62
- removeFilter: mockRemoveFilter,
63
- };
64
- return selector(state);
106
+ setupStore({
107
+ correlationId: 'test-123',
108
+ timeRange: { preset: 'last1hour' } as TimeRange,
109
+ selectedArtifactTypes: ['__main__.Example'],
65
110
  });
66
111
 
67
112
  render(<FilterPills />);
68
- expect(screen.getByText('Correlation ID: test-123')).toBeInTheDocument();
69
- expect(screen.getByText('Time: Last hour')).toBeInTheDocument();
113
+ expect(screen.getByText('Correlation ID')).toBeInTheDocument();
114
+ expect(screen.getByText('test-123')).toBeInTheDocument();
115
+ expect(screen.getByText('Time')).toBeInTheDocument();
116
+ expect(screen.getByText('Last hour')).toBeInTheDocument();
117
+ expect(screen.getByText('Type')).toBeInTheDocument();
118
+ expect(screen.getByText('__main__.Example')).toBeInTheDocument();
70
119
  });
71
120
 
72
121
  it('should render remove button for each pill', () => {
73
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
74
- const state = {
75
- correlationId: 'test-123',
76
- timeRange: { preset: 'last10min' as const },
77
- removeFilter: mockRemoveFilter,
78
- };
79
- return selector(state);
80
- });
122
+ setupStore({ correlationId: 'test-123' });
81
123
 
82
124
  render(<FilterPills />);
83
125
  const removeButton = screen.getByRole('button', { name: /remove.*correlation/i });
@@ -85,89 +127,63 @@ describe('FilterPills', () => {
85
127
  });
86
128
 
87
129
  it('should call removeFilter when remove button is clicked', () => {
88
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
89
- const state = {
90
- correlationId: 'test-123',
91
- timeRange: { preset: 'last10min' as const },
92
- removeFilter: mockRemoveFilter,
93
- };
94
- return selector(state);
95
- });
130
+ setupStore({ correlationId: 'test-123' });
96
131
 
97
132
  render(<FilterPills />);
98
133
  const removeButton = screen.getByRole('button', { name: /remove.*correlation/i });
99
134
 
100
135
  fireEvent.click(removeButton);
101
136
 
102
- expect(mockRemoveFilter).toHaveBeenCalledWith('correlationId');
137
+ expect(mockRemoveFilter).toHaveBeenCalledWith({
138
+ type: 'correlationId',
139
+ value: 'test-123',
140
+ label: 'Correlation ID: test-123',
141
+ });
103
142
  });
104
143
 
105
144
  it('should call removeFilter with correct type for time range', () => {
106
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
107
- const state = {
108
- correlationId: null,
109
- timeRange: { preset: 'last5min' as const },
110
- removeFilter: mockRemoveFilter,
111
- };
112
- return selector(state);
113
- });
145
+ const timeRange = { preset: 'last5min' } as TimeRange;
146
+ setupStore({ timeRange });
114
147
 
115
148
  render(<FilterPills />);
116
149
  const removeButton = screen.getByRole('button', { name: /remove.*time/i });
117
150
 
118
151
  fireEvent.click(removeButton);
119
152
 
120
- expect(mockRemoveFilter).toHaveBeenCalledWith('timeRange');
153
+ expect(mockRemoveFilter).toHaveBeenCalledWith({
154
+ type: 'timeRange',
155
+ value: timeRange,
156
+ label: 'Time: Last 5 min',
157
+ });
121
158
  });
122
159
 
123
160
  it('should display pills in a horizontal layout', () => {
124
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
125
- const state = {
126
- correlationId: 'test-123',
127
- timeRange: { preset: 'last5min' as const },
128
- removeFilter: mockRemoveFilter,
129
- };
130
- return selector(state);
131
- });
161
+ setupStore({ correlationId: 'test-123' });
132
162
 
133
163
  render(<FilterPills />);
134
- const container = screen.getByText('Correlation ID: test-123').closest('div')?.parentElement;
135
-
136
- // Should have container class (hashed by CSS modules)
164
+ const container = screen.getByRole('list');
137
165
  expect(container?.className).toMatch(/container/);
138
166
  });
139
167
 
140
168
  it('should render X icon in remove button', () => {
141
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
142
- const state = {
143
- correlationId: 'test-123',
144
- timeRange: { preset: 'last10min' as const },
145
- removeFilter: mockRemoveFilter,
146
- };
147
- return selector(state);
148
- });
169
+ setupStore({ correlationId: 'test-123' });
149
170
 
150
171
  render(<FilterPills />);
151
172
  const removeButton = screen.getByRole('button', { name: /remove.*correlation/i });
152
173
 
153
- expect(removeButton).toHaveTextContent('×');
174
+ expect(within(removeButton).getByText('×')).toBeInTheDocument();
154
175
  });
155
176
 
156
177
  it('should handle custom time range label', () => {
157
- vi.mocked(useFilterStore).mockImplementation((selector: any) => {
158
- const state = {
159
- correlationId: null,
160
- timeRange: {
161
- preset: 'custom' as const,
162
- start: new Date('2025-01-01T10:00:00').getTime(),
163
- end: new Date('2025-01-01T12:00:00').getTime(),
164
- },
165
- removeFilter: mockRemoveFilter,
166
- };
167
- return selector(state);
168
- });
178
+ const customRange: TimeRange = {
179
+ preset: 'custom',
180
+ start: new Date('2025-01-01T10:00:00').getTime(),
181
+ end: new Date('2025-01-01T12:00:00').getTime(),
182
+ };
183
+ setupStore({ timeRange: customRange });
169
184
 
170
185
  render(<FilterPills />);
171
- expect(screen.getByText(/Time:/)).toBeInTheDocument();
186
+ expect(screen.getByText('Time')).toBeInTheDocument();
187
+ expect(screen.getByText(formatTimeRange(customRange))).toBeInTheDocument();
172
188
  });
173
189
  });