ruby_reactor 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +10 -2
- data/README.md +72 -3
- data/Rakefile +27 -2
- data/documentation/images/failed_order_processing.png +0 -0
- data/documentation/images/payment_workflow.png +0 -0
- data/documentation/interrupts.md +161 -0
- data/gui/.gitignore +24 -0
- data/gui/README.md +73 -0
- data/gui/eslint.config.js +23 -0
- data/gui/index.html +13 -0
- data/gui/package-lock.json +5925 -0
- data/gui/package.json +46 -0
- data/gui/postcss.config.js +6 -0
- data/gui/public/vite.svg +1 -0
- data/gui/src/App.css +42 -0
- data/gui/src/App.tsx +51 -0
- data/gui/src/assets/react.svg +1 -0
- data/gui/src/components/DagVisualizer.tsx +424 -0
- data/gui/src/components/Dashboard.tsx +163 -0
- data/gui/src/components/ErrorBoundary.tsx +47 -0
- data/gui/src/components/ReactorDetail.tsx +135 -0
- data/gui/src/components/StepInspector.tsx +492 -0
- data/gui/src/components/__tests__/DagVisualizer.test.tsx +140 -0
- data/gui/src/components/__tests__/ReactorDetail.test.tsx +111 -0
- data/gui/src/components/__tests__/StepInspector.test.tsx +408 -0
- data/gui/src/globals.d.ts +7 -0
- data/gui/src/index.css +14 -0
- data/gui/src/lib/utils.ts +13 -0
- data/gui/src/main.tsx +14 -0
- data/gui/src/test/setup.ts +11 -0
- data/gui/tailwind.config.js +11 -0
- data/gui/tsconfig.app.json +28 -0
- data/gui/tsconfig.json +7 -0
- data/gui/tsconfig.node.json +26 -0
- data/gui/vite.config.ts +8 -0
- data/gui/vitest.config.ts +13 -0
- data/lib/ruby_reactor/async_router.rb +6 -2
- data/lib/ruby_reactor/context.rb +35 -9
- data/lib/ruby_reactor/dependency_graph.rb +2 -0
- data/lib/ruby_reactor/dsl/compose_builder.rb +8 -0
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +48 -0
- data/lib/ruby_reactor/dsl/interrupt_step_config.rb +21 -0
- data/lib/ruby_reactor/dsl/map_builder.rb +8 -0
- data/lib/ruby_reactor/dsl/reactor.rb +12 -0
- data/lib/ruby_reactor/dsl/step_builder.rb +4 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +60 -27
- data/lib/ruby_reactor/executor/graph_manager.rb +2 -0
- data/lib/ruby_reactor/executor/result_handler.rb +117 -39
- data/lib/ruby_reactor/executor/retry_manager.rb +1 -0
- data/lib/ruby_reactor/executor/step_executor.rb +38 -4
- data/lib/ruby_reactor/executor.rb +86 -13
- data/lib/ruby_reactor/interrupt_result.rb +20 -0
- data/lib/ruby_reactor/map/collector.rb +0 -2
- data/lib/ruby_reactor/map/element_executor.rb +3 -0
- data/lib/ruby_reactor/map/execution.rb +28 -1
- data/lib/ruby_reactor/map/helpers.rb +44 -6
- data/lib/ruby_reactor/reactor.rb +187 -1
- data/lib/ruby_reactor/registry.rb +25 -0
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +1 -1
- data/lib/ruby_reactor/step/compose_step.rb +22 -6
- data/lib/ruby_reactor/step/map_step.rb +30 -3
- data/lib/ruby_reactor/storage/adapter.rb +32 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +154 -11
- data/lib/ruby_reactor/utils/code_extractor.rb +31 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +206 -0
- data/lib/ruby_reactor/web/application.rb +53 -0
- data/lib/ruby_reactor/web/config.ru +5 -0
- data/lib/ruby_reactor/web/public/assets/index-VdeLgH9k.js +19 -0
- data/lib/ruby_reactor/web/public/assets/index-_z-6BvuM.css +1 -0
- data/lib/ruby_reactor/web/public/index.html +14 -0
- data/lib/ruby_reactor/web/public/vite.svg +1 -0
- data/lib/ruby_reactor.rb +94 -28
- data/llms-full.txt +66 -0
- data/llms.txt +7 -0
- metadata +63 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import ReactorDetail from '../ReactorDetail.tsx';
|
|
4
|
+
import useSWR from 'swr';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('swr');
|
|
8
|
+
vi.mock('react-router-dom', () => ({
|
|
9
|
+
useParams: () => ({ id: 'test-reactor-123' }),
|
|
10
|
+
useNavigate: () => vi.fn(),
|
|
11
|
+
Link: ({ children }: { children: React.ReactNode }) => <a>{children}</a>
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock child components to isolate tests
|
|
15
|
+
vi.mock('../DagVisualizer.tsx', () => ({
|
|
16
|
+
default: ({ reactorStatus, error }: any) => (
|
|
17
|
+
<div data-testid="dag-visualizer">
|
|
18
|
+
<span data-testid="dag-status">{reactorStatus}</span>
|
|
19
|
+
<span data-testid="dag-error">{error?.message}</span>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../StepInspector.tsx', () => ({
|
|
25
|
+
default: ({ error }: any) => (
|
|
26
|
+
<div data-testid="step-inspector">
|
|
27
|
+
<span data-testid="inspector-error">{error?.message}</span>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe('ReactorDetail', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders failure status correctly', () => {
|
|
38
|
+
const mockError = {
|
|
39
|
+
message: "Something went wrong",
|
|
40
|
+
step_name: "step2",
|
|
41
|
+
backtrace: ["line 1", "line 2"]
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const mockData = {
|
|
45
|
+
id: "test-reactor-123",
|
|
46
|
+
class: "TestReactor",
|
|
47
|
+
status: "failed",
|
|
48
|
+
error: mockError,
|
|
49
|
+
inputs: {},
|
|
50
|
+
structure: {},
|
|
51
|
+
steps: []
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Mock useSWR response
|
|
55
|
+
(useSWR as any).mockReturnValue({
|
|
56
|
+
data: mockData,
|
|
57
|
+
error: null,
|
|
58
|
+
isLoading: false,
|
|
59
|
+
mutate: vi.fn()
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
render(<ReactorDetail />);
|
|
63
|
+
|
|
64
|
+
// 1. Check Status Badge
|
|
65
|
+
const statusBadges = screen.getAllByText('failed');
|
|
66
|
+
// Find the one that is the badge (has class text-red-400), not the one in the graph
|
|
67
|
+
const badge = statusBadges.find(el => el.classList.contains('text-red-400'));
|
|
68
|
+
expect(badge).toBeInTheDocument();
|
|
69
|
+
|
|
70
|
+
// 2. Check Failure Banner
|
|
71
|
+
expect(screen.getByText('Workflow Failed')).toBeInTheDocument();
|
|
72
|
+
// "Something went wrong" appears in multiple places (banner, mock props)
|
|
73
|
+
expect(screen.getAllByText('Something went wrong').length).toBeGreaterThan(0);
|
|
74
|
+
|
|
75
|
+
// 3. Check Props passed to DagVisualizer
|
|
76
|
+
expect(screen.getByTestId('dag-status')).toHaveTextContent('failed');
|
|
77
|
+
expect(screen.getByTestId('dag-error')).toHaveTextContent('Something went wrong');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders success status correctly', () => {
|
|
81
|
+
const mockData = {
|
|
82
|
+
id: "test-reactor-123",
|
|
83
|
+
class: "TestReactor",
|
|
84
|
+
status: "completed",
|
|
85
|
+
error: null,
|
|
86
|
+
inputs: {},
|
|
87
|
+
structure: {},
|
|
88
|
+
steps: []
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
(useSWR as any).mockReturnValue({
|
|
92
|
+
data: mockData,
|
|
93
|
+
error: null,
|
|
94
|
+
isLoading: false,
|
|
95
|
+
mutate: vi.fn()
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
render(<ReactorDetail />);
|
|
99
|
+
|
|
100
|
+
// 1. Check Status Badge
|
|
101
|
+
const statusBadges = screen.getAllByText('completed');
|
|
102
|
+
const badge = statusBadges.find(el => el.classList.contains('text-emerald-400'));
|
|
103
|
+
expect(badge).toBeInTheDocument();
|
|
104
|
+
|
|
105
|
+
expect(screen.queryByText('Workflow Failed')).not.toBeInTheDocument();
|
|
106
|
+
|
|
107
|
+
// 2. Check Props passed to DagVisualizer
|
|
108
|
+
expect(screen.getByTestId('dag-status')).toHaveTextContent('completed');
|
|
109
|
+
expect(screen.getByTestId('dag-error')).toBeEmptyDOMElement();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import StepInspector from '../StepInspector.tsx';
|
|
4
|
+
|
|
5
|
+
describe('StepInspector', () => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
stepName: 'test_step',
|
|
8
|
+
structure: {
|
|
9
|
+
test_step: { type: 'step', depends_on: [] }
|
|
10
|
+
},
|
|
11
|
+
results: {},
|
|
12
|
+
trace: [],
|
|
13
|
+
inputs: {},
|
|
14
|
+
stepAttempts: {
|
|
15
|
+
test_step: 1
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it('should not show retries if there is only 1 attempt', () => {
|
|
20
|
+
render(<StepInspector {...defaultProps} />);
|
|
21
|
+
expect(screen.queryByText(/Retries:/)).not.toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should show Retries: 1 if there are 2 attempts', () => {
|
|
25
|
+
const props = {
|
|
26
|
+
...defaultProps,
|
|
27
|
+
stepAttempts: {
|
|
28
|
+
test_step: 2
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
render(<StepInspector {...props} />);
|
|
32
|
+
expect(screen.getByText('Retries: 1')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should show Retries: 2 if there are 3 attempts', () => {
|
|
36
|
+
const props = {
|
|
37
|
+
...defaultProps,
|
|
38
|
+
stepAttempts: {
|
|
39
|
+
test_step: 3
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
render(<StepInspector {...props} />);
|
|
43
|
+
expect(screen.getByText('Retries: 2')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should not show retries if stepAttempts is missing for the step', () => {
|
|
47
|
+
const props = {
|
|
48
|
+
...defaultProps,
|
|
49
|
+
stepAttempts: {}
|
|
50
|
+
};
|
|
51
|
+
render(<StepInspector {...props} />);
|
|
52
|
+
expect(screen.queryByText(/Retries:/)).not.toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Compensation History', () => {
|
|
56
|
+
const propsWithUndo = {
|
|
57
|
+
...defaultProps,
|
|
58
|
+
stepName: null, // Global View
|
|
59
|
+
trace: [
|
|
60
|
+
{ type: 'run', step: 'step1', result: 'result1' },
|
|
61
|
+
{ type: 'run', step: 'step2', result: 'result2' },
|
|
62
|
+
{ type: 'compensate', step: 'step2', result: 'compensated step2' },
|
|
63
|
+
{ type: 'undo', step: 'step1', result: 'undid step1' }
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it('should show compensation history in execution order (chronological)', () => {
|
|
68
|
+
render(<StepInspector {...propsWithUndo} />);
|
|
69
|
+
|
|
70
|
+
// Verify labels order (use exact match to avoid matching result blocks)
|
|
71
|
+
const labels = screen.queryAllByText(/^step\d$/, { selector: 'span' });
|
|
72
|
+
expect(labels[0]).toHaveTextContent('step2');
|
|
73
|
+
expect(labels[1]).toHaveTextContent('step1');
|
|
74
|
+
|
|
75
|
+
// Verify results are also in order
|
|
76
|
+
expect(screen.getByText(/"compensated step2"/)).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByText(/"undid step1"/)).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should show the result of the undo/compensate operation', () => {
|
|
81
|
+
render(<StepInspector {...propsWithUndo} />);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByText(/"compensated step2"/)).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText(/"undid step1"/)).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should show COMPENSATE and UNDO labels', () => {
|
|
88
|
+
render(<StepInspector {...propsWithUndo} />);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText('compensate')).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByText('undo')).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Nested Reactor Support', () => {
|
|
96
|
+
const mockStructure = {
|
|
97
|
+
sub_reactor: {
|
|
98
|
+
type: 'compose',
|
|
99
|
+
nested_structure: {
|
|
100
|
+
inner_step: { type: 'step' }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const mockComposedContexts = {
|
|
106
|
+
sub_reactor: {
|
|
107
|
+
context: {
|
|
108
|
+
value: {
|
|
109
|
+
intermediate_results: {
|
|
110
|
+
inner_step: 'inner_result_value',
|
|
111
|
+
deep_reactor: 'deep_done'
|
|
112
|
+
},
|
|
113
|
+
execution_trace: [
|
|
114
|
+
{ step: 'inner_step', type: 'run', arguments: { val: 456 } }
|
|
115
|
+
],
|
|
116
|
+
composed_contexts: {
|
|
117
|
+
deep_reactor: {
|
|
118
|
+
context: {
|
|
119
|
+
value: {
|
|
120
|
+
intermediate_results: {
|
|
121
|
+
deep_step: 'deep_result_value'
|
|
122
|
+
},
|
|
123
|
+
execution_trace: [
|
|
124
|
+
{ step: 'deep_step', type: 'run', arguments: { deep_val: 789 } }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
it('correctly resolves and displays results for a nested step', () => {
|
|
136
|
+
render(
|
|
137
|
+
<StepInspector
|
|
138
|
+
stepName="sub_reactor.inner_step"
|
|
139
|
+
structure={mockStructure}
|
|
140
|
+
results={{}}
|
|
141
|
+
inputs={{}}
|
|
142
|
+
trace={[]}
|
|
143
|
+
composedContexts={mockComposedContexts}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Should show step name (base name only)
|
|
148
|
+
expect(screen.getByText('inner_step')).toBeInTheDocument();
|
|
149
|
+
|
|
150
|
+
// Should show resolved result
|
|
151
|
+
expect(screen.getByText(/"inner_result_value"/)).toBeInTheDocument();
|
|
152
|
+
|
|
153
|
+
// Should show resolved arguments from nested trace
|
|
154
|
+
expect(screen.getByText(/456/)).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('correctly resolves and displays results for a deeply nested step', () => {
|
|
158
|
+
render(
|
|
159
|
+
<StepInspector
|
|
160
|
+
stepName="sub_reactor.deep_reactor.deep_step"
|
|
161
|
+
structure={{
|
|
162
|
+
sub_reactor: {
|
|
163
|
+
type: 'compose',
|
|
164
|
+
nested_structure: {
|
|
165
|
+
deep_reactor: {
|
|
166
|
+
type: 'compose',
|
|
167
|
+
nested_structure: {
|
|
168
|
+
deep_step: { type: 'step' }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
results={{}}
|
|
175
|
+
inputs={{}}
|
|
176
|
+
trace={[]}
|
|
177
|
+
composedContexts={mockComposedContexts}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(screen.getByText('deep_step')).toBeInTheDocument();
|
|
182
|
+
expect(screen.getByText(/"deep_result_value"/)).toBeInTheDocument();
|
|
183
|
+
expect(screen.getByText(/789/)).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('identifies failures in nested contexts', () => {
|
|
187
|
+
const failedNestedContexts = {
|
|
188
|
+
sub_reactor: {
|
|
189
|
+
context: {
|
|
190
|
+
value: {
|
|
191
|
+
failure_reason: { step_name: 'inner_step', message: 'Nested failure' },
|
|
192
|
+
intermediate_results: {},
|
|
193
|
+
execution_trace: []
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
render(
|
|
200
|
+
<StepInspector
|
|
201
|
+
stepName="sub_reactor.inner_step"
|
|
202
|
+
structure={mockStructure}
|
|
203
|
+
results={{}}
|
|
204
|
+
inputs={{}}
|
|
205
|
+
trace={[]}
|
|
206
|
+
composedContexts={failedNestedContexts}
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(screen.getByText('Failure Details')).toBeInTheDocument();
|
|
211
|
+
expect(screen.getByText('Nested failure')).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('Recursive Compensation History', () => {
|
|
216
|
+
const mockComposedContexts = {
|
|
217
|
+
math_operation: {
|
|
218
|
+
context: {
|
|
219
|
+
value: {
|
|
220
|
+
execution_trace: [
|
|
221
|
+
{ type: 'compensate', step: 'do_something', result: 'comp_val' },
|
|
222
|
+
{ type: 'undo', step: 'multiply', result: 'undo_val' }
|
|
223
|
+
],
|
|
224
|
+
undo_stack: [
|
|
225
|
+
{ step_name: 'pending_step' }
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
child_reactor: {
|
|
231
|
+
context: {
|
|
232
|
+
value: {
|
|
233
|
+
execution_trace: [
|
|
234
|
+
{ type: 'undo', step: 'wait_for', result: 'child_undo' }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const props = {
|
|
242
|
+
...defaultProps,
|
|
243
|
+
stepName: null, // Global View
|
|
244
|
+
trace: [
|
|
245
|
+
{ type: 'compensate', step: 'math_operation', result: 'math_res' },
|
|
246
|
+
{ type: 'undo', step: 'child_reactor', result: 'child_res' }
|
|
247
|
+
],
|
|
248
|
+
composedContexts: mockComposedContexts
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
it('should display grouped blocks for each reactor', () => {
|
|
252
|
+
render(<StepInspector {...props} />);
|
|
253
|
+
|
|
254
|
+
expect(screen.getByText('Root Reactor')).toBeInTheDocument();
|
|
255
|
+
// math_operation and child_reactor appear twice (header and step)
|
|
256
|
+
expect(screen.getAllByText('math_operation').length).toBe(2);
|
|
257
|
+
expect(screen.getAllByText('child_reactor').length).toBe(2);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should recursively collect executed undo and compensate events', () => {
|
|
261
|
+
render(<StepInspector {...props} />);
|
|
262
|
+
|
|
263
|
+
// Root level (found twice as discussed)
|
|
264
|
+
expect(screen.getAllByText('math_operation').length).toBe(2);
|
|
265
|
+
expect(screen.getAllByText('child_reactor').length).toBe(2);
|
|
266
|
+
|
|
267
|
+
// Nested math_operation level
|
|
268
|
+
expect(screen.getByText('do_something')).toBeInTheDocument();
|
|
269
|
+
expect(screen.getByText('multiply')).toBeInTheDocument();
|
|
270
|
+
expect(screen.getByText(/"comp_val"/)).toBeInTheDocument();
|
|
271
|
+
expect(screen.getByText(/"undo_val"/)).toBeInTheDocument();
|
|
272
|
+
|
|
273
|
+
// Nested child_reactor level
|
|
274
|
+
expect(screen.getByText('wait_for')).toBeInTheDocument();
|
|
275
|
+
expect(screen.getByText(/"child_undo"/)).toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should recursively collect pending items from undo_stack', () => {
|
|
279
|
+
render(<StepInspector {...props} />);
|
|
280
|
+
|
|
281
|
+
expect(screen.getByText('pending_step')).toBeInTheDocument();
|
|
282
|
+
// Verify it's marked as pending
|
|
283
|
+
expect(screen.getAllByText('pending').length).toBeGreaterThan(0);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('Failure Information', () => {
|
|
288
|
+
const failedProps = {
|
|
289
|
+
...defaultProps,
|
|
290
|
+
stepName: 'failed_step',
|
|
291
|
+
composedContexts: {
|
|
292
|
+
failed_step: {
|
|
293
|
+
context: {
|
|
294
|
+
value: {
|
|
295
|
+
status: 'failed',
|
|
296
|
+
failure_reason: {
|
|
297
|
+
message: 'Something went wrong',
|
|
298
|
+
step_name: 'failed_step',
|
|
299
|
+
exception_class: 'CustomError',
|
|
300
|
+
backtrace: [
|
|
301
|
+
'line 1',
|
|
302
|
+
'line 2',
|
|
303
|
+
'line 3',
|
|
304
|
+
'line 4',
|
|
305
|
+
'line 5',
|
|
306
|
+
'line 6',
|
|
307
|
+
'line 7'
|
|
308
|
+
]
|
|
309
|
+
},
|
|
310
|
+
intermediate_results: {},
|
|
311
|
+
execution_trace: []
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
error: {
|
|
317
|
+
message: 'Something went wrong',
|
|
318
|
+
step_name: 'failed_step',
|
|
319
|
+
exception_class: 'CustomError',
|
|
320
|
+
backtrace: [
|
|
321
|
+
'line 1',
|
|
322
|
+
'line 2',
|
|
323
|
+
'line 3',
|
|
324
|
+
'line 4',
|
|
325
|
+
'line 5',
|
|
326
|
+
'line 6',
|
|
327
|
+
'line 7'
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
it('should display the exception class', () => {
|
|
333
|
+
render(<StepInspector {...failedProps} />);
|
|
334
|
+
expect(screen.getByText('CustomError')).toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should truncate the backtrace to 5 lines by default', () => {
|
|
338
|
+
render(<StepInspector {...failedProps} />);
|
|
339
|
+
|
|
340
|
+
const backtraceText = screen.getByText(/line 1/);
|
|
341
|
+
expect(backtraceText.textContent).toContain('line 5');
|
|
342
|
+
expect(backtraceText.textContent).not.toContain('line 6');
|
|
343
|
+
|
|
344
|
+
expect(screen.getByText(/Show More/)).toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should expand the backtrace when clicking Show More', () => {
|
|
348
|
+
render(<StepInspector {...failedProps} />);
|
|
349
|
+
|
|
350
|
+
const showMoreButton = screen.getByText(/Show More/);
|
|
351
|
+
fireEvent.click(showMoreButton);
|
|
352
|
+
|
|
353
|
+
const backtraceText = screen.getByText(/line 1/);
|
|
354
|
+
expect(backtraceText.textContent).toContain('line 7');
|
|
355
|
+
expect(screen.getByText(/Show Less/)).toBeInTheDocument();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('Validation Errors', () => {
|
|
360
|
+
const validationErrorProps = {
|
|
361
|
+
...defaultProps,
|
|
362
|
+
stepName: 'validation_step',
|
|
363
|
+
composedContexts: {
|
|
364
|
+
validation_step: {
|
|
365
|
+
context: {
|
|
366
|
+
value: {
|
|
367
|
+
status: 'failed',
|
|
368
|
+
failure_reason: {
|
|
369
|
+
message: 'Validation failed',
|
|
370
|
+
step_name: 'validation_step',
|
|
371
|
+
validation_errors: {
|
|
372
|
+
field1: ['Error 1', 'Error 2'],
|
|
373
|
+
field2: 'Single Error'
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
intermediate_results: {},
|
|
377
|
+
execution_trace: []
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
error: {
|
|
383
|
+
message: 'Validation failed',
|
|
384
|
+
step_name: 'validation_step',
|
|
385
|
+
validation_errors: {
|
|
386
|
+
field1: ['Error 1', 'Error 2'],
|
|
387
|
+
field2: 'Single Error'
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
it('should display validation errors section', () => {
|
|
393
|
+
render(<StepInspector {...validationErrorProps} />);
|
|
394
|
+
expect(screen.getByText('Validation Errors')).toBeInTheDocument();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should display field names and error messages', () => {
|
|
398
|
+
render(<StepInspector {...validationErrorProps} />);
|
|
399
|
+
|
|
400
|
+
expect(screen.getByText('field1:')).toBeInTheDocument();
|
|
401
|
+
expect(screen.getByText('- Error 1')).toBeInTheDocument();
|
|
402
|
+
expect(screen.getByText('- Error 2')).toBeInTheDocument();
|
|
403
|
+
|
|
404
|
+
expect(screen.getByText('field2:')).toBeInTheDocument();
|
|
405
|
+
expect(screen.getByText('- Single Error')).toBeInTheDocument();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
});
|
data/gui/src/index.css
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
|
|
4
|
+
@theme {
|
|
5
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
6
|
+
--color-slate-950: #020617;
|
|
7
|
+
--color-slate-900: #0f172a;
|
|
8
|
+
--color-slate-800: #1e293b;
|
|
9
|
+
--color-indigo-500: #6366f1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
@apply bg-slate-950 text-slate-50 font-sans antialiased;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function apiUrl(path: string) {
|
|
9
|
+
const base = window.RUBY_REACTOR_BASE || '/';
|
|
10
|
+
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
11
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
12
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
13
|
+
}
|
data/gui/src/main.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './index.css'
|
|
4
|
+
import App from './App.tsx'
|
|
5
|
+
|
|
6
|
+
import { ErrorBoundary } from './components/ErrorBoundary.tsx'
|
|
7
|
+
|
|
8
|
+
createRoot(document.getElementById('root')!).render(
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<ErrorBoundary>
|
|
11
|
+
<App />
|
|
12
|
+
</ErrorBoundary>
|
|
13
|
+
</StrictMode>,
|
|
14
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
data/gui/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
data/gui/vite.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vite'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
test: {
|
|
8
|
+
globals: true,
|
|
9
|
+
environment: 'jsdom',
|
|
10
|
+
setupFiles: './src/test/setup.ts',
|
|
11
|
+
css: false,
|
|
12
|
+
},
|
|
13
|
+
})
|