@marktoflow/gui 2.0.0-alpha.4 → 2.0.0-alpha.5
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.
- package/.turbo/turbo-build.log +24 -8
- package/README.md +11 -1
- package/dist/client/assets/index-CM44OayM.js +704 -0
- package/dist/client/assets/index-CM44OayM.js.map +1 -0
- package/dist/client/assets/index-Dru63gi6.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/{server/index.js → index.js} +22 -1
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/executions.js +125 -0
- package/dist/server/routes/executions.js.map +1 -0
- package/dist/server/{server/routes → routes}/workflows.js +37 -1
- package/dist/server/routes/workflows.js.map +1 -0
- package/dist/server/{server/services → services}/WorkflowService.js +158 -15
- package/dist/server/services/WorkflowService.js.map +1 -0
- package/dist/server/{server/websocket → websocket}/index.js +12 -0
- package/dist/server/{server/websocket → websocket}/index.js.map +1 -1
- package/marktoflow-gui-2.0.0-alpha.5.tgz +0 -0
- package/package.json +19 -5
- package/scripts/flatten-dist.js +69 -0
- package/src/client/components/Canvas/Canvas.tsx +3 -1
- package/src/client/components/Canvas/ExecutionOverlay.tsx +120 -32
- package/src/client/components/Canvas/ForEachNode.tsx +27 -3
- package/src/client/components/Canvas/IfElseNode.tsx +22 -7
- package/src/client/components/Canvas/NodeContextMenu.tsx +8 -4
- package/src/client/components/Canvas/ParallelNode.tsx +25 -8
- package/src/client/components/Canvas/SwitchNode.tsx +41 -20
- package/src/client/components/Canvas/Toolbar.tsx +59 -21
- package/src/client/components/Canvas/TransformNode.tsx +9 -0
- package/src/client/components/Canvas/WhileNode.tsx +35 -3
- package/src/client/components/Debug/VariableInspector.tsx +148 -0
- package/src/client/components/Prompt/PromptInput.tsx +3 -1
- package/src/client/components/Settings/ProviderSwitcher.tsx +228 -0
- package/src/client/components/Sidebar/ImportDialog.tsx +257 -0
- package/src/client/components/Sidebar/Sidebar.tsx +21 -2
- package/src/client/components/common/KeyboardShortcuts.tsx +8 -2
- package/src/client/stores/agentStore.ts +109 -0
- package/src/client/stores/executionStore.ts +64 -2
- package/src/client/stores/workflowStore.ts +10 -2
- package/src/client/styles/globals.css +106 -0
- package/src/client/utils/platform.ts +46 -0
- package/src/client/utils/workflowToGraph.ts +245 -21
- package/src/server/index.ts +24 -1
- package/src/server/routes/executions.ts +136 -0
- package/src/server/routes/workflows.ts +42 -1
- package/src/server/services/WorkflowService.ts +176 -16
- package/src/server/websocket/index.ts +13 -0
- package/tests/unit/ForEachNode.test.tsx +96 -6
- package/tests/unit/IfElseNode.test.tsx +47 -0
- package/tests/unit/ParallelNode.test.tsx +80 -0
- package/tests/unit/SwitchNode.test.tsx +75 -0
- package/tests/unit/WhileNode.test.tsx +12 -8
- package/tests/unit/agentStore.test.ts +218 -0
- package/tests/unit/executionStore.test.ts +40 -0
- package/tests/unit/platform.test.ts +118 -0
- package/tests/unit/workflowToGraph.test.ts +22 -0
- package/dist/client/assets/index-C90Y_aBX.js +0 -678
- package/dist/client/assets/index-C90Y_aBX.js.map +0 -1
- package/dist/client/assets/index-CRWeQ3NN.css +0 -1
- package/dist/server/server/index.js.map +0 -1
- package/dist/server/server/routes/workflows.js.map +0 -1
- package/dist/server/server/services/WorkflowService.js.map +0 -1
- /package/dist/server/{server/routes → routes}/ai.js +0 -0
- /package/dist/server/{server/routes → routes}/ai.js.map +0 -0
- /package/dist/server/{server/routes → routes}/execute.js +0 -0
- /package/dist/server/{server/routes → routes}/execute.js.map +0 -0
- /package/dist/server/{server/routes → routes}/tools.js +0 -0
- /package/dist/server/{server/routes → routes}/tools.js.map +0 -0
- /package/dist/server/{server/services → services}/AIService.js +0 -0
- /package/dist/server/{server/services → services}/AIService.js.map +0 -0
- /package/dist/server/{server/services → services}/FileWatcher.js +0 -0
- /package/dist/server/{server/services → services}/FileWatcher.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/claude-code-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/claude-code-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/claude-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/claude-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/codex-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/codex-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/copilot-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/copilot-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/demo-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/demo-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/index.js +0 -0
- /package/dist/server/{server/services → services}/agents/index.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/ollama-provider.js +0 -0
- /package/dist/server/{server/services → services}/agents/ollama-provider.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/prompts.js +0 -0
- /package/dist/server/{server/services → services}/agents/prompts.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/registry.js +0 -0
- /package/dist/server/{server/services → services}/agents/registry.js.map +0 -0
- /package/dist/server/{server/services → services}/agents/types.js +0 -0
- /package/dist/server/{server/services → services}/agents/types.js.map +0 -0
- /package/dist/{server/shared → shared}/constants.js +0 -0
- /package/dist/{server/shared → shared}/constants.js.map +0 -0
- /package/dist/{server/shared → shared}/types.js +0 -0
- /package/dist/{server/shared → shared}/types.js.map +0 -0
|
@@ -185,4 +185,51 @@ describe('IfElseNode', () => {
|
|
|
185
185
|
expect(screen.getByText(complexCondition)).toBeInTheDocument();
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
|
+
|
|
189
|
+
describe('skipped branch indicators', () => {
|
|
190
|
+
it('should show SKIP badge on skipped then branch', () => {
|
|
191
|
+
renderNode({ skippedBranch: 'then' });
|
|
192
|
+
|
|
193
|
+
expect(screen.getByText('SKIP')).toBeInTheDocument();
|
|
194
|
+
const thenBranch = screen.getByText('✓ Then');
|
|
195
|
+
expect(thenBranch).toHaveClass('bg-gray-500/20', 'text-gray-400');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should show SKIP badge on skipped else branch', () => {
|
|
199
|
+
renderNode({ skippedBranch: 'else' });
|
|
200
|
+
|
|
201
|
+
expect(screen.getByText('SKIP')).toBeInTheDocument();
|
|
202
|
+
const elseBranch = screen.getByText('✗ Else');
|
|
203
|
+
expect(elseBranch).toHaveClass('bg-gray-500/20', 'text-gray-400');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should highlight active branch with ring when one is active', () => {
|
|
207
|
+
renderNode({ activeBranch: 'then' });
|
|
208
|
+
|
|
209
|
+
const thenBranch = screen.getByText('✓ Then');
|
|
210
|
+
expect(thenBranch).toHaveClass('ring-1', 'ring-green-400/50');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should not show SKIP when no branch is skipped', () => {
|
|
214
|
+
renderNode({ skippedBranch: null });
|
|
215
|
+
|
|
216
|
+
expect(screen.queryByText('SKIP')).not.toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('completed and failed states', () => {
|
|
221
|
+
it('should show completed class on node', () => {
|
|
222
|
+
renderNode({ status: 'completed' });
|
|
223
|
+
|
|
224
|
+
const node = screen.getByText('Check Count').closest('.control-flow-node');
|
|
225
|
+
expect(node).toHaveClass('completed');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should show failed class on node', () => {
|
|
229
|
+
renderNode({ status: 'failed' });
|
|
230
|
+
|
|
231
|
+
const node = screen.getByText('Check Count').closest('.control-flow-node');
|
|
232
|
+
expect(node).toHaveClass('failed');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
188
235
|
});
|
|
@@ -261,4 +261,84 @@ describe('ParallelNode', () => {
|
|
|
261
261
|
expect(screen.getByText('0')).toBeInTheDocument();
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
|
+
|
|
265
|
+
describe('failed branches', () => {
|
|
266
|
+
it('should highlight failed branches in red', () => {
|
|
267
|
+
renderNode({
|
|
268
|
+
failedBranches: ['branch-1'],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const jiraBranch = screen.getByText('Jira');
|
|
272
|
+
expect(jiraBranch).toHaveClass('bg-red-500/30', 'text-red-200');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should prioritize failed over completed and active', () => {
|
|
276
|
+
renderNode({
|
|
277
|
+
activeBranches: ['branch-1'],
|
|
278
|
+
completedBranches: ['branch-1'],
|
|
279
|
+
failedBranches: ['branch-1'],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const jiraBranch = screen.getByText('Jira');
|
|
283
|
+
expect(jiraBranch).toHaveClass('bg-red-500/30', 'text-red-200');
|
|
284
|
+
expect(jiraBranch).not.toHaveClass('bg-green-500/30');
|
|
285
|
+
expect(jiraBranch).not.toHaveClass('animate-pulse');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('max concurrent exceeded', () => {
|
|
290
|
+
it('should show rate limiting warning when maxConcurrentExceeded is true', () => {
|
|
291
|
+
renderNode({
|
|
292
|
+
maxConcurrent: 2,
|
|
293
|
+
maxConcurrentExceeded: true,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(screen.getByText('Rate limiting active')).toBeInTheDocument();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should highlight max concurrent value in yellow when exceeded', () => {
|
|
300
|
+
renderNode({
|
|
301
|
+
maxConcurrent: 2,
|
|
302
|
+
maxConcurrentExceeded: true,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const maxValue = screen.getByText('2');
|
|
306
|
+
expect(maxValue).toHaveClass('text-yellow-300');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should not show warning when maxConcurrentExceeded is false', () => {
|
|
310
|
+
renderNode({
|
|
311
|
+
maxConcurrent: 2,
|
|
312
|
+
maxConcurrentExceeded: false,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(screen.queryByText('Rate limiting active')).not.toBeInTheDocument();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should not highlight max concurrent value when not exceeded', () => {
|
|
319
|
+
renderNode({
|
|
320
|
+
maxConcurrent: 2,
|
|
321
|
+
maxConcurrentExceeded: false,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const maxValue = screen.getByText('2');
|
|
325
|
+
expect(maxValue).not.toHaveClass('text-yellow-300');
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('completed and failed states', () => {
|
|
330
|
+
it('should show completed class on node', () => {
|
|
331
|
+
renderNode({ status: 'completed' });
|
|
332
|
+
|
|
333
|
+
const node = screen.getByText('Fetch Data').closest('.control-flow-node');
|
|
334
|
+
expect(node).toHaveClass('completed');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should show failed class on node', () => {
|
|
338
|
+
renderNode({ status: 'failed' });
|
|
339
|
+
|
|
340
|
+
const node = screen.getByText('Fetch Data').closest('.control-flow-node');
|
|
341
|
+
expect(node).toHaveClass('failed');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
264
344
|
});
|
|
@@ -249,4 +249,79 @@ describe('SwitchNode', () => {
|
|
|
249
249
|
expect(screen.getByText('0 cases + default')).toBeInTheDocument();
|
|
250
250
|
});
|
|
251
251
|
});
|
|
252
|
+
|
|
253
|
+
describe('skipped branches', () => {
|
|
254
|
+
it('should show SKIPPED badge on skipped case', () => {
|
|
255
|
+
renderNode({
|
|
256
|
+
skippedBranches: ['high', 'medium'],
|
|
257
|
+
activeCase: 'critical'
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const skippedBadges = screen.getAllByText('SKIPPED');
|
|
261
|
+
expect(skippedBadges).toHaveLength(2);
|
|
262
|
+
|
|
263
|
+
const highCase = screen.getByText('high');
|
|
264
|
+
expect(highCase).toHaveClass('line-through', 'text-gray-400');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should not show SKIPPED when no branches are skipped', () => {
|
|
268
|
+
renderNode({ skippedBranches: [] });
|
|
269
|
+
|
|
270
|
+
expect(screen.queryByText('SKIPPED')).not.toBeInTheDocument();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should show active case with ring highlight', () => {
|
|
274
|
+
renderNode({ activeCase: 'critical' });
|
|
275
|
+
|
|
276
|
+
const criticalCase = screen.getByText('critical');
|
|
277
|
+
expect(criticalCase).toHaveClass('ring-1', 'ring-purple-400/50');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('handle positioning', () => {
|
|
282
|
+
it('should render output handles for all visible cases', () => {
|
|
283
|
+
const { container } = renderNode({
|
|
284
|
+
cases: { critical: {}, high: {}, medium: {} },
|
|
285
|
+
hasDefault: true
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Should have 4 handles: 3 cases + 1 default
|
|
289
|
+
const handles = container.querySelectorAll('.react-flow__handle-bottom');
|
|
290
|
+
expect(handles.length).toBeGreaterThanOrEqual(3);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should position handles evenly to avoid overlap', () => {
|
|
294
|
+
const { container } = renderNode({
|
|
295
|
+
cases: { case1: {}, case2: {}, case3: {}, case4: {} },
|
|
296
|
+
hasDefault: true
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const handles = container.querySelectorAll('.react-flow__handle-bottom');
|
|
300
|
+
|
|
301
|
+
// Each handle should have a unique position
|
|
302
|
+
const positions = Array.from(handles).map(h =>
|
|
303
|
+
(h as HTMLElement).style.left
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// All positions should be different
|
|
307
|
+
const uniquePositions = new Set(positions);
|
|
308
|
+
expect(uniquePositions.size).toBe(positions.length);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('completed and failed states', () => {
|
|
313
|
+
it('should show completed class on node', () => {
|
|
314
|
+
renderNode({ status: 'completed' });
|
|
315
|
+
|
|
316
|
+
const node = screen.getByText('Route By Severity').closest('.control-flow-node');
|
|
317
|
+
expect(node).toHaveClass('completed');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should show failed class on node', () => {
|
|
321
|
+
renderNode({ status: 'failed' });
|
|
322
|
+
|
|
323
|
+
const node = screen.getByText('Route By Severity').closest('.control-flow-node');
|
|
324
|
+
expect(node).toHaveClass('failed');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
252
327
|
});
|
|
@@ -123,8 +123,9 @@ describe('WhileNode', () => {
|
|
|
123
123
|
maxIterations: 10,
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
const progressBar = container.querySelector('.bg-orange-400');
|
|
127
|
-
expect(progressBar).
|
|
126
|
+
const progressBar = container.querySelector('.bg-orange-400, .bg-orange-500');
|
|
127
|
+
expect(progressBar).toBeInTheDocument();
|
|
128
|
+
expect(progressBar).toHaveAttribute('style', expect.stringContaining('50%'));
|
|
128
129
|
});
|
|
129
130
|
|
|
130
131
|
it('should display 0% progress when currentIteration is 0', () => {
|
|
@@ -133,8 +134,9 @@ describe('WhileNode', () => {
|
|
|
133
134
|
maxIterations: 10,
|
|
134
135
|
});
|
|
135
136
|
|
|
136
|
-
const progressBar = container.querySelector('.bg-orange-400');
|
|
137
|
-
expect(progressBar).
|
|
137
|
+
const progressBar = container.querySelector('.bg-orange-400, .bg-orange-500');
|
|
138
|
+
expect(progressBar).toBeInTheDocument();
|
|
139
|
+
expect(progressBar).toHaveAttribute('style', expect.stringContaining('0%'));
|
|
138
140
|
});
|
|
139
141
|
|
|
140
142
|
it('should display 100% progress when at max iterations', () => {
|
|
@@ -143,8 +145,9 @@ describe('WhileNode', () => {
|
|
|
143
145
|
maxIterations: 10,
|
|
144
146
|
});
|
|
145
147
|
|
|
146
|
-
const progressBar = container.querySelector('.bg-orange-400');
|
|
147
|
-
expect(progressBar).
|
|
148
|
+
const progressBar = container.querySelector('.bg-orange-400, .bg-orange-500');
|
|
149
|
+
expect(progressBar).toBeInTheDocument();
|
|
150
|
+
expect(progressBar).toHaveAttribute('style', expect.stringContaining('100%'));
|
|
148
151
|
});
|
|
149
152
|
|
|
150
153
|
it('should not display progress when currentIteration is undefined', () => {
|
|
@@ -219,8 +222,9 @@ describe('WhileNode', () => {
|
|
|
219
222
|
});
|
|
220
223
|
|
|
221
224
|
expect(screen.getByText('25 / 50')).toBeInTheDocument();
|
|
222
|
-
const progressBar = container.querySelector('.bg-orange-400');
|
|
223
|
-
expect(progressBar).
|
|
225
|
+
const progressBar = container.querySelector('.bg-orange-400, .bg-orange-500');
|
|
226
|
+
expect(progressBar).toBeInTheDocument();
|
|
227
|
+
expect(progressBar).toHaveAttribute('style', expect.stringContaining('50%'));
|
|
224
228
|
});
|
|
225
229
|
});
|
|
226
230
|
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for agent store
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { useAgentStore } from '../../src/client/stores/agentStore';
|
|
7
|
+
|
|
8
|
+
// Mock fetch
|
|
9
|
+
global.fetch = vi.fn();
|
|
10
|
+
|
|
11
|
+
describe('Agent Store', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Reset store
|
|
14
|
+
useAgentStore.setState({
|
|
15
|
+
providers: [],
|
|
16
|
+
activeProviderId: null,
|
|
17
|
+
isLoading: false,
|
|
18
|
+
error: null,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Reset fetch mock
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('loadProviders', () => {
|
|
26
|
+
it('should load providers successfully', async () => {
|
|
27
|
+
const mockProviders = [
|
|
28
|
+
{
|
|
29
|
+
id: 'copilot',
|
|
30
|
+
name: 'GitHub Copilot',
|
|
31
|
+
status: 'ready',
|
|
32
|
+
isActive: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'claude',
|
|
36
|
+
name: 'Claude Code',
|
|
37
|
+
status: 'needs_config',
|
|
38
|
+
isActive: false,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
43
|
+
ok: true,
|
|
44
|
+
json: async () => ({
|
|
45
|
+
activeProvider: 'copilot',
|
|
46
|
+
providers: mockProviders,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const { loadProviders } = useAgentStore.getState();
|
|
51
|
+
await loadProviders();
|
|
52
|
+
|
|
53
|
+
const state = useAgentStore.getState();
|
|
54
|
+
expect(state.providers).toEqual(mockProviders);
|
|
55
|
+
expect(state.activeProviderId).toBe('copilot');
|
|
56
|
+
expect(state.isLoading).toBe(false);
|
|
57
|
+
expect(state.error).toBe(null);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle loading errors', async () => {
|
|
61
|
+
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
|
|
62
|
+
|
|
63
|
+
const { loadProviders } = useAgentStore.getState();
|
|
64
|
+
await loadProviders();
|
|
65
|
+
|
|
66
|
+
const state = useAgentStore.getState();
|
|
67
|
+
expect(state.error).toBe('Network error');
|
|
68
|
+
expect(state.isLoading).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle HTTP errors', async () => {
|
|
72
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
73
|
+
ok: false,
|
|
74
|
+
status: 500,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const { loadProviders } = useAgentStore.getState();
|
|
78
|
+
await loadProviders();
|
|
79
|
+
|
|
80
|
+
const state = useAgentStore.getState();
|
|
81
|
+
expect(state.error).toBe('Failed to load providers');
|
|
82
|
+
expect(state.isLoading).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should set loading state during fetch', async () => {
|
|
86
|
+
let resolvePromise: any;
|
|
87
|
+
const promise = new Promise((resolve) => {
|
|
88
|
+
resolvePromise = resolve;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
(global.fetch as any).mockReturnValueOnce(promise);
|
|
92
|
+
|
|
93
|
+
const { loadProviders } = useAgentStore.getState();
|
|
94
|
+
const loadPromise = loadProviders();
|
|
95
|
+
|
|
96
|
+
// Check loading state
|
|
97
|
+
expect(useAgentStore.getState().isLoading).toBe(true);
|
|
98
|
+
|
|
99
|
+
// Resolve promise
|
|
100
|
+
resolvePromise({
|
|
101
|
+
ok: true,
|
|
102
|
+
json: async () => ({ activeProvider: null, providers: [] }),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await loadPromise;
|
|
106
|
+
expect(useAgentStore.getState().isLoading).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('setProvider', () => {
|
|
111
|
+
it('should set provider successfully', async () => {
|
|
112
|
+
const mockProviders = [
|
|
113
|
+
{
|
|
114
|
+
id: 'copilot',
|
|
115
|
+
name: 'GitHub Copilot',
|
|
116
|
+
status: 'ready',
|
|
117
|
+
isActive: false,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'claude',
|
|
121
|
+
name: 'Claude Code',
|
|
122
|
+
status: 'ready',
|
|
123
|
+
isActive: true,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
128
|
+
ok: true,
|
|
129
|
+
json: async () => ({
|
|
130
|
+
success: true,
|
|
131
|
+
status: {
|
|
132
|
+
activeProvider: 'claude',
|
|
133
|
+
providers: mockProviders,
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const { setProvider } = useAgentStore.getState();
|
|
139
|
+
const result = await setProvider('claude');
|
|
140
|
+
|
|
141
|
+
expect(result).toBe(true);
|
|
142
|
+
const state = useAgentStore.getState();
|
|
143
|
+
expect(state.activeProviderId).toBe('claude');
|
|
144
|
+
expect(state.providers).toEqual(mockProviders);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should send configuration with request', async () => {
|
|
148
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
149
|
+
ok: true,
|
|
150
|
+
json: async () => ({
|
|
151
|
+
success: true,
|
|
152
|
+
status: { activeProvider: 'claude', providers: [] },
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const { setProvider } = useAgentStore.getState();
|
|
157
|
+
await setProvider('claude', {
|
|
158
|
+
apiKey: 'test-key',
|
|
159
|
+
baseUrl: 'https://api.test.com',
|
|
160
|
+
model: 'claude-3',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
164
|
+
'/api/ai/providers/claude',
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: {
|
|
168
|
+
'Content-Type': 'application/json',
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
apiKey: 'test-key',
|
|
172
|
+
baseUrl: 'https://api.test.com',
|
|
173
|
+
model: 'claude-3',
|
|
174
|
+
}),
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle set provider errors', async () => {
|
|
180
|
+
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
|
|
181
|
+
|
|
182
|
+
const { setProvider } = useAgentStore.getState();
|
|
183
|
+
const result = await setProvider('claude');
|
|
184
|
+
|
|
185
|
+
expect(result).toBe(false);
|
|
186
|
+
const state = useAgentStore.getState();
|
|
187
|
+
expect(state.error).toBe('Network error');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle HTTP errors', async () => {
|
|
191
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
192
|
+
ok: false,
|
|
193
|
+
json: async () => ({ message: 'Provider not found' }),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const { setProvider } = useAgentStore.getState();
|
|
197
|
+
const result = await setProvider('invalid');
|
|
198
|
+
|
|
199
|
+
expect(result).toBe(false);
|
|
200
|
+
const state = useAgentStore.getState();
|
|
201
|
+
expect(state.error).toBe('Provider not found');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('refreshStatus', () => {
|
|
206
|
+
it('should call loadProviders', async () => {
|
|
207
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
208
|
+
ok: true,
|
|
209
|
+
json: async () => ({ activeProvider: null, providers: [] }),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const { refreshStatus } = useAgentStore.getState();
|
|
213
|
+
await refreshStatus();
|
|
214
|
+
|
|
215
|
+
expect(global.fetch).toHaveBeenCalledWith('/api/ai/providers');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -101,6 +101,46 @@ describe('executionStore', () => {
|
|
|
101
101
|
const run = state.runs.find(r => r.id === runId);
|
|
102
102
|
expect(run?.steps[0].error).toBe('Something went wrong');
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
it('should record step inputs', () => {
|
|
106
|
+
const { startExecution, updateStepStatus } = useExecutionStore.getState();
|
|
107
|
+
|
|
108
|
+
const runId = startExecution('wf-1', 'Test');
|
|
109
|
+
const inputs = { channel: '#general', message: 'Hello' };
|
|
110
|
+
updateStepStatus(runId, 'step-1', 'running', undefined, undefined, undefined, inputs);
|
|
111
|
+
|
|
112
|
+
const state = useExecutionStore.getState();
|
|
113
|
+
const run = state.runs.find(r => r.id === runId);
|
|
114
|
+
expect(run?.steps[0].inputs).toEqual(inputs);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should record step output and output variable', () => {
|
|
118
|
+
const { startExecution, updateStepStatus } = useExecutionStore.getState();
|
|
119
|
+
|
|
120
|
+
const runId = startExecution('wf-1', 'Test');
|
|
121
|
+
const output = { ts: '1234567890.123456', ok: true };
|
|
122
|
+
updateStepStatus(runId, 'step-1', 'running');
|
|
123
|
+
updateStepStatus(runId, 'step-1', 'completed', output, undefined, 'result');
|
|
124
|
+
|
|
125
|
+
const state = useExecutionStore.getState();
|
|
126
|
+
const run = state.runs.find(r => r.id === runId);
|
|
127
|
+
expect(run?.steps[0].output).toEqual(output);
|
|
128
|
+
expect(run?.steps[0].outputVariable).toBe('result');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should preserve inputs when updating step status', () => {
|
|
132
|
+
const { startExecution, updateStepStatus } = useExecutionStore.getState();
|
|
133
|
+
|
|
134
|
+
const runId = startExecution('wf-1', 'Test');
|
|
135
|
+
const inputs = { foo: 'bar' };
|
|
136
|
+
updateStepStatus(runId, 'step-1', 'running', undefined, undefined, undefined, inputs);
|
|
137
|
+
updateStepStatus(runId, 'step-1', 'completed', { result: 'success' });
|
|
138
|
+
|
|
139
|
+
const state = useExecutionStore.getState();
|
|
140
|
+
const run = state.runs.find(r => r.id === runId);
|
|
141
|
+
expect(run?.steps[0].inputs).toEqual(inputs);
|
|
142
|
+
expect(run?.steps[0].output).toEqual({ result: 'success' });
|
|
143
|
+
});
|
|
104
144
|
});
|
|
105
145
|
|
|
106
146
|
describe('completeExecution', () => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for platform detection utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { isMac, getModKey, getShortcutDisplay, isModKeyPressed } from '../../src/client/utils/platform';
|
|
7
|
+
|
|
8
|
+
describe('Platform Utilities', () => {
|
|
9
|
+
describe('isMac', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Reset navigator mock
|
|
12
|
+
vi.stubGlobal('navigator', {
|
|
13
|
+
platform: 'MacIntel',
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return true for Mac platforms', () => {
|
|
18
|
+
vi.stubGlobal('navigator', { platform: 'MacIntel' });
|
|
19
|
+
expect(isMac()).toBe(true);
|
|
20
|
+
|
|
21
|
+
vi.stubGlobal('navigator', { platform: 'MacPPC' });
|
|
22
|
+
expect(isMac()).toBe(true);
|
|
23
|
+
|
|
24
|
+
vi.stubGlobal('navigator', { platform: 'Mac68K' });
|
|
25
|
+
expect(isMac()).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false for non-Mac platforms', () => {
|
|
29
|
+
vi.stubGlobal('navigator', { platform: 'Win32' });
|
|
30
|
+
expect(isMac()).toBe(false);
|
|
31
|
+
|
|
32
|
+
vi.stubGlobal('navigator', { platform: 'Linux x86_64' });
|
|
33
|
+
expect(isMac()).toBe(false);
|
|
34
|
+
|
|
35
|
+
vi.stubGlobal('navigator', { platform: 'Linux' });
|
|
36
|
+
expect(isMac()).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return false when navigator is undefined', () => {
|
|
40
|
+
vi.stubGlobal('navigator', undefined);
|
|
41
|
+
expect(isMac()).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('getModKey', () => {
|
|
46
|
+
it('should return ⌘ on Mac', () => {
|
|
47
|
+
vi.stubGlobal('navigator', { platform: 'MacIntel' });
|
|
48
|
+
expect(getModKey()).toBe('⌘');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return Ctrl on Windows', () => {
|
|
52
|
+
vi.stubGlobal('navigator', { platform: 'Win32' });
|
|
53
|
+
expect(getModKey()).toBe('Ctrl');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return Ctrl on Linux', () => {
|
|
57
|
+
vi.stubGlobal('navigator', { platform: 'Linux x86_64' });
|
|
58
|
+
expect(getModKey()).toBe('Ctrl');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('getShortcutDisplay', () => {
|
|
63
|
+
it('should convert ⌘ to Ctrl on Windows', () => {
|
|
64
|
+
vi.stubGlobal('navigator', { platform: 'Win32' });
|
|
65
|
+
expect(getShortcutDisplay('⌘+S')).toBe('Ctrl+S');
|
|
66
|
+
expect(getShortcutDisplay('⌘+Z')).toBe('Ctrl+Z');
|
|
67
|
+
expect(getShortcutDisplay('⌘+⇧+Z')).toBe('Ctrl+⇧+Z');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should keep ⌘ on Mac', () => {
|
|
71
|
+
vi.stubGlobal('navigator', { platform: 'MacIntel' });
|
|
72
|
+
expect(getShortcutDisplay('⌘+S')).toBe('⌘+S');
|
|
73
|
+
expect(getShortcutDisplay('⌘+Z')).toBe('⌘+Z');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should convert Cmd to appropriate modifier', () => {
|
|
77
|
+
vi.stubGlobal('navigator', { platform: 'Win32' });
|
|
78
|
+
expect(getShortcutDisplay('Cmd+S')).toBe('Ctrl+S');
|
|
79
|
+
|
|
80
|
+
vi.stubGlobal('navigator', { platform: 'MacIntel' });
|
|
81
|
+
expect(getShortcutDisplay('Cmd+S')).toBe('⌘+S');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle multiple occurrences', () => {
|
|
85
|
+
vi.stubGlobal('navigator', { platform: 'Win32' });
|
|
86
|
+
expect(getShortcutDisplay('⌘+⌘')).toBe('Ctrl+Ctrl');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('isModKeyPressed', () => {
|
|
91
|
+
it('should check metaKey on Mac', () => {
|
|
92
|
+
vi.stubGlobal('navigator', { platform: 'MacIntel' });
|
|
93
|
+
|
|
94
|
+
const event = { metaKey: true, ctrlKey: false } as KeyboardEvent;
|
|
95
|
+
expect(isModKeyPressed(event)).toBe(true);
|
|
96
|
+
|
|
97
|
+
const event2 = { metaKey: false, ctrlKey: true } as KeyboardEvent;
|
|
98
|
+
expect(isModKeyPressed(event2)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should check ctrlKey on Windows', () => {
|
|
102
|
+
vi.stubGlobal('navigator', { platform: 'Win32' });
|
|
103
|
+
|
|
104
|
+
const event = { metaKey: false, ctrlKey: true } as KeyboardEvent;
|
|
105
|
+
expect(isModKeyPressed(event)).toBe(true);
|
|
106
|
+
|
|
107
|
+
const event2 = { metaKey: true, ctrlKey: false } as KeyboardEvent;
|
|
108
|
+
expect(isModKeyPressed(event2)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should check ctrlKey on Linux', () => {
|
|
112
|
+
vi.stubGlobal('navigator', { platform: 'Linux x86_64' });
|
|
113
|
+
|
|
114
|
+
const event = { metaKey: false, ctrlKey: true } as KeyboardEvent;
|
|
115
|
+
expect(isModKeyPressed(event)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -49,6 +49,28 @@ describe('workflowToGraph', () => {
|
|
|
49
49
|
expect(stepNode?.data.action).toBe('github.pulls.get');
|
|
50
50
|
expect(stepNode?.data.status).toBe('pending');
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
it('should space steps with 180px vertical spacing', () => {
|
|
54
|
+
const workflow = {
|
|
55
|
+
metadata: { id: 'test-1', name: 'Test Workflow' },
|
|
56
|
+
steps: [
|
|
57
|
+
{ id: 'step-1', name: 'Step 1', action: 'test', inputs: {} },
|
|
58
|
+
{ id: 'step-2', name: 'Step 2', action: 'test', inputs: {} },
|
|
59
|
+
{ id: 'step-3', name: 'Step 3', action: 'test', inputs: {} },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const { nodes } = workflowToGraph(workflow);
|
|
64
|
+
const stepNodes = nodes.filter(n => n.type === 'step');
|
|
65
|
+
|
|
66
|
+
// Check vertical spacing between steps
|
|
67
|
+
const step1Y = stepNodes[0].position.y;
|
|
68
|
+
const step2Y = stepNodes[1].position.y;
|
|
69
|
+
const step3Y = stepNodes[2].position.y;
|
|
70
|
+
|
|
71
|
+
expect(step2Y - step1Y).toBe(180);
|
|
72
|
+
expect(step3Y - step2Y).toBe(180);
|
|
73
|
+
});
|
|
52
74
|
});
|
|
53
75
|
|
|
54
76
|
describe('trigger nodes', () => {
|