- 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>
367 lines
14 KiB
Python
367 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Real Functionality Test Utilities for Documentation System
|
|
|
|
Provides utilities for testing real documentation functionality without mocks:
|
|
- Real API integration testing
|
|
- Real data validation
|
|
- Real service management
|
|
- Real user workflow simulation
|
|
"""
|
|
|
|
import json
|
|
import requests
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any
|
|
|
|
|
|
class RealDocumentationData:
|
|
"""Manages real documentation data for testing"""
|
|
|
|
@staticmethod
|
|
def get_sample_real_data() -> List[Dict[str, Any]]:
|
|
"""Get sample documentation data that matches real RPA Vision V3 system structure"""
|
|
return [
|
|
{
|
|
"id": "screen_capture",
|
|
"name": "Screen Capture",
|
|
"description": "Captures screenshots for RPA Vision V3 analysis using Layer 0 components",
|
|
"category": "Layer 0 - Raw Capture",
|
|
"rpa_layer": 0,
|
|
"parameters": [
|
|
{"name": "region", "type": "bbox", "required": False},
|
|
{"name": "format", "type": "string", "default": "png"}
|
|
],
|
|
"examples": [
|
|
{"description": "Full screen capture", "code": "capture_screen()"},
|
|
{"description": "Region capture", "code": "capture_screen(region=(0,0,800,600))"}
|
|
],
|
|
"related_components": ["core.capture.screen_capturer", "agent_v0.screen_capturer"]
|
|
},
|
|
{
|
|
"id": "ui_detection",
|
|
"name": "UI Element Detection",
|
|
"description": "Detects UI elements using OWL-ViT and VLM models for semantic understanding",
|
|
"category": "Layer 2 - UI Detection",
|
|
"rpa_layer": 2,
|
|
"parameters": [
|
|
{"name": "screenshot", "type": "image", "required": True},
|
|
{"name": "target_description", "type": "string", "required": True},
|
|
{"name": "confidence_threshold", "type": "float", "default": 0.8}
|
|
],
|
|
"examples": [
|
|
{"description": "Detect button", "code": "detect_ui(screenshot, 'Submit button')"},
|
|
{"description": "Find input field", "code": "detect_ui(screenshot, 'username input', 0.9)"}
|
|
],
|
|
"related_components": ["core.detection.ui_detector", "core.detection.owl_detector"]
|
|
},
|
|
{
|
|
"id": "embedding_fusion",
|
|
"name": "Multi-modal Embedding Fusion",
|
|
"description": "Combines visual, textual, and spatial embeddings for robust matching",
|
|
"category": "Layer 3 - State Embedding",
|
|
"rpa_layer": 3,
|
|
"parameters": [
|
|
{"name": "image_embedding", "type": "ndarray", "required": True},
|
|
{"name": "text_embedding", "type": "ndarray", "required": True},
|
|
{"name": "weights", "type": "dict", "default": {"image": 0.6, "text": 0.4}}
|
|
],
|
|
"examples": [
|
|
{"description": "Fuse embeddings", "code": "fusion_engine.fuse({'image': img_emb, 'text': txt_emb})"}
|
|
],
|
|
"related_components": ["core.embedding.fusion_engine", "core.embedding.faiss_manager"]
|
|
},
|
|
{
|
|
"id": "workflow_execution",
|
|
"name": "Workflow Execution with Self-Healing",
|
|
"description": "Executes RPA workflows with automatic adaptation to UI changes",
|
|
"category": "Layer 4 - Workflow Execution",
|
|
"rpa_layer": 4,
|
|
"parameters": [
|
|
{"name": "workflow_graph", "type": "WorkflowGraph", "required": True},
|
|
{"name": "healing_enabled", "type": "boolean", "default": True},
|
|
{"name": "max_retries", "type": "int", "default": 3}
|
|
],
|
|
"examples": [
|
|
{"description": "Execute workflow", "code": "execute_workflow(workflow_graph)"},
|
|
{"description": "Execute with healing", "code": "execute_workflow(workflow_graph, healing_enabled=True)"}
|
|
],
|
|
"related_components": ["core.execution.action_executor", "core.healing.healing_engine"]
|
|
}
|
|
]
|
|
|
|
@staticmethod
|
|
def create_real_documentation_file(file_path: Path) -> None:
|
|
"""Create a real documentation file for testing"""
|
|
data = RealDocumentationData.get_sample_real_data()
|
|
|
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(file_path, 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
@staticmethod
|
|
def validate_documentation_structure(data: List[Dict[str, Any]]) -> bool:
|
|
"""Validate that documentation data has correct real RPA Vision V3 structure"""
|
|
required_fields = ['id', 'name', 'description', 'category']
|
|
rpa_specific_fields = ['rpa_layer', 'related_components']
|
|
|
|
for entry in data:
|
|
# Check basic required fields
|
|
if not all(field in entry for field in required_fields):
|
|
return False
|
|
|
|
# Validate parameters if present
|
|
if 'parameters' in entry:
|
|
for param in entry['parameters']:
|
|
if not all(field in param for field in ['name', 'type']):
|
|
return False
|
|
|
|
# Check for RPA Vision V3 specific fields (at least some entries should have them)
|
|
if any(field in entry for field in rpa_specific_fields):
|
|
# Validate RPA layer is valid (0-4)
|
|
if 'rpa_layer' in entry:
|
|
if not isinstance(entry['rpa_layer'], int) or not (0 <= entry['rpa_layer'] <= 4):
|
|
return False
|
|
|
|
# Validate related components format
|
|
if 'related_components' in entry:
|
|
if not isinstance(entry['related_components'], list):
|
|
return False
|
|
|
|
# Check component naming follows RPA Vision V3 conventions
|
|
for component in entry['related_components']:
|
|
if not isinstance(component, str) or not component.startswith('core.'):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class RealServiceManager:
|
|
"""Manages real services for testing"""
|
|
|
|
def __init__(self, base_dir: Path = None):
|
|
self.base_dir = base_dir or Path.cwd()
|
|
self.processes = {}
|
|
|
|
def start_backend_service(self, port: int = 5000) -> bool:
|
|
"""Start real backend service"""
|
|
backend_dir = self.base_dir / "visual_workflow_builder" / "backend"
|
|
|
|
if not backend_dir.exists():
|
|
print(f"Backend directory not found: {backend_dir}")
|
|
return False
|
|
|
|
try:
|
|
# Check if already running
|
|
if self._is_port_in_use(port):
|
|
print(f"Backend already running on port {port}")
|
|
return True
|
|
|
|
# Start backend
|
|
process = subprocess.Popen(
|
|
["python", "app.py"],
|
|
cwd=backend_dir,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env={**os.environ, "FLASK_ENV": "testing", "PORT": str(port)}
|
|
)
|
|
|
|
self.processes['backend'] = process
|
|
|
|
# Wait for service to be ready
|
|
return self._wait_for_service(f"http://localhost:{port}", timeout=30)
|
|
|
|
except Exception as e:
|
|
print(f"Failed to start backend: {e}")
|
|
return False
|
|
|
|
def start_frontend_service(self, port: int = 3000) -> bool:
|
|
"""Start real frontend service"""
|
|
frontend_dir = self.base_dir / "visual_workflow_builder" / "frontend"
|
|
|
|
if not frontend_dir.exists():
|
|
print(f"Frontend directory not found: {frontend_dir}")
|
|
return False
|
|
|
|
try:
|
|
# Check if already running
|
|
if self._is_port_in_use(port):
|
|
print(f"Frontend already running on port {port}")
|
|
return True
|
|
|
|
# Start frontend
|
|
process = subprocess.Popen(
|
|
["npm", "start"],
|
|
cwd=frontend_dir,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env={**os.environ, "PORT": str(port), "BROWSER": "none"}
|
|
)
|
|
|
|
self.processes['frontend'] = process
|
|
|
|
# Wait for service to be ready
|
|
return self._wait_for_service(f"http://localhost:{port}", timeout=60)
|
|
|
|
except Exception as e:
|
|
print(f"Failed to start frontend: {e}")
|
|
return False
|
|
|
|
def _is_port_in_use(self, port: int) -> bool:
|
|
"""Check if port is in use"""
|
|
try:
|
|
response = requests.get(f"http://localhost:{port}", timeout=2)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def _wait_for_service(self, url: str, timeout: int = 30) -> bool:
|
|
"""Wait for service to be ready"""
|
|
start_time = time.time()
|
|
|
|
while time.time() - start_time < timeout:
|
|
try:
|
|
response = requests.get(url, timeout=2)
|
|
if response.status_code == 200:
|
|
return True
|
|
except:
|
|
pass
|
|
time.sleep(1)
|
|
|
|
return False
|
|
|
|
def cleanup(self):
|
|
"""Cleanup all started processes"""
|
|
for name, process in self.processes.items():
|
|
try:
|
|
process.terminate()
|
|
process.wait(timeout=5)
|
|
print(f"Stopped {name} service")
|
|
except:
|
|
try:
|
|
process.kill()
|
|
except:
|
|
pass
|
|
|
|
|
|
class RealAPITester:
|
|
"""Tests real API functionality"""
|
|
|
|
def __init__(self, base_url: str):
|
|
self.base_url = base_url.rstrip('/')
|
|
|
|
def test_documentation_endpoints(self) -> Dict[str, Any]:
|
|
"""Test real documentation API endpoints"""
|
|
results = {
|
|
'tools_endpoint': False,
|
|
'categories_endpoint': False,
|
|
'search_endpoint': False,
|
|
'data_valid': False,
|
|
'response_time_ok': False
|
|
}
|
|
|
|
# Test tools endpoint
|
|
try:
|
|
start_time = time.time()
|
|
response = requests.get(f"{self.base_url}/api/documentation/tools", timeout=10)
|
|
response_time = time.time() - start_time
|
|
|
|
if response.status_code == 200:
|
|
results['tools_endpoint'] = True
|
|
results['response_time_ok'] = response_time < 2.0 # Should be fast
|
|
|
|
data = response.json()
|
|
results['data_valid'] = RealDocumentationData.validate_documentation_structure(data)
|
|
|
|
except Exception as e:
|
|
print(f"Tools endpoint error: {e}")
|
|
|
|
# Test categories endpoint
|
|
try:
|
|
response = requests.get(f"{self.base_url}/api/documentation/categories", timeout=10)
|
|
results['categories_endpoint'] = response.status_code == 200
|
|
except Exception as e:
|
|
print(f"Categories endpoint error: {e}")
|
|
|
|
# Test search endpoint
|
|
try:
|
|
response = requests.get(f"{self.base_url}/api/documentation/search?q=click", timeout=10)
|
|
results['search_endpoint'] = response.status_code == 200
|
|
except Exception as e:
|
|
print(f"Search endpoint error: {e}")
|
|
|
|
return results
|
|
|
|
def get_real_documentation_data(self) -> Optional[List[Dict[str, Any]]]:
|
|
"""Get real documentation data from API"""
|
|
try:
|
|
response = requests.get(f"{self.base_url}/api/documentation/tools", timeout=10)
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
except Exception as e:
|
|
print(f"Failed to get documentation data: {e}")
|
|
|
|
return None
|
|
|
|
|
|
class RealUserWorkflowTester:
|
|
"""Tests real user workflows"""
|
|
|
|
@staticmethod
|
|
def validate_tab_interaction(driver, tab_element) -> bool:
|
|
"""Validate real tab interaction behavior"""
|
|
try:
|
|
# Get initial state
|
|
initial_selected = tab_element.get_attribute("aria-selected") == "true"
|
|
|
|
# Click tab
|
|
driver.execute_script("arguments[0].click();", tab_element)
|
|
time.sleep(1)
|
|
|
|
# Verify state changed
|
|
final_selected = tab_element.get_attribute("aria-selected") == "true"
|
|
|
|
return final_selected and not initial_selected
|
|
|
|
except Exception as e:
|
|
print(f"Tab interaction validation failed: {e}")
|
|
return False
|
|
|
|
@staticmethod
|
|
def validate_content_loading(driver, expected_data: List[Dict[str, Any]]) -> bool:
|
|
"""Validate that real content loaded correctly"""
|
|
try:
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
|
|
wait = WebDriverWait(driver, 10)
|
|
|
|
# Wait for content panel
|
|
content_panel = wait.until(
|
|
EC.presence_of_element_located((By.CSS_SELECTOR, "[role='tabpanel'], .documentation-content"))
|
|
)
|
|
|
|
if not content_panel.is_displayed():
|
|
return False
|
|
|
|
# Check for expected content
|
|
content_text = content_panel.text.lower()
|
|
|
|
# Verify at least some expected entries are present
|
|
found_count = 0
|
|
for entry in expected_data[:3]: # Check first 3 entries
|
|
if entry['name'].lower() in content_text:
|
|
found_count += 1
|
|
|
|
return found_count > 0
|
|
|
|
except Exception as e:
|
|
print(f"Content loading validation failed: {e}")
|
|
return False
|
|
|
|
|
|
# Import os for environment variables
|
|
import os |