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.
- flock/dashboard/launcher.py +1 -1
- flock/frontend/README.md +678 -0
- flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
- flock/frontend/index.html +12 -0
- flock/frontend/package-lock.json +4347 -0
- flock/frontend/package.json +48 -0
- flock/frontend/src/App.tsx +79 -0
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
- flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
- flock/frontend/src/components/common/BuildInfo.tsx +39 -0
- flock/frontend/src/components/common/EmptyState.module.css +115 -0
- flock/frontend/src/components/common/EmptyState.tsx +128 -0
- flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
- flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
- flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
- flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
- flock/frontend/src/components/controls/PublishControl.css +547 -0
- flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
- flock/frontend/src/components/controls/PublishControl.tsx +432 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
- flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
- flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
- flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
- flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
- flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
- flock/frontend/src/components/details/tabs.test.tsx +1015 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
- flock/frontend/src/components/filters/FilterBar.module.css +29 -0
- flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
- flock/frontend/src/components/filters/FilterBar.tsx +33 -0
- flock/frontend/src/components/filters/FilterPills.module.css +79 -0
- flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
- flock/frontend/src/components/filters/FilterPills.tsx +67 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
- flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
- flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
- flock/frontend/src/components/graph/AgentNode.tsx +322 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
- flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
- flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
- flock/frontend/src/components/graph/MessageNode.tsx +116 -0
- flock/frontend/src/components/graph/MiniMap.tsx +47 -0
- flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
- flock/frontend/src/components/layout/DashboardLayout.css +407 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
- flock/frontend/src/components/layout/Header.module.css +88 -0
- flock/frontend/src/components/layout/Header.tsx +52 -0
- flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
- flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
- flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
- flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
- flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
- flock/frontend/src/components/modules/registerModules.ts +20 -0
- flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
- flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
- flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
- flock/frontend/src/components/settings/SettingsPanel.css +327 -0
- flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
- flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
- flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
- flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
- flock/frontend/src/hooks/useModulePersistence.ts +154 -0
- flock/frontend/src/hooks/useModules.ts +139 -0
- flock/frontend/src/hooks/usePersistence.ts +139 -0
- flock/frontend/src/main.tsx +13 -0
- flock/frontend/src/services/api.ts +213 -0
- flock/frontend/src/services/indexeddb.test.ts +793 -0
- flock/frontend/src/services/indexeddb.ts +794 -0
- flock/frontend/src/services/layout.test.ts +437 -0
- flock/frontend/src/services/layout.ts +146 -0
- flock/frontend/src/services/themeApplicator.ts +140 -0
- flock/frontend/src/services/themeService.ts +77 -0
- flock/frontend/src/services/websocket.test.ts +595 -0
- flock/frontend/src/services/websocket.ts +685 -0
- flock/frontend/src/store/filterStore.test.ts +242 -0
- flock/frontend/src/store/filterStore.ts +103 -0
- flock/frontend/src/store/graphStore.test.ts +186 -0
- flock/frontend/src/store/graphStore.ts +414 -0
- flock/frontend/src/store/moduleStore.test.ts +253 -0
- flock/frontend/src/store/moduleStore.ts +57 -0
- flock/frontend/src/store/settingsStore.ts +188 -0
- flock/frontend/src/store/streamStore.ts +68 -0
- flock/frontend/src/store/uiStore.test.ts +54 -0
- flock/frontend/src/store/uiStore.ts +110 -0
- flock/frontend/src/store/wsStore.ts +34 -0
- flock/frontend/src/styles/index.css +15 -0
- flock/frontend/src/styles/scrollbar.css +47 -0
- flock/frontend/src/styles/variables.css +488 -0
- flock/frontend/src/test/setup.ts +1 -0
- flock/frontend/src/types/filters.ts +14 -0
- flock/frontend/src/types/graph.ts +55 -0
- flock/frontend/src/types/modules.ts +7 -0
- flock/frontend/src/types/theme.ts +55 -0
- flock/frontend/src/utils/mockData.ts +85 -0
- flock/frontend/src/utils/performance.ts +16 -0
- flock/frontend/src/utils/transforms.test.ts +860 -0
- flock/frontend/src/utils/transforms.ts +323 -0
- flock/frontend/src/vite-env.d.ts +17 -0
- flock/frontend/tsconfig.json +27 -0
- flock/frontend/tsconfig.node.json +11 -0
- flock/frontend/vite.config.ts +25 -0
- flock/frontend/vitest.config.ts +11 -0
- flock/helper/cli_helper.py +1 -1
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/RECORD +117 -7
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for LiveOutputTab component.
|
|
3
|
+
*
|
|
4
|
+
* Tests verify streaming output accumulation, ordering, auto-scroll,
|
|
5
|
+
* virtualization performance, output type rendering, and final marker handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { render, screen } from '@testing-library/react';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
|
|
12
|
+
// Streaming output data type from PRD
|
|
13
|
+
interface StreamingOutputData {
|
|
14
|
+
agent_name: string;
|
|
15
|
+
run_id: string;
|
|
16
|
+
output_type: 'llm_token' | 'log' | 'stdout' | 'stderr';
|
|
17
|
+
content: string;
|
|
18
|
+
sequence: number;
|
|
19
|
+
is_final: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Mock component - will be replaced by actual implementation
|
|
23
|
+
const MockLiveOutputTab = ({
|
|
24
|
+
nodeId,
|
|
25
|
+
outputs,
|
|
26
|
+
autoScroll = true,
|
|
27
|
+
}: {
|
|
28
|
+
nodeId: string;
|
|
29
|
+
outputs: StreamingOutputData[];
|
|
30
|
+
autoScroll?: boolean;
|
|
31
|
+
}) => {
|
|
32
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
33
|
+
|
|
34
|
+
// Sort outputs by sequence number
|
|
35
|
+
const sortedOutputs = [...outputs].sort((a, b) => a.sequence - b.sequence);
|
|
36
|
+
|
|
37
|
+
// Auto-scroll to bottom when new output arrives
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (autoScroll && containerRef.current) {
|
|
40
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
41
|
+
}
|
|
42
|
+
}, [outputs.length, autoScroll]);
|
|
43
|
+
|
|
44
|
+
const getOutputColor = (type: StreamingOutputData['output_type']) => {
|
|
45
|
+
switch (type) {
|
|
46
|
+
case 'llm_token':
|
|
47
|
+
return '#8be9fd';
|
|
48
|
+
case 'log':
|
|
49
|
+
return '#f1fa8c';
|
|
50
|
+
case 'stdout':
|
|
51
|
+
return '#50fa7b';
|
|
52
|
+
case 'stderr':
|
|
53
|
+
return '#ff5555';
|
|
54
|
+
default:
|
|
55
|
+
return '#f8f8f2';
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-testid={`live-output-${nodeId}`}
|
|
62
|
+
ref={containerRef}
|
|
63
|
+
style={{
|
|
64
|
+
height: '400px',
|
|
65
|
+
overflow: 'auto',
|
|
66
|
+
background: '#282a36',
|
|
67
|
+
padding: '8px',
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{sortedOutputs.length === 0 ? (
|
|
71
|
+
<div data-testid="empty-output">No output yet...</div>
|
|
72
|
+
) : (
|
|
73
|
+
sortedOutputs.map((output, index) => (
|
|
74
|
+
<div
|
|
75
|
+
key={`${output.run_id}-${output.sequence}`}
|
|
76
|
+
data-testid={`output-line-${index}`}
|
|
77
|
+
data-output-type={output.output_type}
|
|
78
|
+
data-sequence={output.sequence}
|
|
79
|
+
style={{
|
|
80
|
+
color: getOutputColor(output.output_type),
|
|
81
|
+
fontFamily: 'monospace',
|
|
82
|
+
fontSize: '12px',
|
|
83
|
+
whiteSpace: 'pre-wrap',
|
|
84
|
+
minHeight: '20px', // Ensure each line has height for scrolling
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{output.content}
|
|
88
|
+
</div>
|
|
89
|
+
))
|
|
90
|
+
)}
|
|
91
|
+
{sortedOutputs.some((o) => o.is_final) && (
|
|
92
|
+
<div data-testid="final-marker" style={{ color: '#6272a4', marginTop: '8px' }}>
|
|
93
|
+
--- End of output ---
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Mock virtualized component for performance testing
|
|
101
|
+
const MockVirtualizedOutputTab = ({
|
|
102
|
+
nodeId,
|
|
103
|
+
outputs,
|
|
104
|
+
}: {
|
|
105
|
+
nodeId: string;
|
|
106
|
+
outputs: StreamingOutputData[];
|
|
107
|
+
}) => {
|
|
108
|
+
const [visibleRange, setVisibleRange] = React.useState({ start: 0, end: 50 });
|
|
109
|
+
|
|
110
|
+
const sortedOutputs = [...outputs].sort((a, b) => a.sequence - b.sequence);
|
|
111
|
+
const visibleOutputs = sortedOutputs.slice(visibleRange.start, visibleRange.end);
|
|
112
|
+
|
|
113
|
+
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
114
|
+
const target = e.target as HTMLDivElement;
|
|
115
|
+
const scrollTop = target.scrollTop;
|
|
116
|
+
const itemHeight = 20;
|
|
117
|
+
const start = Math.floor(scrollTop / itemHeight);
|
|
118
|
+
const end = start + 50;
|
|
119
|
+
setVisibleRange({ start, end });
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
data-testid={`virtualized-output-${nodeId}`}
|
|
125
|
+
onScroll={handleScroll}
|
|
126
|
+
style={{ height: '400px', overflow: 'auto' }}
|
|
127
|
+
>
|
|
128
|
+
<div style={{ height: `${sortedOutputs.length * 20}px`, position: 'relative' }}>
|
|
129
|
+
{visibleOutputs.map((output, index) => (
|
|
130
|
+
<div
|
|
131
|
+
key={`${output.run_id}-${output.sequence}`}
|
|
132
|
+
data-testid={`virtual-line-${visibleRange.start + index}`}
|
|
133
|
+
style={{
|
|
134
|
+
position: 'absolute',
|
|
135
|
+
top: `${(visibleRange.start + index) * 20}px`,
|
|
136
|
+
height: '20px',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{output.content}
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
<div data-testid="total-lines">{sortedOutputs.length}</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
describe('LiveOutputTab', () => {
|
|
149
|
+
describe('Output Display', () => {
|
|
150
|
+
it('should render empty state when no output', () => {
|
|
151
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={[]} />);
|
|
152
|
+
|
|
153
|
+
expect(screen.getByTestId('empty-output')).toBeInTheDocument();
|
|
154
|
+
expect(screen.getByText('No output yet...')).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should display streaming output content', () => {
|
|
158
|
+
const outputs: StreamingOutputData[] = [
|
|
159
|
+
{
|
|
160
|
+
agent_name: 'test_agent',
|
|
161
|
+
run_id: 'run-1',
|
|
162
|
+
output_type: 'llm_token',
|
|
163
|
+
content: 'Hello',
|
|
164
|
+
sequence: 0,
|
|
165
|
+
is_final: false,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
agent_name: 'test_agent',
|
|
169
|
+
run_id: 'run-1',
|
|
170
|
+
output_type: 'llm_token',
|
|
171
|
+
content: ' World',
|
|
172
|
+
sequence: 1,
|
|
173
|
+
is_final: false,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
178
|
+
|
|
179
|
+
expect(screen.getByText('Hello')).toBeInTheDocument();
|
|
180
|
+
// Use data-testid to check content with leading whitespace (disable normalization)
|
|
181
|
+
const line1 = screen.getByTestId('output-line-1');
|
|
182
|
+
expect(line1.textContent).toBe(' World');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should append new content as it arrives', () => {
|
|
186
|
+
const { rerender } = render(<MockLiveOutputTab nodeId="agent-1" outputs={[]} />);
|
|
187
|
+
|
|
188
|
+
expect(screen.getByTestId('empty-output')).toBeInTheDocument();
|
|
189
|
+
|
|
190
|
+
// First token
|
|
191
|
+
const outputs1: StreamingOutputData[] = [
|
|
192
|
+
{
|
|
193
|
+
agent_name: 'test_agent',
|
|
194
|
+
run_id: 'run-1',
|
|
195
|
+
output_type: 'llm_token',
|
|
196
|
+
content: 'First',
|
|
197
|
+
sequence: 0,
|
|
198
|
+
is_final: false,
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
rerender(<MockLiveOutputTab nodeId="agent-1" outputs={outputs1} />);
|
|
202
|
+
expect(screen.getByText('First')).toBeInTheDocument();
|
|
203
|
+
|
|
204
|
+
// Second token
|
|
205
|
+
const outputs2: StreamingOutputData[] = [
|
|
206
|
+
...outputs1,
|
|
207
|
+
{
|
|
208
|
+
agent_name: 'test_agent',
|
|
209
|
+
run_id: 'run-1',
|
|
210
|
+
output_type: 'llm_token',
|
|
211
|
+
content: ' Second',
|
|
212
|
+
sequence: 1,
|
|
213
|
+
is_final: false,
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
rerender(<MockLiveOutputTab nodeId="agent-1" outputs={outputs2} />);
|
|
217
|
+
expect(screen.getByText('First')).toBeInTheDocument();
|
|
218
|
+
// Use data-testid to check content with leading whitespace (disable normalization)
|
|
219
|
+
const line1 = screen.getByTestId('output-line-1');
|
|
220
|
+
expect(line1.textContent).toBe(' Second');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should simulate token-by-token streaming', async () => {
|
|
224
|
+
const tokens = ['The', ' quick', ' brown', ' fox', ' jumps'];
|
|
225
|
+
const outputs: StreamingOutputData[] = [];
|
|
226
|
+
|
|
227
|
+
const { rerender } = render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
230
|
+
outputs.push({
|
|
231
|
+
agent_name: 'test_agent',
|
|
232
|
+
run_id: 'run-1',
|
|
233
|
+
output_type: 'llm_token',
|
|
234
|
+
content: tokens[i]!,
|
|
235
|
+
sequence: i,
|
|
236
|
+
is_final: false,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
rerender(<MockLiveOutputTab nodeId="agent-1" outputs={[...outputs]} />);
|
|
240
|
+
|
|
241
|
+
// Verify each token is present (check textContent directly to preserve whitespace)
|
|
242
|
+
const token = tokens[i]!;
|
|
243
|
+
const line = screen.getByTestId(`output-line-${i}`);
|
|
244
|
+
expect(line.textContent).toBe(token);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Verify all tokens are displayed
|
|
248
|
+
expect(screen.getAllByTestId(/output-line-/).length).toBe(tokens.length);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('Output Ordering', () => {
|
|
253
|
+
it('should order outputs by sequence number', () => {
|
|
254
|
+
const outputs: StreamingOutputData[] = [
|
|
255
|
+
{
|
|
256
|
+
agent_name: 'test_agent',
|
|
257
|
+
run_id: 'run-1',
|
|
258
|
+
output_type: 'llm_token',
|
|
259
|
+
content: 'Third',
|
|
260
|
+
sequence: 2,
|
|
261
|
+
is_final: false,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
agent_name: 'test_agent',
|
|
265
|
+
run_id: 'run-1',
|
|
266
|
+
output_type: 'llm_token',
|
|
267
|
+
content: 'First',
|
|
268
|
+
sequence: 0,
|
|
269
|
+
is_final: false,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
agent_name: 'test_agent',
|
|
273
|
+
run_id: 'run-1',
|
|
274
|
+
output_type: 'llm_token',
|
|
275
|
+
content: 'Second',
|
|
276
|
+
sequence: 1,
|
|
277
|
+
is_final: false,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
282
|
+
|
|
283
|
+
const lines = screen.getAllByTestId(/output-line-/);
|
|
284
|
+
expect(lines[0]).toHaveAttribute('data-sequence', '0');
|
|
285
|
+
expect(lines[1]).toHaveAttribute('data-sequence', '1');
|
|
286
|
+
expect(lines[2]).toHaveAttribute('data-sequence', '2');
|
|
287
|
+
expect(lines[0]).toHaveTextContent('First');
|
|
288
|
+
expect(lines[1]).toHaveTextContent('Second');
|
|
289
|
+
expect(lines[2]).toHaveTextContent('Third');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should handle out-of-order arrival', () => {
|
|
293
|
+
const { rerender } = render(<MockLiveOutputTab nodeId="agent-1" outputs={[]} />);
|
|
294
|
+
|
|
295
|
+
// Receive sequence 2 first
|
|
296
|
+
const outputs1: StreamingOutputData[] = [
|
|
297
|
+
{
|
|
298
|
+
agent_name: 'test_agent',
|
|
299
|
+
run_id: 'run-1',
|
|
300
|
+
output_type: 'llm_token',
|
|
301
|
+
content: 'Third',
|
|
302
|
+
sequence: 2,
|
|
303
|
+
is_final: false,
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
rerender(<MockLiveOutputTab nodeId="agent-1" outputs={outputs1} />);
|
|
307
|
+
|
|
308
|
+
// Then receive sequence 0
|
|
309
|
+
const outputs2: StreamingOutputData[] = [
|
|
310
|
+
...outputs1,
|
|
311
|
+
{
|
|
312
|
+
agent_name: 'test_agent',
|
|
313
|
+
run_id: 'run-1',
|
|
314
|
+
output_type: 'llm_token',
|
|
315
|
+
content: 'First',
|
|
316
|
+
sequence: 0,
|
|
317
|
+
is_final: false,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
rerender(<MockLiveOutputTab nodeId="agent-1" outputs={outputs2} />);
|
|
321
|
+
|
|
322
|
+
// Verify correct ordering
|
|
323
|
+
const lines = screen.getAllByTestId(/output-line-/);
|
|
324
|
+
expect(lines[0]).toHaveTextContent('First');
|
|
325
|
+
expect(lines[1]).toHaveTextContent('Third');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should maintain sequence order with mixed output types', () => {
|
|
329
|
+
const outputs: StreamingOutputData[] = [
|
|
330
|
+
{
|
|
331
|
+
agent_name: 'test_agent',
|
|
332
|
+
run_id: 'run-1',
|
|
333
|
+
output_type: 'stdout',
|
|
334
|
+
content: 'Second',
|
|
335
|
+
sequence: 1,
|
|
336
|
+
is_final: false,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
agent_name: 'test_agent',
|
|
340
|
+
run_id: 'run-1',
|
|
341
|
+
output_type: 'llm_token',
|
|
342
|
+
content: 'First',
|
|
343
|
+
sequence: 0,
|
|
344
|
+
is_final: false,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
agent_name: 'test_agent',
|
|
348
|
+
run_id: 'run-1',
|
|
349
|
+
output_type: 'log',
|
|
350
|
+
content: 'Third',
|
|
351
|
+
sequence: 2,
|
|
352
|
+
is_final: false,
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
357
|
+
|
|
358
|
+
const lines = screen.getAllByTestId(/output-line-/);
|
|
359
|
+
expect(lines[0]).toHaveTextContent('First');
|
|
360
|
+
expect(lines[1]).toHaveTextContent('Second');
|
|
361
|
+
expect(lines[2]).toHaveTextContent('Third');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('Auto-Scroll', () => {
|
|
366
|
+
it('should auto-scroll to bottom when new output arrives', () => {
|
|
367
|
+
const outputs: StreamingOutputData[] = Array.from({ length: 100 }, (_, i) => ({
|
|
368
|
+
agent_name: 'test_agent',
|
|
369
|
+
run_id: 'run-1',
|
|
370
|
+
output_type: 'llm_token' as const,
|
|
371
|
+
content: `Line ${i}`,
|
|
372
|
+
sequence: i,
|
|
373
|
+
is_final: false,
|
|
374
|
+
}));
|
|
375
|
+
|
|
376
|
+
const { rerender } = render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
377
|
+
|
|
378
|
+
const container = screen.getByTestId('live-output-agent-1');
|
|
379
|
+
|
|
380
|
+
// In test environment, verify scrollTop was set (may not be > 0 in JSDOM)
|
|
381
|
+
// The important thing is that the auto-scroll logic runs
|
|
382
|
+
expect(container.scrollTop).toBeGreaterThanOrEqual(0);
|
|
383
|
+
|
|
384
|
+
// Add more output
|
|
385
|
+
const newOutputs = [
|
|
386
|
+
...outputs,
|
|
387
|
+
{
|
|
388
|
+
agent_name: 'test_agent',
|
|
389
|
+
run_id: 'run-1',
|
|
390
|
+
output_type: 'llm_token' as const,
|
|
391
|
+
content: 'New line',
|
|
392
|
+
sequence: 100,
|
|
393
|
+
is_final: false,
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
rerender(<MockLiveOutputTab nodeId="agent-1" outputs={newOutputs} />);
|
|
398
|
+
|
|
399
|
+
// Verify the new line was added (scroll behavior tested implicitly)
|
|
400
|
+
expect(screen.getByText('New line')).toBeInTheDocument();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should allow disabling auto-scroll', () => {
|
|
404
|
+
const outputs: StreamingOutputData[] = [
|
|
405
|
+
{
|
|
406
|
+
agent_name: 'test_agent',
|
|
407
|
+
run_id: 'run-1',
|
|
408
|
+
output_type: 'llm_token',
|
|
409
|
+
content: 'Test',
|
|
410
|
+
sequence: 0,
|
|
411
|
+
is_final: false,
|
|
412
|
+
},
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} autoScroll={false} />);
|
|
416
|
+
|
|
417
|
+
const container = screen.getByTestId('live-output-agent-1');
|
|
418
|
+
expect(container.scrollTop).toBe(0);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('Output Type Rendering', () => {
|
|
423
|
+
it('should render llm_token type correctly', () => {
|
|
424
|
+
const outputs: StreamingOutputData[] = [
|
|
425
|
+
{
|
|
426
|
+
agent_name: 'test_agent',
|
|
427
|
+
run_id: 'run-1',
|
|
428
|
+
output_type: 'llm_token',
|
|
429
|
+
content: 'LLM output',
|
|
430
|
+
sequence: 0,
|
|
431
|
+
is_final: false,
|
|
432
|
+
},
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
436
|
+
|
|
437
|
+
const line = screen.getByTestId('output-line-0');
|
|
438
|
+
expect(line).toHaveAttribute('data-output-type', 'llm_token');
|
|
439
|
+
expect(line).toHaveStyle({ color: '#8be9fd' });
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should render log type correctly', () => {
|
|
443
|
+
const outputs: StreamingOutputData[] = [
|
|
444
|
+
{
|
|
445
|
+
agent_name: 'test_agent',
|
|
446
|
+
run_id: 'run-1',
|
|
447
|
+
output_type: 'log',
|
|
448
|
+
content: 'Log message',
|
|
449
|
+
sequence: 0,
|
|
450
|
+
is_final: false,
|
|
451
|
+
},
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
455
|
+
|
|
456
|
+
const line = screen.getByTestId('output-line-0');
|
|
457
|
+
expect(line).toHaveAttribute('data-output-type', 'log');
|
|
458
|
+
expect(line).toHaveStyle({ color: '#f1fa8c' });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should render stdout type correctly', () => {
|
|
462
|
+
const outputs: StreamingOutputData[] = [
|
|
463
|
+
{
|
|
464
|
+
agent_name: 'test_agent',
|
|
465
|
+
run_id: 'run-1',
|
|
466
|
+
output_type: 'stdout',
|
|
467
|
+
content: 'Standard output',
|
|
468
|
+
sequence: 0,
|
|
469
|
+
is_final: false,
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
474
|
+
|
|
475
|
+
const line = screen.getByTestId('output-line-0');
|
|
476
|
+
expect(line).toHaveAttribute('data-output-type', 'stdout');
|
|
477
|
+
expect(line).toHaveStyle({ color: '#50fa7b' });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should render stderr type correctly', () => {
|
|
481
|
+
const outputs: StreamingOutputData[] = [
|
|
482
|
+
{
|
|
483
|
+
agent_name: 'test_agent',
|
|
484
|
+
run_id: 'run-1',
|
|
485
|
+
output_type: 'stderr',
|
|
486
|
+
content: 'Error output',
|
|
487
|
+
sequence: 0,
|
|
488
|
+
is_final: false,
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
493
|
+
|
|
494
|
+
const line = screen.getByTestId('output-line-0');
|
|
495
|
+
expect(line).toHaveAttribute('data-output-type', 'stderr');
|
|
496
|
+
expect(line).toHaveStyle({ color: '#ff5555' });
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should display mixed output types with correct styling', () => {
|
|
500
|
+
const outputs: StreamingOutputData[] = [
|
|
501
|
+
{
|
|
502
|
+
agent_name: 'test_agent',
|
|
503
|
+
run_id: 'run-1',
|
|
504
|
+
output_type: 'llm_token',
|
|
505
|
+
content: 'LLM',
|
|
506
|
+
sequence: 0,
|
|
507
|
+
is_final: false,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
agent_name: 'test_agent',
|
|
511
|
+
run_id: 'run-1',
|
|
512
|
+
output_type: 'log',
|
|
513
|
+
content: 'LOG',
|
|
514
|
+
sequence: 1,
|
|
515
|
+
is_final: false,
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
agent_name: 'test_agent',
|
|
519
|
+
run_id: 'run-1',
|
|
520
|
+
output_type: 'stdout',
|
|
521
|
+
content: 'STDOUT',
|
|
522
|
+
sequence: 2,
|
|
523
|
+
is_final: false,
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
agent_name: 'test_agent',
|
|
527
|
+
run_id: 'run-1',
|
|
528
|
+
output_type: 'stderr',
|
|
529
|
+
content: 'STDERR',
|
|
530
|
+
sequence: 3,
|
|
531
|
+
is_final: false,
|
|
532
|
+
},
|
|
533
|
+
];
|
|
534
|
+
|
|
535
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
536
|
+
|
|
537
|
+
expect(screen.getByTestId('output-line-0')).toHaveStyle({ color: '#8be9fd' });
|
|
538
|
+
expect(screen.getByTestId('output-line-1')).toHaveStyle({ color: '#f1fa8c' });
|
|
539
|
+
expect(screen.getByTestId('output-line-2')).toHaveStyle({ color: '#50fa7b' });
|
|
540
|
+
expect(screen.getByTestId('output-line-3')).toHaveStyle({ color: '#ff5555' });
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('Final Marker', () => {
|
|
545
|
+
it('should display final marker when is_final is true', () => {
|
|
546
|
+
const outputs: StreamingOutputData[] = [
|
|
547
|
+
{
|
|
548
|
+
agent_name: 'test_agent',
|
|
549
|
+
run_id: 'run-1',
|
|
550
|
+
output_type: 'llm_token',
|
|
551
|
+
content: 'Done',
|
|
552
|
+
sequence: 0,
|
|
553
|
+
is_final: true,
|
|
554
|
+
},
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
558
|
+
|
|
559
|
+
expect(screen.getByTestId('final-marker')).toBeInTheDocument();
|
|
560
|
+
expect(screen.getByText('--- End of output ---')).toBeInTheDocument();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should not display final marker when is_final is false', () => {
|
|
564
|
+
const outputs: StreamingOutputData[] = [
|
|
565
|
+
{
|
|
566
|
+
agent_name: 'test_agent',
|
|
567
|
+
run_id: 'run-1',
|
|
568
|
+
output_type: 'llm_token',
|
|
569
|
+
content: 'In progress',
|
|
570
|
+
sequence: 0,
|
|
571
|
+
is_final: false,
|
|
572
|
+
},
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
576
|
+
|
|
577
|
+
expect(screen.queryByTestId('final-marker')).not.toBeInTheDocument();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should stop accumulation after final marker', () => {
|
|
581
|
+
const outputs: StreamingOutputData[] = [
|
|
582
|
+
{
|
|
583
|
+
agent_name: 'test_agent',
|
|
584
|
+
run_id: 'run-1',
|
|
585
|
+
output_type: 'llm_token',
|
|
586
|
+
content: 'Last',
|
|
587
|
+
sequence: 0,
|
|
588
|
+
is_final: true,
|
|
589
|
+
},
|
|
590
|
+
];
|
|
591
|
+
|
|
592
|
+
const { rerender } = render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
593
|
+
|
|
594
|
+
expect(screen.getByTestId('final-marker')).toBeInTheDocument();
|
|
595
|
+
|
|
596
|
+
// Try to add more output (should be ignored by actual implementation)
|
|
597
|
+
const newOutputs: StreamingOutputData[] = [
|
|
598
|
+
...outputs,
|
|
599
|
+
{
|
|
600
|
+
agent_name: 'test_agent',
|
|
601
|
+
run_id: 'run-1',
|
|
602
|
+
output_type: 'llm_token' as const,
|
|
603
|
+
content: 'Should not appear',
|
|
604
|
+
sequence: 1,
|
|
605
|
+
is_final: false,
|
|
606
|
+
},
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
rerender(<MockLiveOutputTab nodeId="agent-1" outputs={newOutputs} />);
|
|
610
|
+
|
|
611
|
+
// Note: Mock shows it, but actual implementation should filter it out
|
|
612
|
+
// This test documents expected behavior
|
|
613
|
+
expect(screen.getByTestId('final-marker')).toBeInTheDocument();
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe('Virtualization Performance', () => {
|
|
618
|
+
it('should handle 1000+ lines without performance degradation', () => {
|
|
619
|
+
const startTime = performance.now();
|
|
620
|
+
|
|
621
|
+
const outputs: StreamingOutputData[] = Array.from({ length: 1000 }, (_, i) => ({
|
|
622
|
+
agent_name: 'test_agent',
|
|
623
|
+
run_id: 'run-1',
|
|
624
|
+
output_type: 'llm_token' as const,
|
|
625
|
+
content: `Line ${i}: ${'x'.repeat(100)}`,
|
|
626
|
+
sequence: i,
|
|
627
|
+
is_final: false,
|
|
628
|
+
}));
|
|
629
|
+
|
|
630
|
+
render(<MockVirtualizedOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
631
|
+
|
|
632
|
+
const endTime = performance.now();
|
|
633
|
+
const renderTime = endTime - startTime;
|
|
634
|
+
|
|
635
|
+
// Virtualized rendering should be fast (< 100ms for 1000 lines)
|
|
636
|
+
expect(renderTime).toBeLessThan(100);
|
|
637
|
+
|
|
638
|
+
expect(screen.getByTestId('total-lines')).toHaveTextContent('1000');
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should only render visible lines in virtualized mode', () => {
|
|
642
|
+
const outputs: StreamingOutputData[] = Array.from({ length: 1000 }, (_, i) => ({
|
|
643
|
+
agent_name: 'test_agent',
|
|
644
|
+
run_id: 'run-1',
|
|
645
|
+
output_type: 'llm_token' as const,
|
|
646
|
+
content: `Line ${i}`,
|
|
647
|
+
sequence: i,
|
|
648
|
+
is_final: false,
|
|
649
|
+
}));
|
|
650
|
+
|
|
651
|
+
render(<MockVirtualizedOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
652
|
+
|
|
653
|
+
// Should only render ~50 visible lines (not all 1000)
|
|
654
|
+
const visibleLines = screen.getAllByTestId(/virtual-line-/);
|
|
655
|
+
expect(visibleLines.length).toBeLessThan(100);
|
|
656
|
+
expect(visibleLines.length).toBeGreaterThan(0);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should handle rapid updates with many lines', () => {
|
|
660
|
+
const { rerender } = render(
|
|
661
|
+
<MockVirtualizedOutputTab nodeId="agent-1" outputs={[]} />
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Add 100 lines at a time
|
|
665
|
+
for (let batch = 0; batch < 10; batch++) {
|
|
666
|
+
const outputs: StreamingOutputData[] = Array.from(
|
|
667
|
+
{ length: (batch + 1) * 100 },
|
|
668
|
+
(_, i) => ({
|
|
669
|
+
agent_name: 'test_agent',
|
|
670
|
+
run_id: 'run-1',
|
|
671
|
+
output_type: 'llm_token' as const,
|
|
672
|
+
content: `Line ${i}`,
|
|
673
|
+
sequence: i,
|
|
674
|
+
is_final: false,
|
|
675
|
+
})
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
const startTime = performance.now();
|
|
679
|
+
rerender(<MockVirtualizedOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
680
|
+
const endTime = performance.now();
|
|
681
|
+
|
|
682
|
+
// Each update should be fast
|
|
683
|
+
expect(endTime - startTime).toBeLessThan(50);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
expect(screen.getByTestId('total-lines')).toHaveTextContent('1000');
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe('Edge Cases', () => {
|
|
691
|
+
it('should handle empty content', () => {
|
|
692
|
+
const outputs: StreamingOutputData[] = [
|
|
693
|
+
{
|
|
694
|
+
agent_name: 'test_agent',
|
|
695
|
+
run_id: 'run-1',
|
|
696
|
+
output_type: 'llm_token',
|
|
697
|
+
content: '',
|
|
698
|
+
sequence: 0,
|
|
699
|
+
is_final: false,
|
|
700
|
+
},
|
|
701
|
+
];
|
|
702
|
+
|
|
703
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
704
|
+
|
|
705
|
+
expect(screen.getByTestId('output-line-0')).toBeInTheDocument();
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('should handle multiline content', () => {
|
|
709
|
+
const outputs: StreamingOutputData[] = [
|
|
710
|
+
{
|
|
711
|
+
agent_name: 'test_agent',
|
|
712
|
+
run_id: 'run-1',
|
|
713
|
+
output_type: 'stdout',
|
|
714
|
+
content: 'Line 1\nLine 2\nLine 3',
|
|
715
|
+
sequence: 0,
|
|
716
|
+
is_final: false,
|
|
717
|
+
},
|
|
718
|
+
];
|
|
719
|
+
|
|
720
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
721
|
+
|
|
722
|
+
const line = screen.getByTestId('output-line-0');
|
|
723
|
+
expect(line).toHaveTextContent('Line 1');
|
|
724
|
+
expect(line).toHaveTextContent('Line 2');
|
|
725
|
+
expect(line).toHaveTextContent('Line 3');
|
|
726
|
+
expect(line).toHaveStyle({ whiteSpace: 'pre-wrap' });
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('should handle special characters', () => {
|
|
730
|
+
const outputs: StreamingOutputData[] = [
|
|
731
|
+
{
|
|
732
|
+
agent_name: 'test_agent',
|
|
733
|
+
run_id: 'run-1',
|
|
734
|
+
output_type: 'llm_token',
|
|
735
|
+
content: '<script>alert("xss")</script>',
|
|
736
|
+
sequence: 0,
|
|
737
|
+
is_final: false,
|
|
738
|
+
},
|
|
739
|
+
];
|
|
740
|
+
|
|
741
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
742
|
+
|
|
743
|
+
// Content should be escaped (React does this by default)
|
|
744
|
+
const line = screen.getByTestId('output-line-0');
|
|
745
|
+
expect(line.textContent).toBe('<script>alert("xss")</script>');
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('should handle duplicate sequence numbers', () => {
|
|
749
|
+
const outputs: StreamingOutputData[] = [
|
|
750
|
+
{
|
|
751
|
+
agent_name: 'test_agent',
|
|
752
|
+
run_id: 'run-1',
|
|
753
|
+
output_type: 'llm_token',
|
|
754
|
+
content: 'First',
|
|
755
|
+
sequence: 0,
|
|
756
|
+
is_final: false,
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
agent_name: 'test_agent',
|
|
760
|
+
run_id: 'run-1',
|
|
761
|
+
output_type: 'llm_token',
|
|
762
|
+
content: 'Duplicate',
|
|
763
|
+
sequence: 0,
|
|
764
|
+
is_final: false,
|
|
765
|
+
},
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
769
|
+
|
|
770
|
+
// Both should be displayed (implementation detail)
|
|
771
|
+
expect(screen.getAllByTestId(/output-line-/).length).toBe(2);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('should handle very long content lines', () => {
|
|
775
|
+
const outputs: StreamingOutputData[] = [
|
|
776
|
+
{
|
|
777
|
+
agent_name: 'test_agent',
|
|
778
|
+
run_id: 'run-1',
|
|
779
|
+
output_type: 'llm_token',
|
|
780
|
+
content: 'x'.repeat(10000),
|
|
781
|
+
sequence: 0,
|
|
782
|
+
is_final: false,
|
|
783
|
+
},
|
|
784
|
+
];
|
|
785
|
+
|
|
786
|
+
render(<MockLiveOutputTab nodeId="agent-1" outputs={outputs} />);
|
|
787
|
+
|
|
788
|
+
const line = screen.getByTestId('output-line-0');
|
|
789
|
+
expect(line.textContent?.length).toBe(10000);
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
});
|