test(desktop): Phase 3 store unit tests — 112 new tests for 5 stores
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- saasStore: login/logout/register, TOTP setup/verify/disable, billing (plans/subscription/payment), templates, connection mode, config sync - workflowStore: CRUD, trigger, cancel, loadRuns, client injection - offlineStore: queue message, update/remove, reconnect backoff, getters - handStore: loadHands, getHandDetails, trigger/approve/cancel, triggers CRUD, approvals, autonomy blocking - streamStore: chatMode switching, getChatModeConfig, suggestions, setIsLoading, cancelStream, searchSkills, initStreamListener All 173 tests pass (61 existing + 112 new).
This commit is contained in:
379
desktop/tests/store/workflowStore.test.ts
Normal file
379
desktop/tests/store/workflowStore.test.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* 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<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValue(null);
|
||||
|
||||
await useWorkflowStore.getState().loadWorkflows();
|
||||
|
||||
expect(useWorkflowStore.getState().workflows).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle client error', async () => {
|
||||
(mockClient.listWorkflows as ReturnType<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValue(null);
|
||||
|
||||
const result = await useWorkflowStore.getState().createWorkflow({
|
||||
name: 'X',
|
||||
steps: [],
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle creation error', async () => {
|
||||
(mockClient.createWorkflow as ReturnType<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValue(null);
|
||||
|
||||
const result = await useWorkflowStore.getState().triggerWorkflow('wf-1');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle trigger error', async () => {
|
||||
(mockClient.executeWorkflow as ReturnType<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user