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).
380 lines
15 KiB
TypeScript
380 lines
15 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|
|
});
|