/** * Workflow Store Tests * * Tests for workflow CRUD, trigger, cancel, and run management. * Uses a mock WorkflowClient injected via setWorkflowStoreClient. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useWorkflowStore, type WorkflowClient, type WorkflowCreateOptions, type UpdateWorkflowInput, } from '../../src/store/workflowStore'; // ── Mock Tauri ── vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(), })); // ── Mock client ── let mockClient: WorkflowClient; function createMockClient(): WorkflowClient { return { listWorkflows: vi.fn(async () => ({ workflows: [] })), getWorkflow: vi.fn(async () => null), createWorkflow: vi.fn(async () => null), updateWorkflow: vi.fn(async () => null), deleteWorkflow: vi.fn(async () => ({ status: 'deleted' })), executeWorkflow: vi.fn(async () => null), cancelWorkflow: vi.fn(async () => ({ status: 'cancelled' })), listWorkflowRuns: vi.fn(async () => ({ runs: [] })), }; } beforeEach(() => { mockClient = createMockClient(); useWorkflowStore.setState({ workflows: [], workflowRuns: {}, isLoading: false, error: null, client: mockClient, }); vi.clearAllMocks(); }); // ═══════════════════════════════════════════════════════════════════ // Initial State // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — initial state', () => { it('should start with empty workflows', () => { const state = useWorkflowStore.getState(); expect(state.workflows).toEqual([]); }); it('should not be loading initially', () => { expect(useWorkflowStore.getState().isLoading).toBe(false); }); it('should have no error', () => { expect(useWorkflowStore.getState().error).toBeNull(); }); it('should have empty workflowRuns', () => { expect(useWorkflowStore.getState().workflowRuns).toEqual({}); }); }); // ═══════════════════════════════════════════════════════════════════ // loadWorkflows // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — loadWorkflows', () => { it('should load workflows from client', async () => { (mockClient.listWorkflows as ReturnType).mockResolvedValue({ workflows: [ { id: 'wf-1', name: 'Test Workflow', steps: 3, description: 'desc', createdAt: '2026-01-01' }, { id: 'wf-2', name: 'Another', steps: 1 }, ], }); await useWorkflowStore.getState().loadWorkflows(); const state = useWorkflowStore.getState(); expect(state.workflows).toHaveLength(2); expect(state.workflows[0].name).toBe('Test Workflow'); expect(state.workflows[0].steps).toBe(3); expect(state.workflows[1].id).toBe('wf-2'); expect(state.isLoading).toBe(false); }); it('should handle null result', async () => { (mockClient.listWorkflows as ReturnType).mockResolvedValue(null); await useWorkflowStore.getState().loadWorkflows(); expect(useWorkflowStore.getState().workflows).toEqual([]); }); it('should handle client error', async () => { (mockClient.listWorkflows as ReturnType).mockRejectedValue(new Error('Network error')); await useWorkflowStore.getState().loadWorkflows(); const state = useWorkflowStore.getState(); expect(state.error).toBe('Network error'); expect(state.isLoading).toBe(false); }); }); // ═══════════════════════════════════════════════════════════════════ // createWorkflow // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — createWorkflow', () => { it('should create workflow and add to list', async () => { const opts: WorkflowCreateOptions = { name: 'New Pipeline', description: 'A test pipeline', steps: [{ handName: 'browser' }], }; (mockClient.createWorkflow as ReturnType).mockResolvedValue({ id: 'wf-new', name: 'New Pipeline', }); const result = await useWorkflowStore.getState().createWorkflow(opts); expect(result).toBeDefined(); expect(result!.id).toBe('wf-new'); expect(result!.steps).toBe(1); expect(useWorkflowStore.getState().workflows).toHaveLength(1); }); it('should return undefined on null result', async () => { (mockClient.createWorkflow as ReturnType).mockResolvedValue(null); const result = await useWorkflowStore.getState().createWorkflow({ name: 'X', steps: [], }); expect(result).toBeUndefined(); }); it('should handle creation error', async () => { (mockClient.createWorkflow as ReturnType).mockRejectedValue(new Error('Create failed')); const result = await useWorkflowStore.getState().createWorkflow({ name: 'X', steps: [], }); expect(result).toBeUndefined(); expect(useWorkflowStore.getState().error).toBe('Create failed'); }); }); // ═══════════════════════════════════════════════════════════════════ // updateWorkflow // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — updateWorkflow', () => { it('should update existing workflow', async () => { useWorkflowStore.setState({ workflows: [{ id: 'wf-1', name: 'Old', steps: 2 }], }); (mockClient.updateWorkflow as ReturnType).mockResolvedValue({ id: 'wf-1', name: 'Updated', }); const updates: UpdateWorkflowInput = { name: 'Updated', steps: [{ handName: 'researcher' }] }; const result = await useWorkflowStore.getState().updateWorkflow('wf-1', updates); expect(result).toBeDefined(); expect(result!.name).toBe('Updated'); expect(result!.steps).toBe(1); }); it('should preserve unchanged fields', async () => { useWorkflowStore.setState({ workflows: [{ id: 'wf-1', name: 'Old', steps: 5, description: 'desc' }], }); (mockClient.updateWorkflow as ReturnType).mockResolvedValue({ id: 'wf-1', name: 'Old', }); await useWorkflowStore.getState().updateWorkflow('wf-1', { description: 'new desc' }); const wf = useWorkflowStore.getState().workflows[0]; expect(wf.name).toBe('Old'); }); }); // ═══════════════════════════════════════════════════════════════════ // deleteWorkflow // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — deleteWorkflow', () => { it('should remove workflow from list', async () => { useWorkflowStore.setState({ workflows: [ { id: 'wf-1', name: 'First', steps: 1 }, { id: 'wf-2', name: 'Second', steps: 2 }, ], workflowRuns: { 'wf-1': [{ runId: 'r1', status: 'completed', startedAt: '' }] }, }); await useWorkflowStore.getState().deleteWorkflow('wf-1'); const state = useWorkflowStore.getState(); expect(state.workflows).toHaveLength(1); expect(state.workflows[0].id).toBe('wf-2'); expect(state.workflowRuns).not.toHaveProperty('wf-1'); }); it('should handle delete error', async () => { (mockClient.deleteWorkflow as ReturnType).mockRejectedValue(new Error('Delete failed')); await expect( useWorkflowStore.getState().deleteWorkflow('wf-1') ).rejects.toThrow('Delete failed'); expect(useWorkflowStore.getState().error).toBe('Delete failed'); }); }); // ═══════════════════════════════════════════════════════════════════ // triggerWorkflow // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — triggerWorkflow', () => { it('should return run result on success', async () => { (mockClient.executeWorkflow as ReturnType).mockResolvedValue({ runId: 'run-123', status: 'running', }); const result = await useWorkflowStore.getState().triggerWorkflow('wf-1', { topic: 'test' }); expect(result).toEqual({ runId: 'run-123', status: 'running' }); }); it('should return undefined on null result', async () => { (mockClient.executeWorkflow as ReturnType).mockResolvedValue(null); const result = await useWorkflowStore.getState().triggerWorkflow('wf-1'); expect(result).toBeUndefined(); }); it('should handle trigger error', async () => { (mockClient.executeWorkflow as ReturnType).mockRejectedValue(new Error('Exec failed')); const result = await useWorkflowStore.getState().triggerWorkflow('wf-1'); expect(result).toBeUndefined(); expect(useWorkflowStore.getState().error).toBe('Exec failed'); }); }); // ═══════════════════════════════════════════════════════════════════ // cancelWorkflow // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — cancelWorkflow', () => { it('should cancel and reload workflows', async () => { (mockClient.listWorkflows as ReturnType).mockResolvedValue({ workflows: [] }); await useWorkflowStore.getState().cancelWorkflow('wf-1', 'run-1'); expect(mockClient.cancelWorkflow).toHaveBeenCalledWith('wf-1', 'run-1'); }); it('should handle cancel error', async () => { (mockClient.cancelWorkflow as ReturnType).mockRejectedValue(new Error('Cancel failed')); await expect( useWorkflowStore.getState().cancelWorkflow('wf-1', 'run-1') ).rejects.toThrow('Cancel failed'); }); }); // ═══════════════════════════════════════════════════════════════════ // loadWorkflowRuns // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — loadWorkflowRuns', () => { it('should load and normalize runs', async () => { (mockClient.listWorkflowRuns as ReturnType).mockResolvedValue({ runs: [ { runId: 'r1', status: 'completed', started_at: '2026-01-01', completed_at: '2026-01-01' }, { run_id: 'r2', status: 'running', current_step: 3 }, { id: 'r3', status: 'failed', error: 'boom' }, ], }); const runs = await useWorkflowStore.getState().loadWorkflowRuns('wf-1'); expect(runs).toHaveLength(3); expect(runs[0].runId).toBe('r1'); expect(runs[0].startedAt).toBe('2026-01-01'); expect(runs[1].runId).toBe('r2'); expect(runs[1].step).toBe('3'); expect(runs[2].runId).toBe('r3'); expect(runs[2].error).toBe('boom'); // Stored in workflowRuns map expect(useWorkflowStore.getState().workflowRuns['wf-1']).toHaveLength(3); }); it('should return empty array on error', async () => { (mockClient.listWorkflowRuns as ReturnType).mockRejectedValue(new Error('fail')); const runs = await useWorkflowStore.getState().loadWorkflowRuns('wf-1'); expect(runs).toEqual([]); }); }); // ═══════════════════════════════════════════════════════════════════ // getWorkflow (local lookup) // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — getWorkflow', () => { it('should find workflow by id', () => { useWorkflowStore.setState({ workflows: [ { id: 'wf-1', name: 'Test', steps: 3 }, { id: 'wf-2', name: 'Other', steps: 1 }, ], }); const found = useWorkflowStore.getState().getWorkflow('wf-1'); expect(found).toBeDefined(); expect(found!.name).toBe('Test'); }); it('should return undefined for unknown id', () => { useWorkflowStore.setState({ workflows: [{ id: 'wf-1', name: 'Test', steps: 1 }], }); expect(useWorkflowStore.getState().getWorkflow('unknown')).toBeUndefined(); }); }); // ═══════════════════════════════════════════════════════════════════ // clearError / reset // ═══════════════════════════════════════════════════════════════════ describe('workflowStore — utility actions', () => { it('clearError should clear error', () => { useWorkflowStore.setState({ error: 'Some error' }); useWorkflowStore.getState().clearError(); expect(useWorkflowStore.getState().error).toBeNull(); }); it('reset should restore initial state', () => { useWorkflowStore.setState({ workflows: [{ id: 'x', name: 'Y', steps: 1 }], isLoading: true, error: 'err', }); useWorkflowStore.getState().reset(); const state = useWorkflowStore.getState(); expect(state.workflows).toEqual([]); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); }); });