import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { WorkflowClient, CreateWorkflowInput, UpdateWorkflowInput } from '../../../desktop/src/store/workflowStore'; // Mock client with all workflow methods const mockClient: WorkflowClient = { listWorkflows: vi.fn(), createWorkflow: vi.fn(), updateWorkflow: vi.fn(), deleteWorkflow: vi.fn(), executeWorkflow: vi.fn(), cancelWorkflow: vi.fn(), listWorkflowRuns: vi.fn(), }; function resetMocks() { vi.clearAllMocks(); mockClient.listWorkflows = vi.fn().mockResolvedValue({ workflows: [ { id: 'wf_1', name: 'Data Pipeline', steps: 3, description: 'ETL pipeline', createdAt: '2026-03-14T10:00:00Z' }, { id: 'wf_2', name: 'Report Generator', steps: 5, description: 'Weekly reports' }, ], }); mockClient.createWorkflow = vi.fn().mockImplementation((workflow: CreateWorkflowInput) => ({ id: 'wf_new', name: workflow.name, })); mockClient.updateWorkflow = vi.fn().mockImplementation((id: string, _updates: UpdateWorkflowInput) => ({ id, name: 'Updated Workflow', })); mockClient.deleteWorkflow = vi.fn().mockResolvedValue({ status: 'deleted' }); mockClient.executeWorkflow = vi.fn().mockImplementation((id: string, _input?: Record) => ({ runId: `run_${id}_123`, status: 'running', })); mockClient.cancelWorkflow = vi.fn().mockResolvedValue({ status: 'cancelled' }); mockClient.listWorkflowRuns = vi.fn().mockResolvedValue({ runs: [ { runId: 'run_wf1_001', status: 'completed', startedAt: '2026-03-14T10:00:00Z', completedAt: '2026-03-14T10:05:00Z' }, { runId: 'run_wf1_002', status: 'running', startedAt: '2026-03-14T11:00:00Z', currentStep: 2, totalSteps: 3 }, ], }); } describe('workflowStore', () => { beforeEach(async () => { vi.resetModules(); resetMocks(); }); describe('initial state', () => { it('initializes with empty workflows and no error', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); // Reset to initial state useWorkflowStore.getState().reset(); const state = useWorkflowStore.getState(); expect(state.workflows).toEqual([]); expect(state.workflowRuns).toEqual({}); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); }); }); describe('client injection', () => { it('accepts a client via setWorkflowStoreClient', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); expect(useWorkflowStore.getState().client).toBe(mockClient); }); }); describe('loadWorkflows', () => { it('loads workflows from the client and updates state', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); expect(mockClient.listWorkflows).toHaveBeenCalledTimes(1); const state = useWorkflowStore.getState(); expect(state.workflows).toHaveLength(2); expect(state.workflows[0]).toMatchObject({ id: 'wf_1', name: 'Data Pipeline', steps: 3, description: 'ETL pipeline', }); expect(state.workflows[1]).toMatchObject({ id: 'wf_2', name: 'Report Generator', steps: 5, }); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); }); it('sets isLoading during load and clears on success', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const loadPromise = useWorkflowStore.getState().loadWorkflows(); await loadPromise; expect(useWorkflowStore.getState().isLoading).toBe(false); }); it('sets error when loadWorkflows fails', async () => { mockClient.listWorkflows = vi.fn().mockRejectedValue(new Error('Network error')); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); const state = useWorkflowStore.getState(); expect(state.error).toBe('Network error'); expect(state.isLoading).toBe(false); expect(state.workflows).toEqual([]); }); it('handles null result from client', async () => { mockClient.listWorkflows = vi.fn().mockResolvedValue(null); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); expect(useWorkflowStore.getState().workflows).toEqual([]); }); }); describe('getWorkflow', () => { it('returns workflow by id', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); const workflow = useWorkflowStore.getState().getWorkflow('wf_1'); expect(workflow).toMatchObject({ id: 'wf_1', name: 'Data Pipeline', }); }); it('returns undefined for unknown id', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); const workflow = useWorkflowStore.getState().getWorkflow('unknown'); expect(workflow).toBeUndefined(); }); }); describe('createWorkflow', () => { it('creates a workflow and adds it to state', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); const input: CreateWorkflowInput = { name: 'New Workflow', description: 'A new workflow', steps: [ { handName: 'echo', params: { message: 'hello' } }, { handName: 'notify', params: { channel: 'email' } }, ], }; const result = await useWorkflowStore.getState().createWorkflow(input); expect(mockClient.createWorkflow).toHaveBeenCalledWith(input); expect(result).toMatchObject({ id: 'wf_new', name: 'New Workflow', steps: 2, description: 'A new workflow', }); const state = useWorkflowStore.getState(); expect(state.workflows).toHaveLength(3); expect(state.workflows.find(w => w.id === 'wf_new')).toBeDefined(); }); it('returns undefined and sets error when create fails', async () => { mockClient.createWorkflow = vi.fn().mockRejectedValue(new Error('Create failed')); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const input: CreateWorkflowInput = { name: 'Failed Workflow', steps: [{ handName: 'echo' }], }; const result = await useWorkflowStore.getState().createWorkflow(input); expect(result).toBeUndefined(); expect(useWorkflowStore.getState().error).toBe('Create failed'); }); it('returns undefined when client returns null', async () => { mockClient.createWorkflow = vi.fn().mockResolvedValue(null); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const input: CreateWorkflowInput = { name: 'Null Workflow', steps: [{ handName: 'echo' }], }; const result = await useWorkflowStore.getState().createWorkflow(input); expect(result).toBeUndefined(); }); }); describe('updateWorkflow', () => { it('updates a workflow and reflects changes in state', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); const updates: UpdateWorkflowInput = { name: 'Updated Pipeline', description: 'Updated description', steps: [ { handName: 'echo' }, { handName: 'notify' }, { handName: 'collector' }, ], }; const result = await useWorkflowStore.getState().updateWorkflow('wf_1', updates); expect(mockClient.updateWorkflow).toHaveBeenCalledWith('wf_1', updates); // The store updates name from updates, not from mock response expect(result?.name).toBe('Updated Pipeline'); const state = useWorkflowStore.getState(); const updated = state.workflows.find(w => w.id === 'wf_1'); expect(updated?.steps).toBe(3); }); it('preserves existing values when partial updates are provided', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); // Only update description, not name or steps const updates: UpdateWorkflowInput = { description: 'New description only', }; await useWorkflowStore.getState().updateWorkflow('wf_1', updates); const state = useWorkflowStore.getState(); const updated = state.workflows.find(w => w.id === 'wf_1'); // Name should be preserved from original (Data Pipeline) // But since mock returns "Updated Workflow", we check the steps are preserved expect(updated?.steps).toBe(3); }); it('sets error when update fails', async () => { mockClient.updateWorkflow = vi.fn().mockRejectedValue(new Error('Update failed')); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const result = await useWorkflowStore.getState().updateWorkflow('wf_1', { name: 'Fail' }); expect(result).toBeUndefined(); expect(useWorkflowStore.getState().error).toBe('Update failed'); }); }); describe('deleteWorkflow', () => { it('deletes a workflow and removes it from state', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); // Add some runs for this workflow useWorkflowStore.setState({ workflowRuns: { wf_1: [{ runId: 'run_1', status: 'completed' }], }, }); await useWorkflowStore.getState().deleteWorkflow('wf_1'); expect(mockClient.deleteWorkflow).toHaveBeenCalledWith('wf_1'); const state = useWorkflowStore.getState(); expect(state.workflows.find(w => w.id === 'wf_1')).toBeUndefined(); expect(state.workflowRuns['wf_1']).toBeUndefined(); }); it('sets error and throws when delete fails', async () => { mockClient.deleteWorkflow = vi.fn().mockRejectedValue(new Error('Delete failed')); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await expect(useWorkflowStore.getState().deleteWorkflow('wf_1')).rejects.toThrow('Delete failed'); expect(useWorkflowStore.getState().error).toBe('Delete failed'); }); }); describe('triggerWorkflow', () => { it('triggers workflow execution and returns run info', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const result = await useWorkflowStore.getState().triggerWorkflow('wf_1', { input: 'data' }); expect(mockClient.executeWorkflow).toHaveBeenCalledWith('wf_1', { input: 'data' }); expect(result).toMatchObject({ runId: 'run_wf_1_123', status: 'running', }); }); it('returns undefined when trigger fails', async () => { mockClient.executeWorkflow = vi.fn().mockRejectedValue(new Error('Trigger failed')); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const result = await useWorkflowStore.getState().triggerWorkflow('wf_1'); expect(result).toBeUndefined(); expect(useWorkflowStore.getState().error).toBe('Trigger failed'); }); it('returns undefined when client returns null', async () => { mockClient.executeWorkflow = vi.fn().mockResolvedValue(null); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const result = await useWorkflowStore.getState().triggerWorkflow('wf_1'); expect(result).toBeUndefined(); }); }); describe('cancelWorkflow', () => { it('cancels a running workflow and refreshes workflows', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); await useWorkflowStore.getState().cancelWorkflow('wf_1', 'run_123'); expect(mockClient.cancelWorkflow).toHaveBeenCalledWith('wf_1', 'run_123'); expect(mockClient.listWorkflows).toHaveBeenCalledTimes(2); // load + refresh }); it('sets error and throws when cancel fails', async () => { mockClient.cancelWorkflow = vi.fn().mockRejectedValue(new Error('Cancel failed')); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await expect(useWorkflowStore.getState().cancelWorkflow('wf_1', 'run_123')).rejects.toThrow('Cancel failed'); expect(useWorkflowStore.getState().error).toBe('Cancel failed'); }); }); describe('loadWorkflowRuns', () => { it('loads runs for a specific workflow', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const runs = await useWorkflowStore.getState().loadWorkflowRuns('wf_1', { limit: 10, offset: 0 }); expect(mockClient.listWorkflowRuns).toHaveBeenCalledWith('wf_1', { limit: 10, offset: 0 }); expect(runs).toHaveLength(2); expect(runs[0]).toMatchObject({ runId: 'run_wf1_001', status: 'completed', startedAt: '2026-03-14T10:00:00Z', completedAt: '2026-03-14T10:05:00Z', }); expect(runs[1]).toMatchObject({ runId: 'run_wf1_002', status: 'running', step: '2', }); const state = useWorkflowStore.getState(); expect(state.workflowRuns['wf_1']).toEqual(runs); }); it('handles alternative field names from API (snake_case)', async () => { mockClient.listWorkflowRuns = vi.fn().mockResolvedValue({ runs: [ { run_id: 'run_snake', workflow_id: 'wf_1', status: 'completed', started_at: '2026-03-14T10:00:00Z', completed_at: '2026-03-14T10:05:00Z', current_step: 2, total_steps: 5, }, ], }); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const runs = await useWorkflowStore.getState().loadWorkflowRuns('wf_1'); expect(runs[0]).toMatchObject({ runId: 'run_snake', status: 'completed', startedAt: '2026-03-14T10:00:00Z', completedAt: '2026-03-14T10:05:00Z', step: '2', }); }); it('handles runs with id field instead of runId', async () => { mockClient.listWorkflowRuns = vi.fn().mockResolvedValue({ runs: [ { id: 'run_by_id', status: 'running' }, ], }); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const runs = await useWorkflowStore.getState().loadWorkflowRuns('wf_1'); expect(runs[0].runId).toBe('run_by_id'); }); it('returns empty array and handles errors gracefully', async () => { mockClient.listWorkflowRuns = vi.fn().mockRejectedValue(new Error('Failed to load runs')); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const runs = await useWorkflowStore.getState().loadWorkflowRuns('wf_1'); expect(runs).toEqual([]); }); it('handles null result from client', async () => { mockClient.listWorkflowRuns = vi.fn().mockResolvedValue(null); const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); const runs = await useWorkflowStore.getState().loadWorkflowRuns('wf_1'); expect(runs).toEqual([]); }); }); describe('clearError', () => { it('clears the error state', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.setState({ error: 'Some error' }); useWorkflowStore.getState().clearError(); expect(useWorkflowStore.getState().error).toBeNull(); }); }); describe('reset', () => { it('resets all state to initial values', async () => { const { useWorkflowStore } = await import('../../../desktop/src/store/workflowStore'); useWorkflowStore.getState().setWorkflowStoreClient(mockClient); await useWorkflowStore.getState().loadWorkflows(); await useWorkflowStore.getState().loadWorkflowRuns('wf_1'); useWorkflowStore.setState({ error: 'Some error', isLoading: true }); useWorkflowStore.getState().reset(); const state = useWorkflowStore.getState(); expect(state.workflows).toEqual([]); expect(state.workflowRuns).toEqual({}); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); }); }); });