Files
zclaw_openfang/desktop/tests/store/workflowStore.test.ts
iven d758a4477f
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
test(desktop): Phase 3 store unit tests — 112 new tests for 5 stores
- 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).
2026-04-07 17:08:34 +08:00

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();
});
});