mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-07-07 02:57:55 +02:00
feat: Complete Temporal migration cleanup and fixes
- Remove obsolete docker_logs.py module and container diagnostics from SDK - Fix security_assessment workflow metadata (vertical: rust -> python) - Remove all Prefect references from documentation - Add SDK exception handling test suite - Clean up old test artifacts
This commit is contained in:
@@ -1,387 +0,0 @@
|
||||
"""
|
||||
Docker log integration for enhanced error reporting.
|
||||
|
||||
This module provides functionality to fetch and parse Docker container logs
|
||||
to provide better context for deployment and workflow execution errors.
|
||||
"""
|
||||
# Copyright (c) 2025 FuzzingLabs
|
||||
#
|
||||
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||
# at the root of this repository for details.
|
||||
#
|
||||
# After the Change Date (four years from publication), this version of the
|
||||
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Additional attribution and requirements are provided in the NOTICE file.
|
||||
|
||||
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContainerLogEntry:
|
||||
"""A single log entry from a container."""
|
||||
timestamp: datetime
|
||||
level: str
|
||||
message: str
|
||||
stream: str # 'stdout' or 'stderr'
|
||||
raw: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContainerDiagnostics:
|
||||
"""Complete diagnostics for a container."""
|
||||
container_id: Optional[str]
|
||||
status: str
|
||||
exit_code: Optional[int]
|
||||
error: Optional[str]
|
||||
logs: List[ContainerLogEntry]
|
||||
resource_usage: Dict[str, Any]
|
||||
volume_mounts: List[Dict[str, str]]
|
||||
|
||||
|
||||
class DockerLogIntegration:
|
||||
"""
|
||||
Integration with Docker to fetch container logs and diagnostics.
|
||||
|
||||
This class provides methods to fetch container logs, parse common error
|
||||
patterns, and extract meaningful diagnostic information from Docker
|
||||
containers related to FuzzForge workflow execution.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.docker_available = self._check_docker_availability()
|
||||
|
||||
# Common error patterns in container logs
|
||||
self.error_patterns = {
|
||||
'permission_denied': [
|
||||
r'permission denied',
|
||||
r'operation not permitted',
|
||||
r'cannot access.*permission denied'
|
||||
],
|
||||
'out_of_memory': [
|
||||
r'out of memory',
|
||||
r'oom killed',
|
||||
r'cannot allocate memory'
|
||||
],
|
||||
'image_pull_failed': [
|
||||
r'failed to pull image',
|
||||
r'pull access denied',
|
||||
r'image not found'
|
||||
],
|
||||
'volume_mount_failed': [
|
||||
r'invalid mount config',
|
||||
r'mount denied',
|
||||
r'no such file or directory.*mount'
|
||||
],
|
||||
'network_error': [
|
||||
r'network is unreachable',
|
||||
r'connection refused',
|
||||
r'timeout.*connect'
|
||||
]
|
||||
}
|
||||
|
||||
def _check_docker_availability(self) -> bool:
|
||||
"""Check if Docker is available and accessible."""
|
||||
try:
|
||||
result = subprocess.run(['docker', 'version', '--format', 'json'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
return result.returncode == 0
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
||||
return False
|
||||
|
||||
def get_container_logs(self, container_name_or_id: str, tail: int = 100) -> List[ContainerLogEntry]:
|
||||
"""
|
||||
Fetch logs from a Docker container.
|
||||
|
||||
Args:
|
||||
container_name_or_id: Container name or ID
|
||||
tail: Number of log lines to retrieve
|
||||
|
||||
Returns:
|
||||
List of parsed log entries
|
||||
"""
|
||||
if not self.docker_available:
|
||||
logger.warning("Docker not available, cannot fetch container logs")
|
||||
return []
|
||||
|
||||
try:
|
||||
cmd = ['docker', 'logs', '--timestamps', '--tail', str(tail), container_name_or_id]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Failed to fetch logs for container {container_name_or_id}: {result.stderr}")
|
||||
return []
|
||||
|
||||
return self._parse_docker_logs(result.stdout + result.stderr)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"Timeout fetching logs for container {container_name_or_id}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching container logs: {e}")
|
||||
return []
|
||||
|
||||
def _parse_docker_logs(self, raw_logs: str) -> List[ContainerLogEntry]:
|
||||
"""Parse raw Docker logs into structured entries."""
|
||||
entries = []
|
||||
|
||||
for line in raw_logs.strip().split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
entry = self._parse_log_line(line)
|
||||
if entry:
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
def _parse_log_line(self, line: str) -> Optional[ContainerLogEntry]:
|
||||
"""Parse a single log line with timestamp."""
|
||||
# Docker log format: 2023-10-01T12:00:00.000000000Z message
|
||||
timestamp_match = re.match(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(.*)', line)
|
||||
|
||||
if timestamp_match:
|
||||
timestamp_str, message = timestamp_match.groups()
|
||||
try:
|
||||
timestamp = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
else:
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
message = line
|
||||
|
||||
# Determine log level from message content
|
||||
level = self._extract_log_level(message)
|
||||
|
||||
# Determine stream (simplified - Docker doesn't clearly separate in combined output)
|
||||
stream = 'stderr' if any(keyword in message.lower() for keyword in ['error', 'failed', 'exception']) else 'stdout'
|
||||
|
||||
return ContainerLogEntry(
|
||||
timestamp=timestamp,
|
||||
level=level,
|
||||
message=message.strip(),
|
||||
stream=stream,
|
||||
raw=line
|
||||
)
|
||||
|
||||
def _extract_log_level(self, message: str) -> str:
|
||||
"""Extract log level from message content."""
|
||||
message_lower = message.lower()
|
||||
|
||||
if any(keyword in message_lower for keyword in ['error', 'failed', 'exception', 'fatal']):
|
||||
return 'ERROR'
|
||||
elif any(keyword in message_lower for keyword in ['warning', 'warn']):
|
||||
return 'WARNING'
|
||||
elif any(keyword in message_lower for keyword in ['info', 'information']):
|
||||
return 'INFO'
|
||||
elif any(keyword in message_lower for keyword in ['debug']):
|
||||
return 'DEBUG'
|
||||
else:
|
||||
return 'INFO'
|
||||
|
||||
def get_container_diagnostics(self, container_name_or_id: str) -> ContainerDiagnostics:
|
||||
"""
|
||||
Get complete diagnostics for a container including logs, status, and resource usage.
|
||||
|
||||
Args:
|
||||
container_name_or_id: Container name or ID
|
||||
|
||||
Returns:
|
||||
Complete container diagnostics
|
||||
"""
|
||||
if not self.docker_available:
|
||||
return ContainerDiagnostics(
|
||||
container_id=None,
|
||||
status="unknown",
|
||||
exit_code=None,
|
||||
error="Docker not available",
|
||||
logs=[],
|
||||
resource_usage={},
|
||||
volume_mounts=[]
|
||||
)
|
||||
|
||||
# Get container inspect data
|
||||
inspect_data = self._get_container_inspect(container_name_or_id)
|
||||
|
||||
# Get logs
|
||||
logs = self.get_container_logs(container_name_or_id)
|
||||
|
||||
# Extract key information
|
||||
if inspect_data:
|
||||
state = inspect_data.get('State', {})
|
||||
config = inspect_data.get('Config', {})
|
||||
host_config = inspect_data.get('HostConfig', {})
|
||||
|
||||
status = state.get('Status', 'unknown')
|
||||
exit_code = state.get('ExitCode')
|
||||
error = state.get('Error', '')
|
||||
|
||||
# Get volume mounts
|
||||
mounts = inspect_data.get('Mounts', [])
|
||||
volume_mounts = [
|
||||
{
|
||||
'source': mount.get('Source', ''),
|
||||
'destination': mount.get('Destination', ''),
|
||||
'mode': mount.get('Mode', ''),
|
||||
'type': mount.get('Type', '')
|
||||
}
|
||||
for mount in mounts
|
||||
]
|
||||
|
||||
# Get resource limits
|
||||
resource_usage = {
|
||||
'memory_limit': host_config.get('Memory', 0),
|
||||
'cpu_limit': host_config.get('CpuQuota', 0),
|
||||
'cpu_period': host_config.get('CpuPeriod', 0)
|
||||
}
|
||||
|
||||
else:
|
||||
status = "not_found"
|
||||
exit_code = None
|
||||
error = f"Container {container_name_or_id} not found"
|
||||
volume_mounts = []
|
||||
resource_usage = {}
|
||||
|
||||
return ContainerDiagnostics(
|
||||
container_id=container_name_or_id,
|
||||
status=status,
|
||||
exit_code=exit_code,
|
||||
error=error,
|
||||
logs=logs,
|
||||
resource_usage=resource_usage,
|
||||
volume_mounts=volume_mounts
|
||||
)
|
||||
|
||||
def _get_container_inspect(self, container_name_or_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get container inspection data."""
|
||||
try:
|
||||
cmd = ['docker', 'inspect', container_name_or_id]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
return data[0] if data else None
|
||||
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception) as e:
|
||||
logger.debug(f"Failed to inspect container {container_name_or_id}: {e}")
|
||||
return None
|
||||
|
||||
def analyze_error_patterns(self, logs: List[ContainerLogEntry]) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Analyze logs for common error patterns.
|
||||
|
||||
Args:
|
||||
logs: List of log entries to analyze
|
||||
|
||||
Returns:
|
||||
Dictionary mapping error types to matching log messages
|
||||
"""
|
||||
detected_errors = {}
|
||||
|
||||
for error_type, patterns in self.error_patterns.items():
|
||||
matches = []
|
||||
|
||||
for log_entry in logs:
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, log_entry.message, re.IGNORECASE):
|
||||
matches.append(log_entry.message)
|
||||
break # Don't match the same message multiple times
|
||||
|
||||
if matches:
|
||||
detected_errors[error_type] = matches
|
||||
|
||||
return detected_errors
|
||||
|
||||
def get_container_names_by_label(self, label_filter: str) -> List[str]:
|
||||
"""
|
||||
Get container names that match a specific label filter.
|
||||
|
||||
Args:
|
||||
label_filter: Label filter (e.g., "prefect.flow-run-id=12345")
|
||||
|
||||
Returns:
|
||||
List of container names
|
||||
"""
|
||||
if not self.docker_available:
|
||||
return []
|
||||
|
||||
try:
|
||||
cmd = ['docker', 'ps', '-a', '--filter', f'label={label_filter}', '--format', '{{.Names}}']
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||||
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
|
||||
return [name.strip() for name in result.stdout.strip().split('\n') if name.strip()]
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to get containers by label {label_filter}: {e}")
|
||||
return []
|
||||
|
||||
def suggest_fixes(self, error_analysis: Dict[str, List[str]]) -> List[str]:
|
||||
"""
|
||||
Suggest fixes based on detected error patterns.
|
||||
|
||||
Args:
|
||||
error_analysis: Result from analyze_error_patterns()
|
||||
|
||||
Returns:
|
||||
List of suggested fixes
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
if 'permission_denied' in error_analysis:
|
||||
suggestions.extend([
|
||||
"Check file permissions on the target path",
|
||||
"Ensure the Docker daemon has access to the mounted volumes",
|
||||
"Try running with elevated privileges or adjust volume ownership"
|
||||
])
|
||||
|
||||
if 'out_of_memory' in error_analysis:
|
||||
suggestions.extend([
|
||||
"Increase memory limits for the workflow",
|
||||
"Check if the target files are too large for available memory",
|
||||
"Consider using streaming processing for large datasets"
|
||||
])
|
||||
|
||||
if 'image_pull_failed' in error_analysis:
|
||||
suggestions.extend([
|
||||
"Check network connectivity to Docker registry",
|
||||
"Verify image name and tag are correct",
|
||||
"Ensure Docker registry credentials are configured"
|
||||
])
|
||||
|
||||
if 'volume_mount_failed' in error_analysis:
|
||||
suggestions.extend([
|
||||
"Verify the target path exists and is accessible",
|
||||
"Check volume mount syntax and permissions",
|
||||
"Ensure the path is not already in use by another process"
|
||||
])
|
||||
|
||||
if 'network_error' in error_analysis:
|
||||
suggestions.extend([
|
||||
"Check network connectivity",
|
||||
"Verify backend services are running (docker-compose up -d)",
|
||||
"Check firewall settings and port availability"
|
||||
])
|
||||
|
||||
if not suggestions:
|
||||
suggestions.append("Review the container logs above for specific error details")
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
# Global instance for easy access
|
||||
docker_integration = DockerLogIntegration()
|
||||
@@ -1,8 +1,9 @@
|
||||
"""
|
||||
Enhanced exceptions for FuzzForge SDK with rich context and Docker integration.
|
||||
Enhanced exceptions for FuzzForge SDK with rich context.
|
||||
|
||||
Provides comprehensive error information including container logs, diagnostics,
|
||||
and actionable suggestions for troubleshooting.
|
||||
Provides comprehensive error information and actionable suggestions for troubleshooting.
|
||||
Note: Container diagnostics are not available in Temporal architecture as workflows
|
||||
run in long-lived worker containers rather than ephemeral per-workflow containers.
|
||||
"""
|
||||
# Copyright (c) 2025 FuzzingLabs
|
||||
#
|
||||
@@ -21,8 +22,6 @@ import re
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from .docker_logs import docker_integration, ContainerDiagnostics
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorContext:
|
||||
@@ -31,7 +30,6 @@ class ErrorContext:
|
||||
request_method: Optional[str] = None
|
||||
request_data: Optional[Dict[str, Any]] = None
|
||||
response_data: Optional[Dict[str, Any]] = None
|
||||
container_diagnostics: Optional[ContainerDiagnostics] = None
|
||||
suggested_fixes: List[str] = None
|
||||
error_patterns: Dict[str, List[str]] = None
|
||||
related_run_id: Optional[str] = None
|
||||
@@ -62,49 +60,10 @@ class FuzzForgeError(Exception):
|
||||
self.context = context or ErrorContext()
|
||||
self.original_exception = original_exception
|
||||
|
||||
# Auto-populate container diagnostics if we have a run ID
|
||||
if self.context.related_run_id and not self.context.container_diagnostics:
|
||||
self._fetch_container_diagnostics()
|
||||
|
||||
def _fetch_container_diagnostics(self):
|
||||
"""Fetch container diagnostics for the related run."""
|
||||
if not self.context.related_run_id:
|
||||
return
|
||||
|
||||
try:
|
||||
# Try to find containers by Prefect run ID label
|
||||
label_filter = f"prefect.flow-run-id={self.context.related_run_id}"
|
||||
container_names = docker_integration.get_container_names_by_label(label_filter)
|
||||
|
||||
if container_names:
|
||||
# Use the most recent container
|
||||
container_name = container_names[0]
|
||||
diagnostics = docker_integration.get_container_diagnostics(container_name)
|
||||
|
||||
# Analyze error patterns in logs
|
||||
if diagnostics.logs:
|
||||
error_analysis = docker_integration.analyze_error_patterns(diagnostics.logs)
|
||||
suggestions = docker_integration.suggest_fixes(error_analysis)
|
||||
|
||||
self.context.container_diagnostics = diagnostics
|
||||
self.context.error_patterns = error_analysis
|
||||
self.context.suggested_fixes.extend(suggestions)
|
||||
|
||||
except Exception:
|
||||
# Don't fail the main error because of diagnostics issues
|
||||
pass
|
||||
|
||||
def get_summary(self) -> str:
|
||||
"""Get a summary of the error with key details."""
|
||||
parts = [self.message]
|
||||
|
||||
if self.context.container_diagnostics:
|
||||
diag = self.context.container_diagnostics
|
||||
if diag.status != 'running':
|
||||
parts.append(f"Container status: {diag.status}")
|
||||
if diag.exit_code is not None:
|
||||
parts.append(f"Exit code: {diag.exit_code}")
|
||||
|
||||
if self.context.error_patterns:
|
||||
detected = list(self.context.error_patterns.keys())
|
||||
parts.append(f"Detected issues: {', '.join(detected)}")
|
||||
@@ -153,18 +112,11 @@ class FuzzForgeHTTPError(FuzzForgeError):
|
||||
self.response_text = response_text
|
||||
|
||||
def get_summary(self) -> str:
|
||||
base = f"HTTP {self.status_code}: {self.message}"
|
||||
|
||||
if self.context.container_diagnostics:
|
||||
diag = self.context.container_diagnostics
|
||||
if diag.exit_code is not None and diag.exit_code != 0:
|
||||
base += f" (Container exit code: {diag.exit_code})"
|
||||
|
||||
return base
|
||||
return f"HTTP {self.status_code}: {self.message}"
|
||||
|
||||
|
||||
class DeploymentError(FuzzForgeHTTPError):
|
||||
"""Enhanced deployment errors with container diagnostics."""
|
||||
"""Enhanced deployment errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -181,23 +133,9 @@ class DeploymentError(FuzzForgeHTTPError):
|
||||
|
||||
context.workflow_name = workflow_name
|
||||
|
||||
# If we have a container name, get its diagnostics immediately
|
||||
if container_name:
|
||||
try:
|
||||
diagnostics = docker_integration.get_container_diagnostics(container_name)
|
||||
context.container_diagnostics = diagnostics
|
||||
|
||||
# Analyze logs for error patterns
|
||||
if diagnostics.logs:
|
||||
error_analysis = docker_integration.analyze_error_patterns(diagnostics.logs)
|
||||
suggestions = docker_integration.suggest_fixes(error_analysis)
|
||||
|
||||
context.error_patterns = error_analysis
|
||||
context.suggested_fixes.extend(suggestions)
|
||||
|
||||
except Exception:
|
||||
# Don't fail on diagnostics
|
||||
pass
|
||||
# Note: Container diagnostics are not fetched in Temporal architecture.
|
||||
# Workflows run in long-lived worker containers, not per-workflow containers.
|
||||
# The container_name parameter is kept for backward compatibility but not used.
|
||||
|
||||
full_message = f"Deployment failed for workflow '{workflow_name}': {message}"
|
||||
super().__init__(full_message, status_code, response_text, context)
|
||||
@@ -292,22 +230,9 @@ class ContainerError(FuzzForgeError):
|
||||
if context is None:
|
||||
context = ErrorContext()
|
||||
|
||||
# Immediately fetch container diagnostics
|
||||
try:
|
||||
diagnostics = docker_integration.get_container_diagnostics(container_name)
|
||||
context.container_diagnostics = diagnostics
|
||||
|
||||
# Analyze logs for patterns
|
||||
if diagnostics.logs:
|
||||
error_analysis = docker_integration.analyze_error_patterns(diagnostics.logs)
|
||||
suggestions = docker_integration.suggest_fixes(error_analysis)
|
||||
|
||||
context.error_patterns = error_analysis
|
||||
context.suggested_fixes.extend(suggestions)
|
||||
|
||||
except Exception:
|
||||
# Don't fail on diagnostics
|
||||
pass
|
||||
# Note: Container diagnostics are not fetched in Temporal architecture.
|
||||
# Workflows run in long-lived worker containers, not per-workflow containers.
|
||||
# The container_name parameter is kept for backward compatibility but not used.
|
||||
|
||||
full_message = f"Container error ({container_name}): {message}"
|
||||
if exit_code is not None:
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick smoke test for SDK exception handling after exceptions.py modifications.
|
||||
Tests that the modified _fetch_container_diagnostics() no-op doesn't break exception flows.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add SDK to path
|
||||
sdk_path = Path(__file__).parent / "src"
|
||||
sys.path.insert(0, str(sdk_path))
|
||||
|
||||
from fuzzforge_sdk.exceptions import (
|
||||
FuzzForgeError,
|
||||
FuzzForgeHTTPError,
|
||||
WorkflowNotFoundError,
|
||||
RunNotFoundError,
|
||||
ErrorContext,
|
||||
DeploymentError,
|
||||
WorkflowExecutionError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
def test_basic_import():
|
||||
"""Test that all exception classes can be imported."""
|
||||
print("✓ All exception classes imported successfully")
|
||||
|
||||
|
||||
def test_error_context():
|
||||
"""Test ErrorContext instantiation."""
|
||||
context = ErrorContext(
|
||||
url="http://localhost:8000/test",
|
||||
related_run_id="test-run-123",
|
||||
workflow_name="test_workflow"
|
||||
)
|
||||
assert context.url == "http://localhost:8000/test"
|
||||
assert context.related_run_id == "test-run-123"
|
||||
assert context.workflow_name == "test_workflow"
|
||||
print("✓ ErrorContext instantiation works")
|
||||
|
||||
|
||||
def test_base_exception():
|
||||
"""Test base FuzzForgeError."""
|
||||
context = ErrorContext(related_run_id="test-run-456")
|
||||
|
||||
error = FuzzForgeError("Test error message", context=context)
|
||||
|
||||
assert error.message == "Test error message"
|
||||
assert error.context.related_run_id == "test-run-456"
|
||||
print("✓ FuzzForgeError creation works")
|
||||
|
||||
|
||||
def test_http_error():
|
||||
"""Test HTTP error creation."""
|
||||
error = FuzzForgeHTTPError(
|
||||
message="Test HTTP error",
|
||||
status_code=500,
|
||||
response_text='{"error": "Internal server error"}'
|
||||
)
|
||||
|
||||
assert error.status_code == 500
|
||||
assert error.message == "Test HTTP error"
|
||||
assert error.context.response_data == {"error": "Internal server error"}
|
||||
print("✓ FuzzForgeHTTPError creation works")
|
||||
|
||||
|
||||
def test_workflow_not_found():
|
||||
"""Test WorkflowNotFoundError with suggestions."""
|
||||
error = WorkflowNotFoundError(
|
||||
workflow_name="nonexistent_workflow",
|
||||
available_workflows=["security_assessment", "secret_detection"]
|
||||
)
|
||||
|
||||
assert error.workflow_name == "nonexistent_workflow"
|
||||
assert len(error.context.suggested_fixes) > 0
|
||||
print("✓ WorkflowNotFoundError with suggestions works")
|
||||
|
||||
|
||||
def test_run_not_found():
|
||||
"""Test RunNotFoundError."""
|
||||
error = RunNotFoundError(run_id="missing-run-123")
|
||||
|
||||
assert error.run_id == "missing-run-123"
|
||||
assert error.context.related_run_id == "missing-run-123"
|
||||
assert len(error.context.suggested_fixes) > 0
|
||||
print("✓ RunNotFoundError creation works")
|
||||
|
||||
|
||||
def test_deployment_error():
|
||||
"""Test DeploymentError."""
|
||||
error = DeploymentError(
|
||||
workflow_name="test_workflow",
|
||||
message="Deployment failed",
|
||||
deployment_id="deploy-123",
|
||||
container_name="test-container-456" # Kept for backward compatibility
|
||||
)
|
||||
|
||||
assert error.workflow_name == "test_workflow"
|
||||
assert error.deployment_id == "deploy-123"
|
||||
print("✓ DeploymentError creation works")
|
||||
|
||||
|
||||
def test_workflow_execution_error():
|
||||
"""Test WorkflowExecutionError."""
|
||||
error = WorkflowExecutionError(
|
||||
workflow_name="security_assessment",
|
||||
run_id="run-789",
|
||||
message="Execution timeout"
|
||||
)
|
||||
|
||||
assert error.workflow_name == "security_assessment"
|
||||
assert error.run_id == "run-789"
|
||||
assert error.context.related_run_id == "run-789"
|
||||
print("✓ WorkflowExecutionError creation works")
|
||||
|
||||
|
||||
def test_validation_error():
|
||||
"""Test ValidationError."""
|
||||
error = ValidationError(
|
||||
field_name="target_path",
|
||||
message="Path does not exist",
|
||||
provided_value="/nonexistent/path",
|
||||
expected_format="Valid directory path"
|
||||
)
|
||||
|
||||
assert error.field_name == "target_path"
|
||||
assert error.provided_value == "/nonexistent/path"
|
||||
assert len(error.context.suggested_fixes) > 0
|
||||
print("✓ ValidationError with suggestions works")
|
||||
|
||||
|
||||
def test_exception_string_representation():
|
||||
"""Test exception summary and string conversion."""
|
||||
error = FuzzForgeHTTPError(
|
||||
message="Test error",
|
||||
status_code=404,
|
||||
response_text="Not found"
|
||||
)
|
||||
|
||||
summary = error.get_summary()
|
||||
assert "404" in summary
|
||||
assert "Test error" in summary
|
||||
|
||||
str_repr = str(error)
|
||||
assert str_repr == summary
|
||||
print("✓ Exception string representation works")
|
||||
|
||||
|
||||
def test_exception_detailed_info():
|
||||
"""Test detailed error information."""
|
||||
context = ErrorContext(
|
||||
url="http://localhost:8000/test",
|
||||
workflow_name="test_workflow"
|
||||
)
|
||||
error = FuzzForgeError("Test error", context=context)
|
||||
|
||||
info = error.get_detailed_info()
|
||||
assert info["message"] == "Test error"
|
||||
assert info["type"] == "FuzzForgeError"
|
||||
assert info["url"] == "http://localhost:8000/test"
|
||||
assert info["workflow_name"] == "test_workflow"
|
||||
print("✓ Exception detailed info works")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("\n" + "="*60)
|
||||
print("SDK Exception Handling Smoke Tests")
|
||||
print("="*60 + "\n")
|
||||
|
||||
tests = [
|
||||
test_basic_import,
|
||||
test_error_context,
|
||||
test_base_exception,
|
||||
test_http_error,
|
||||
test_workflow_not_found,
|
||||
test_run_not_found,
|
||||
test_deployment_error,
|
||||
test_workflow_execution_error,
|
||||
test_validation_error,
|
||||
test_exception_string_representation,
|
||||
test_exception_detailed_info,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test_func in tests:
|
||||
try:
|
||||
test_func()
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ {test_func.__name__} FAILED: {e}")
|
||||
failed += 1
|
||||
|
||||
print("\n" + "="*60)
|
||||
print(f"Results: {passed} passed, {failed} failed")
|
||||
print("="*60 + "\n")
|
||||
|
||||
if failed > 0:
|
||||
print("❌ SDK exception handling has issues")
|
||||
return 1
|
||||
else:
|
||||
print("✅ SDK exception handling works correctly")
|
||||
print("✅ The no-op _fetch_container_diagnostics() doesn't break exception flows")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user