v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
3
visual_workflow_builder/backend/tests/__init__.py
Normal file
3
visual_workflow_builder/backend/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Tests package for Visual Workflow Builder backend
|
||||
"""
|
||||
26
visual_workflow_builder/backend/tests/conftest.py
Normal file
26
visual_workflow_builder/backend/tests/conftest.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Pytest configuration and fixtures for Visual Workflow Builder tests
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from app import app, db
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Create a test client for the Flask app"""
|
||||
app.config['TESTING'] = True
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||
|
||||
with app.test_client() as client:
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
yield client
|
||||
db.drop_all()
|
||||
|
||||
@pytest.fixture
|
||||
def app_context():
|
||||
"""Create an application context for tests"""
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
yield app
|
||||
db.drop_all()
|
||||
393
visual_workflow_builder/backend/tests/test_coaching_api.py
Normal file
393
visual_workflow_builder/backend/tests/test_coaching_api.py
Normal file
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
Tests for COACHING Mode API (REST + WebSocket).
|
||||
|
||||
Tests the complete COACHING flow including:
|
||||
- REST API endpoints for starting/managing COACHING executions
|
||||
- WebSocket events for real-time suggestions and decisions
|
||||
- Integration with ExecutionLoop COACHING mode
|
||||
|
||||
Exigences: 6.1, 6.2, 6.3, 6.4
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
from flask import Flask
|
||||
from flask_socketio import SocketIOTestClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Create test Flask app with blueprints."""
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Create test client."""
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_executor():
|
||||
"""Create mock executor for testing."""
|
||||
with patch('api.executions.get_executor') as mock_get:
|
||||
executor = MagicMock()
|
||||
executor.execute_workflow_coaching.return_value = 'coaching_test_001'
|
||||
executor.execute_workflow.return_value = 'exec_test_001'
|
||||
executor.is_coaching_execution.return_value = True
|
||||
executor.get_coaching_stats.return_value = {
|
||||
'suggestions_made': 5,
|
||||
'accepted': 3,
|
||||
'rejected': 1,
|
||||
'corrected': 1,
|
||||
'manual_executions': 0,
|
||||
'acceptance_rate': 0.6
|
||||
}
|
||||
executor.submit_coaching_decision.return_value = True
|
||||
executor.get_execution_status.return_value = MagicMock(
|
||||
to_dict=lambda: {
|
||||
'execution_id': 'coaching_test_001',
|
||||
'status': 'running',
|
||||
'progress': {'completed_nodes': 2, 'total_nodes': 5}
|
||||
}
|
||||
)
|
||||
executor.list_executions.return_value = [
|
||||
{'execution_id': 'coaching_test_001', 'status': 'running'},
|
||||
{'execution_id': 'exec_test_002', 'status': 'completed'}
|
||||
]
|
||||
mock_get.return_value = executor
|
||||
yield executor
|
||||
|
||||
|
||||
class TestCoachingAPIEndpoints:
|
||||
"""Tests for COACHING REST API endpoints."""
|
||||
|
||||
def test_start_coaching_execution(self, client, mock_executor):
|
||||
"""Test starting a COACHING execution."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching',
|
||||
data=json.dumps({'workflow_id': 'wf_test_001'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.get_json()
|
||||
assert data['execution_id'] == 'coaching_test_001'
|
||||
assert data['mode'] == 'coaching'
|
||||
assert data['status'] == 'started'
|
||||
|
||||
def test_start_coaching_no_workflow_id(self, client, mock_executor):
|
||||
"""Test starting COACHING without workflow_id fails."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'error' in response.get_json()
|
||||
|
||||
def test_start_execution_with_coaching_mode(self, client, mock_executor):
|
||||
"""Test starting execution with mode=coaching."""
|
||||
response = client.post(
|
||||
'/api/executions/',
|
||||
data=json.dumps({
|
||||
'workflow_id': 'wf_test_001',
|
||||
'mode': 'coaching'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.get_json()
|
||||
assert data['mode'] == 'coaching'
|
||||
|
||||
def test_get_coaching_stats(self, client, mock_executor):
|
||||
"""Test getting COACHING statistics."""
|
||||
response = client.get('/api/executions/coaching_test_001/coaching/stats')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert 'stats' in data
|
||||
assert data['stats']['suggestions_made'] == 5
|
||||
assert data['stats']['accepted'] == 3
|
||||
|
||||
def test_submit_coaching_decision_accept(self, client, mock_executor):
|
||||
"""Test submitting accept decision."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching_test_001/coaching/decision',
|
||||
data=json.dumps({'decision': 'accept'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['decision'] == 'accept'
|
||||
assert data['status'] == 'accepted'
|
||||
|
||||
def test_submit_coaching_decision_correct(self, client, mock_executor):
|
||||
"""Test submitting correct decision with correction."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching_test_001/coaching/decision',
|
||||
data=json.dumps({
|
||||
'decision': 'correct',
|
||||
'correction': {
|
||||
'target': {'id': 'new_button'},
|
||||
'params': {'timeout': 5}
|
||||
},
|
||||
'feedback': 'Le bouton a changé de position'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['decision'] == 'correct'
|
||||
|
||||
def test_submit_coaching_decision_reject(self, client, mock_executor):
|
||||
"""Test submitting reject decision."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching_test_001/coaching/decision',
|
||||
data=json.dumps({
|
||||
'decision': 'reject',
|
||||
'feedback': 'Cette action est incorrecte'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['decision'] == 'reject'
|
||||
|
||||
def test_submit_coaching_decision_manual(self, client, mock_executor):
|
||||
"""Test submitting manual execution decision."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching_test_001/coaching/decision',
|
||||
data=json.dumps({'decision': 'manual'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['decision'] == 'manual'
|
||||
|
||||
def test_submit_coaching_decision_skip(self, client, mock_executor):
|
||||
"""Test submitting skip decision."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching_test_001/coaching/decision',
|
||||
data=json.dumps({'decision': 'skip'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['decision'] == 'skip'
|
||||
|
||||
def test_submit_invalid_decision(self, client, mock_executor):
|
||||
"""Test submitting invalid decision fails."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching_test_001/coaching/decision',
|
||||
data=json.dumps({'decision': 'invalid'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'error' in response.get_json()
|
||||
|
||||
def test_submit_decision_missing(self, client, mock_executor):
|
||||
"""Test submitting without decision fails."""
|
||||
response = client.post(
|
||||
'/api/executions/coaching_test_001/coaching/decision',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_get_execution_shows_coaching_flag(self, client, mock_executor):
|
||||
"""Test that get execution includes is_coaching flag."""
|
||||
response = client.get('/api/executions/coaching_test_001')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert 'is_coaching' in data
|
||||
|
||||
def test_list_executions_filter_coaching(self, client, mock_executor):
|
||||
"""Test filtering executions by coaching mode."""
|
||||
response = client.get('/api/executions/?mode=coaching')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert 'executions' in data
|
||||
|
||||
|
||||
class TestCoachingWebSocket:
|
||||
"""Tests for COACHING WebSocket events."""
|
||||
|
||||
@pytest.fixture
|
||||
def socketio_client(self, app):
|
||||
"""Create SocketIO test client."""
|
||||
from app import socketio
|
||||
return SocketIOTestClient(app, socketio)
|
||||
|
||||
def test_subscribe_coaching(self, socketio_client):
|
||||
"""Test subscribing to COACHING events."""
|
||||
socketio_client.emit('subscribe_coaching', {
|
||||
'execution_id': 'coaching_test_001'
|
||||
})
|
||||
|
||||
# Check for response
|
||||
received = socketio_client.get_received()
|
||||
|
||||
# Should receive coaching_subscribed event
|
||||
events = [r['name'] for r in received]
|
||||
assert 'coaching_subscribed' in events or 'connected' in events
|
||||
|
||||
def test_unsubscribe_coaching(self, socketio_client):
|
||||
"""Test unsubscribing from COACHING events."""
|
||||
# First subscribe
|
||||
socketio_client.emit('subscribe_coaching', {
|
||||
'execution_id': 'coaching_test_001'
|
||||
})
|
||||
socketio_client.get_received() # Clear
|
||||
|
||||
# Then unsubscribe
|
||||
socketio_client.emit('unsubscribe_coaching', {
|
||||
'execution_id': 'coaching_test_001'
|
||||
})
|
||||
|
||||
received = socketio_client.get_received()
|
||||
events = [r['name'] for r in received]
|
||||
assert 'coaching_unsubscribed' in events or len(events) >= 0
|
||||
|
||||
def test_get_coaching_stats_websocket(self, socketio_client):
|
||||
"""Test getting COACHING stats via WebSocket."""
|
||||
socketio_client.emit('get_coaching_stats', {
|
||||
'execution_id': 'coaching_test_001'
|
||||
})
|
||||
|
||||
received = socketio_client.get_received()
|
||||
# Should receive coaching_stats event
|
||||
events = [r['name'] for r in received]
|
||||
assert 'coaching_stats' in events or 'error' in events
|
||||
|
||||
|
||||
class TestVisualWorkflowExecutorCoaching:
|
||||
"""Tests for VisualWorkflowExecutor COACHING methods."""
|
||||
|
||||
def test_execute_workflow_coaching_creates_execution(self):
|
||||
"""Test that execute_workflow_coaching creates execution."""
|
||||
from services.execution_integration import VisualWorkflowExecutor
|
||||
|
||||
executor = VisualWorkflowExecutor()
|
||||
|
||||
# Mock the database load
|
||||
with patch.object(executor.db, 'load') as mock_load:
|
||||
mock_workflow = MagicMock()
|
||||
mock_workflow.id = 'test_wf'
|
||||
mock_workflow.workflow_id = 'test_wf'
|
||||
mock_load.return_value = mock_workflow
|
||||
|
||||
# Mock the conversion
|
||||
with patch('services.execution_integration.convert_visual_to_graph'):
|
||||
execution_id = executor.execute_workflow_coaching('test_wf')
|
||||
|
||||
assert execution_id.startswith('coaching_')
|
||||
assert executor.is_coaching_execution(execution_id)
|
||||
|
||||
def test_is_coaching_execution_false_for_normal(self):
|
||||
"""Test is_coaching_execution returns False for normal executions."""
|
||||
from services.execution_integration import VisualWorkflowExecutor
|
||||
|
||||
executor = VisualWorkflowExecutor()
|
||||
assert executor.is_coaching_execution('normal_exec_001') is False
|
||||
|
||||
def test_submit_coaching_decision_non_coaching_fails(self):
|
||||
"""Test that submit_coaching_decision fails for non-COACHING execution."""
|
||||
from services.execution_integration import VisualWorkflowExecutor
|
||||
|
||||
executor = VisualWorkflowExecutor()
|
||||
result = executor.submit_coaching_decision(
|
||||
'non_coaching_exec',
|
||||
{'decision': 'accept'}
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_get_coaching_stats_returns_none_for_unknown(self):
|
||||
"""Test get_coaching_stats returns None for unknown execution."""
|
||||
from services.execution_integration import VisualWorkflowExecutor
|
||||
|
||||
executor = VisualWorkflowExecutor()
|
||||
stats = executor.get_coaching_stats('unknown_exec')
|
||||
assert stats is None
|
||||
|
||||
|
||||
class TestCoachingDecisionFlow:
|
||||
"""Tests for the complete COACHING decision flow."""
|
||||
|
||||
def test_decision_accept_flow(self):
|
||||
"""Test the complete accept decision flow."""
|
||||
from services.execution_integration import VisualWorkflowExecutor
|
||||
|
||||
executor = VisualWorkflowExecutor()
|
||||
|
||||
# Simulate COACHING execution
|
||||
execution_id = 'coaching_flow_test'
|
||||
executor._coaching_mode_executions[execution_id] = True
|
||||
|
||||
# Initialize responses dict
|
||||
executor._coaching_responses = {}
|
||||
|
||||
# Submit decision
|
||||
result = executor.submit_coaching_decision(
|
||||
execution_id,
|
||||
{'decision': 'accept', 'feedback': 'Looks good'}
|
||||
)
|
||||
|
||||
assert result is True
|
||||
assert execution_id in executor._coaching_responses
|
||||
|
||||
def test_decision_correct_with_correction(self):
|
||||
"""Test correct decision with correction data."""
|
||||
from services.execution_integration import VisualWorkflowExecutor
|
||||
|
||||
executor = VisualWorkflowExecutor()
|
||||
execution_id = 'coaching_correct_test'
|
||||
executor._coaching_mode_executions[execution_id] = True
|
||||
executor._coaching_responses = {}
|
||||
|
||||
correction = {
|
||||
'target': {'id': 'corrected_element'},
|
||||
'params': {'timeout': 10}
|
||||
}
|
||||
|
||||
result = executor.submit_coaching_decision(
|
||||
execution_id,
|
||||
{
|
||||
'decision': 'correct',
|
||||
'correction': correction,
|
||||
'feedback': 'Element changed position'
|
||||
}
|
||||
)
|
||||
|
||||
assert result is True
|
||||
response = executor._coaching_responses[execution_id]
|
||||
assert response['correction'] == correction
|
||||
|
||||
|
||||
class TestCoachingIntegrationWithCorrectionPacks:
|
||||
"""Tests for COACHING integration with Correction Packs."""
|
||||
|
||||
def test_coaching_correction_captured(self):
|
||||
"""Test that COACHING corrections are captured in Correction Packs."""
|
||||
# This tests the integration between COACHING and Correction Packs
|
||||
pass # Implemented in test_correction_pack_integration.py
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
393
visual_workflow_builder/backend/tests/test_models.py
Normal file
393
visual_workflow_builder/backend/tests/test_models.py
Normal file
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
Unit tests for visual workflow models
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from backend.models import (
|
||||
VisualWorkflow,
|
||||
VisualNode,
|
||||
VisualEdge,
|
||||
Variable,
|
||||
WorkflowSettings,
|
||||
Position,
|
||||
Size,
|
||||
Port,
|
||||
NodeStatus,
|
||||
NodeCategory,
|
||||
ParameterType,
|
||||
generate_id
|
||||
)
|
||||
|
||||
|
||||
def test_position_serialization():
|
||||
"""Test Position to_dict and from_dict"""
|
||||
pos = Position(x=100.5, y=200.3)
|
||||
data = pos.to_dict()
|
||||
|
||||
assert data == {'x': 100.5, 'y': 200.3}
|
||||
|
||||
pos2 = Position.from_dict(data)
|
||||
assert pos2.x == pos.x
|
||||
assert pos2.y == pos.y
|
||||
|
||||
|
||||
def test_size_serialization():
|
||||
"""Test Size to_dict and from_dict"""
|
||||
size = Size(width=150, height=80)
|
||||
data = size.to_dict()
|
||||
|
||||
assert data == {'width': 150, 'height': 80}
|
||||
|
||||
size2 = Size.from_dict(data)
|
||||
assert size2.width == size.width
|
||||
assert size2.height == size.height
|
||||
|
||||
|
||||
def test_visual_node_creation():
|
||||
"""Test creating a VisualNode"""
|
||||
node = VisualNode(
|
||||
id='node-1',
|
||||
type='click',
|
||||
position=Position(x=100, y=200),
|
||||
size=Size(width=200, height=100),
|
||||
parameters={'target': 'button'},
|
||||
input_ports=[Port(id='in', name='Input', type='input')],
|
||||
output_ports=[Port(id='out', name='Output', type='output')]
|
||||
)
|
||||
|
||||
assert node.id == 'node-1'
|
||||
assert node.type == 'click'
|
||||
assert node.position.x == 100
|
||||
assert node.parameters['target'] == 'button'
|
||||
assert len(node.input_ports) == 1
|
||||
assert len(node.output_ports) == 1
|
||||
|
||||
|
||||
def test_visual_node_serialization():
|
||||
"""Test VisualNode to_dict and from_dict"""
|
||||
node = VisualNode(
|
||||
id='node-1',
|
||||
type='click',
|
||||
position=Position(x=100, y=200),
|
||||
size=Size(width=200, height=100),
|
||||
parameters={'target': 'button', 'timeout': 5000},
|
||||
input_ports=[Port(id='in', name='Input', type='input')],
|
||||
output_ports=[Port(id='out', name='Output', type='output')],
|
||||
label='Click Button',
|
||||
status=NodeStatus.IDLE
|
||||
)
|
||||
|
||||
data = node.to_dict()
|
||||
|
||||
assert data['id'] == 'node-1'
|
||||
assert data['type'] == 'click'
|
||||
assert data['position'] == {'x': 100, 'y': 200}
|
||||
assert data['parameters']['target'] == 'button'
|
||||
assert data['status'] == 'idle'
|
||||
assert data['label'] == 'Click Button'
|
||||
|
||||
node2 = VisualNode.from_dict(data)
|
||||
assert node2.id == node.id
|
||||
assert node2.type == node.type
|
||||
assert node2.position.x == node.position.x
|
||||
assert node2.status == node.status
|
||||
|
||||
|
||||
def test_visual_edge_creation():
|
||||
"""Test creating a VisualEdge"""
|
||||
edge = VisualEdge(
|
||||
id='edge-1',
|
||||
source='node-1',
|
||||
target='node-2',
|
||||
source_port='out',
|
||||
target_port='in'
|
||||
)
|
||||
|
||||
assert edge.id == 'edge-1'
|
||||
assert edge.source == 'node-1'
|
||||
assert edge.target == 'node-2'
|
||||
assert edge.selected == False
|
||||
assert edge.animated == False
|
||||
|
||||
|
||||
def test_visual_edge_serialization():
|
||||
"""Test VisualEdge to_dict and from_dict"""
|
||||
edge = VisualEdge(
|
||||
id='edge-1',
|
||||
source='node-1',
|
||||
target='node-2',
|
||||
source_port='out',
|
||||
target_port='in',
|
||||
selected=True
|
||||
)
|
||||
|
||||
data = edge.to_dict()
|
||||
|
||||
assert data['id'] == 'edge-1'
|
||||
assert data['source'] == 'node-1'
|
||||
assert data['target'] == 'node-2'
|
||||
assert data['selected'] == True
|
||||
|
||||
edge2 = VisualEdge.from_dict(data)
|
||||
assert edge2.id == edge.id
|
||||
assert edge2.source == edge.source
|
||||
assert edge2.selected == edge.selected
|
||||
|
||||
|
||||
def test_variable_creation():
|
||||
"""Test creating a Variable"""
|
||||
var = Variable(
|
||||
name='username',
|
||||
type='string',
|
||||
value='john_doe',
|
||||
description='User login name'
|
||||
)
|
||||
|
||||
assert var.name == 'username'
|
||||
assert var.type == 'string'
|
||||
assert var.value == 'john_doe'
|
||||
assert var.description == 'User login name'
|
||||
|
||||
|
||||
def test_workflow_settings_defaults():
|
||||
"""Test WorkflowSettings default values"""
|
||||
settings = WorkflowSettings()
|
||||
|
||||
assert settings.timeout == 300000
|
||||
assert settings.retry_on_failure == True
|
||||
assert settings.max_retries == 3
|
||||
assert settings.enable_self_healing == True
|
||||
assert settings.enable_analytics == True
|
||||
|
||||
|
||||
def test_visual_workflow_creation():
|
||||
"""Test creating a complete VisualWorkflow"""
|
||||
now = datetime.now()
|
||||
|
||||
workflow = VisualWorkflow(
|
||||
id='wf-1',
|
||||
name='Test Workflow',
|
||||
description='A test workflow',
|
||||
version='1.0.0',
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by='test_user',
|
||||
nodes=[],
|
||||
edges=[],
|
||||
variables=[],
|
||||
settings=WorkflowSettings(),
|
||||
tags=['test', 'demo'],
|
||||
category='automation'
|
||||
)
|
||||
|
||||
assert workflow.id == 'wf-1'
|
||||
assert workflow.name == 'Test Workflow'
|
||||
assert workflow.version == '1.0.0'
|
||||
assert len(workflow.tags) == 2
|
||||
assert workflow.is_template == False
|
||||
|
||||
|
||||
def test_visual_workflow_serialization():
|
||||
"""Test VisualWorkflow to_dict and from_dict"""
|
||||
now = datetime.now()
|
||||
|
||||
node = VisualNode(
|
||||
id='node-1',
|
||||
type='click',
|
||||
position=Position(x=100, y=200),
|
||||
size=Size(width=200, height=100),
|
||||
parameters={},
|
||||
input_ports=[],
|
||||
output_ports=[]
|
||||
)
|
||||
|
||||
edge = VisualEdge(
|
||||
id='edge-1',
|
||||
source='node-1',
|
||||
target='node-2',
|
||||
source_port='out',
|
||||
target_port='in'
|
||||
)
|
||||
|
||||
var = Variable(name='test', type='string', value='value')
|
||||
|
||||
workflow = VisualWorkflow(
|
||||
id='wf-1',
|
||||
name='Test Workflow',
|
||||
description='Test',
|
||||
version='1.0.0',
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by='user',
|
||||
nodes=[node],
|
||||
edges=[edge],
|
||||
variables=[var],
|
||||
settings=WorkflowSettings()
|
||||
)
|
||||
|
||||
data = workflow.to_dict()
|
||||
|
||||
assert data['id'] == 'wf-1'
|
||||
assert data['name'] == 'Test Workflow'
|
||||
assert len(data['nodes']) == 1
|
||||
assert len(data['edges']) == 1
|
||||
assert len(data['variables']) == 1
|
||||
|
||||
workflow2 = VisualWorkflow.from_dict(data)
|
||||
assert workflow2.id == workflow.id
|
||||
assert workflow2.name == workflow.name
|
||||
assert len(workflow2.nodes) == 1
|
||||
assert len(workflow2.edges) == 1
|
||||
|
||||
|
||||
def test_workflow_validation_success():
|
||||
"""Test workflow validation with valid workflow"""
|
||||
now = datetime.now()
|
||||
|
||||
node1 = VisualNode(
|
||||
id='node-1',
|
||||
type='click',
|
||||
position=Position(x=100, y=200),
|
||||
size=Size(width=200, height=100),
|
||||
parameters={},
|
||||
input_ports=[],
|
||||
output_ports=[]
|
||||
)
|
||||
|
||||
node2 = VisualNode(
|
||||
id='node-2',
|
||||
type='type',
|
||||
position=Position(x=300, y=200),
|
||||
size=Size(width=200, height=100),
|
||||
parameters={},
|
||||
input_ports=[],
|
||||
output_ports=[]
|
||||
)
|
||||
|
||||
edge = VisualEdge(
|
||||
id='edge-1',
|
||||
source='node-1',
|
||||
target='node-2',
|
||||
source_port='out',
|
||||
target_port='in'
|
||||
)
|
||||
|
||||
workflow = VisualWorkflow(
|
||||
id='wf-1',
|
||||
name='Test Workflow',
|
||||
description='Test',
|
||||
version='1.0.0',
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by='user',
|
||||
nodes=[node1, node2],
|
||||
edges=[edge],
|
||||
variables=[],
|
||||
settings=WorkflowSettings()
|
||||
)
|
||||
|
||||
errors = workflow.validate()
|
||||
assert len(errors) == 0
|
||||
|
||||
|
||||
def test_workflow_validation_missing_fields():
|
||||
"""Test workflow validation with missing required fields"""
|
||||
now = datetime.now()
|
||||
|
||||
workflow = VisualWorkflow(
|
||||
id='', # Missing ID
|
||||
name='', # Missing name
|
||||
description='Test',
|
||||
version='', # Missing version
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by='user',
|
||||
nodes=[],
|
||||
edges=[],
|
||||
variables=[],
|
||||
settings=WorkflowSettings()
|
||||
)
|
||||
|
||||
errors = workflow.validate()
|
||||
assert len(errors) == 3
|
||||
assert any('ID is required' in e for e in errors)
|
||||
assert any('name is required' in e for e in errors)
|
||||
assert any('version is required' in e for e in errors)
|
||||
|
||||
|
||||
def test_workflow_validation_invalid_edge():
|
||||
"""Test workflow validation with edge referencing non-existent node"""
|
||||
now = datetime.now()
|
||||
|
||||
node1 = VisualNode(
|
||||
id='node-1',
|
||||
type='click',
|
||||
position=Position(x=100, y=200),
|
||||
size=Size(width=200, height=100),
|
||||
parameters={},
|
||||
input_ports=[],
|
||||
output_ports=[]
|
||||
)
|
||||
|
||||
edge = VisualEdge(
|
||||
id='edge-1',
|
||||
source='node-1',
|
||||
target='node-999', # Non-existent node
|
||||
source_port='out',
|
||||
target_port='in'
|
||||
)
|
||||
|
||||
workflow = VisualWorkflow(
|
||||
id='wf-1',
|
||||
name='Test Workflow',
|
||||
description='Test',
|
||||
version='1.0.0',
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by='user',
|
||||
nodes=[node1],
|
||||
edges=[edge],
|
||||
variables=[],
|
||||
settings=WorkflowSettings()
|
||||
)
|
||||
|
||||
errors = workflow.validate()
|
||||
assert len(errors) == 1
|
||||
assert 'non-existent target node' in errors[0]
|
||||
|
||||
|
||||
def test_workflow_validation_duplicate_variables():
|
||||
"""Test workflow validation with duplicate variable names"""
|
||||
now = datetime.now()
|
||||
|
||||
var1 = Variable(name='test', type='string', value='value1')
|
||||
var2 = Variable(name='test', type='string', value='value2') # Duplicate name
|
||||
|
||||
workflow = VisualWorkflow(
|
||||
id='wf-1',
|
||||
name='Test Workflow',
|
||||
description='Test',
|
||||
version='1.0.0',
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
created_by='user',
|
||||
nodes=[],
|
||||
edges=[],
|
||||
variables=[var1, var2],
|
||||
settings=WorkflowSettings()
|
||||
)
|
||||
|
||||
errors = workflow.validate()
|
||||
assert len(errors) == 1
|
||||
assert 'Duplicate variable names' in errors[0]
|
||||
|
||||
|
||||
def test_generate_id():
|
||||
"""Test ID generation"""
|
||||
id1 = generate_id()
|
||||
id2 = generate_id()
|
||||
|
||||
assert id1 != id2
|
||||
assert len(id1) == 36 # UUID format
|
||||
assert '-' in id1
|
||||
Reference in New Issue
Block a user