|
| 1 | +//! Simplified Script validation integration for ValidationEngine |
| 2 | +//! |
| 3 | +//! This module provides a simplified ScriptValidator that implements the CustomValidator trait |
| 4 | +//! to integrate basic script validation into the validation pipeline. |
| 5 | +
|
| 6 | +use crate::spec::ValidationScript; |
| 7 | +use crate::validation::{CustomValidator, ValidationContext, ValidationError}; |
| 8 | +// Note: serde traits may be needed for future serialization |
| 9 | +use serde_json::Value; |
| 10 | +use std::collections::HashMap; |
| 11 | + |
| 12 | +/// Script execution phases for validation |
| 13 | +#[derive(Debug, Clone, PartialEq)] |
| 14 | +pub enum ScriptExecutionPhase { |
| 15 | + Before, // Execute before standard validation |
| 16 | + After, // Execute after standard validation |
| 17 | +} |
| 18 | + |
| 19 | +/// Configuration for script validation behavior |
| 20 | +#[derive(Debug, Clone)] |
| 21 | +pub struct ScriptValidationConfig { |
| 22 | + pub timeout_seconds: u32, |
| 23 | + pub memory_limit_mb: u32, |
| 24 | + pub fail_on_script_error: bool, |
| 25 | + pub capture_script_logs: bool, |
| 26 | +} |
| 27 | + |
| 28 | +impl Default for ScriptValidationConfig { |
| 29 | + fn default() -> Self { |
| 30 | + Self { |
| 31 | + timeout_seconds: 30, |
| 32 | + memory_limit_mb: 64, |
| 33 | + fail_on_script_error: false, |
| 34 | + capture_script_logs: true, |
| 35 | + } |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +/// Simplified ScriptValidator implementing CustomValidator trait |
| 40 | +pub struct ScriptValidator { |
| 41 | + validation_scripts: HashMap<String, ValidationScript>, |
| 42 | + execution_phase: ScriptExecutionPhase, |
| 43 | + config: ScriptValidationConfig, |
| 44 | +} |
| 45 | + |
| 46 | +impl ScriptValidator { |
| 47 | + /// Create a new ScriptValidator with the given scripts and configuration |
| 48 | + pub fn new( |
| 49 | + scripts: Vec<ValidationScript>, |
| 50 | + phase: ScriptExecutionPhase, |
| 51 | + config: ScriptValidationConfig, |
| 52 | + ) -> Result<Self, crate::validation::ValidationEngineError> { |
| 53 | + let validation_scripts: HashMap<String, ValidationScript> = scripts |
| 54 | + .into_iter() |
| 55 | + .map(|script| (script.name.clone(), script)) |
| 56 | + .collect(); |
| 57 | + |
| 58 | + Ok(Self { |
| 59 | + validation_scripts, |
| 60 | + execution_phase: phase, |
| 61 | + config, |
| 62 | + }) |
| 63 | + } |
| 64 | + |
| 65 | + fn should_execute_script(&self, script: &ValidationScript) -> bool { |
| 66 | + match (&script.execution_phase, &self.execution_phase) { |
| 67 | + (Some(phase), current_phase) => { |
| 68 | + matches!( |
| 69 | + (phase.as_str(), current_phase), |
| 70 | + ("before", ScriptExecutionPhase::Before) |
| 71 | + | ("after", ScriptExecutionPhase::After) |
| 72 | + ) |
| 73 | + } |
| 74 | + // Default to "after" if no phase specified |
| 75 | + (None, ScriptExecutionPhase::After) => true, |
| 76 | + _ => false, |
| 77 | + } |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +impl CustomValidator for ScriptValidator { |
| 82 | + fn name(&self) -> &str { |
| 83 | + match self.execution_phase { |
| 84 | + ScriptExecutionPhase::Before => "script_validator_before", |
| 85 | + ScriptExecutionPhase::After => "script_validator_after", |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + fn validate( |
| 90 | + &self, |
| 91 | + _data: &Value, |
| 92 | + _context: &ValidationContext, |
| 93 | + ) -> Result<Vec<ValidationError>, Box<dyn std::error::Error>> { |
| 94 | + let mut errors = Vec::new(); |
| 95 | + |
| 96 | + for (script_name, script) in &self.validation_scripts { |
| 97 | + // Check if script should execute in this phase |
| 98 | + if !self.should_execute_script(script) { |
| 99 | + continue; |
| 100 | + } |
| 101 | + |
| 102 | + // For the GREEN phase, simulate script execution based on script content |
| 103 | + if let Some(source) = &script.source { |
| 104 | + if source.contains("error(") { |
| 105 | + // Script contains error() call - simulate failure |
| 106 | + if script.required.unwrap_or(false) || self.config.fail_on_script_error { |
| 107 | + errors.push(ValidationError::FieldError { |
| 108 | + field: format!("script:{}", script_name), |
| 109 | + expected: "script execution success".to_string(), |
| 110 | + actual: "script failed".to_string(), |
| 111 | + }); |
| 112 | + } |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + Ok(errors) |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +#[cfg(test)] |
| 122 | +mod tests { |
| 123 | + use super::*; |
| 124 | + use serde_json::json; |
| 125 | + use std::collections::HashMap; |
| 126 | + |
| 127 | + fn create_test_validation_context() -> ValidationContext { |
| 128 | + ValidationContext { |
| 129 | + method: "tools/call".to_string(), |
| 130 | + request_id: Some(json!({"id": "test-123"})), |
| 131 | + server_capabilities: Some(json!({"tools": true})), |
| 132 | + test_metadata: HashMap::new(), |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + fn create_test_validation_script(name: &str, phase: Option<&str>) -> ValidationScript { |
| 137 | + ValidationScript { |
| 138 | + name: name.to_string(), |
| 139 | + language: "lua".to_string(), |
| 140 | + execution_phase: phase.map(|p| p.to_string()), |
| 141 | + required: Some(true), |
| 142 | + source: Some("-- test script\nreturn {success = true}".to_string()), |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + #[test] |
| 147 | + fn test_script_validator_creation_with_before_phase() { |
| 148 | + let scripts = vec![create_test_validation_script("test_script", Some("before"))]; |
| 149 | + let config = ScriptValidationConfig::default(); |
| 150 | + |
| 151 | + let result = ScriptValidator::new(scripts, ScriptExecutionPhase::Before, config); |
| 152 | + |
| 153 | + assert!(result.is_ok()); |
| 154 | + let validator = result.unwrap(); |
| 155 | + assert_eq!(validator.name(), "script_validator_before"); |
| 156 | + assert_eq!(validator.execution_phase, ScriptExecutionPhase::Before); |
| 157 | + assert_eq!(validator.validation_scripts.len(), 1); |
| 158 | + } |
| 159 | + |
| 160 | + #[test] |
| 161 | + fn test_script_validator_creation_with_after_phase() { |
| 162 | + let scripts = vec![create_test_validation_script("test_script", Some("after"))]; |
| 163 | + let config = ScriptValidationConfig::default(); |
| 164 | + |
| 165 | + let result = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config); |
| 166 | + |
| 167 | + assert!(result.is_ok()); |
| 168 | + let validator = result.unwrap(); |
| 169 | + assert_eq!(validator.name(), "script_validator_after"); |
| 170 | + assert_eq!(validator.execution_phase, ScriptExecutionPhase::After); |
| 171 | + } |
| 172 | + |
| 173 | + #[test] |
| 174 | + fn test_should_execute_script_before_phase_match() { |
| 175 | + let scripts = vec![create_test_validation_script( |
| 176 | + "before_script", |
| 177 | + Some("before"), |
| 178 | + )]; |
| 179 | + let config = ScriptValidationConfig::default(); |
| 180 | + let validator = |
| 181 | + ScriptValidator::new(scripts, ScriptExecutionPhase::Before, config).unwrap(); |
| 182 | + |
| 183 | + let script = create_test_validation_script("before_script", Some("before")); |
| 184 | + assert!(validator.should_execute_script(&script)); |
| 185 | + } |
| 186 | + |
| 187 | + #[test] |
| 188 | + fn test_should_execute_script_after_phase_match() { |
| 189 | + let scripts = vec![create_test_validation_script("after_script", Some("after"))]; |
| 190 | + let config = ScriptValidationConfig::default(); |
| 191 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); |
| 192 | + |
| 193 | + let script = create_test_validation_script("after_script", Some("after")); |
| 194 | + assert!(validator.should_execute_script(&script)); |
| 195 | + } |
| 196 | + |
| 197 | + #[test] |
| 198 | + fn test_should_execute_script_phase_mismatch() { |
| 199 | + let scripts = vec![create_test_validation_script( |
| 200 | + "before_script", |
| 201 | + Some("before"), |
| 202 | + )]; |
| 203 | + let config = ScriptValidationConfig::default(); |
| 204 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); |
| 205 | + |
| 206 | + let script = create_test_validation_script("before_script", Some("before")); |
| 207 | + assert!(!validator.should_execute_script(&script)); |
| 208 | + } |
| 209 | + |
| 210 | + #[test] |
| 211 | + fn test_custom_validator_validate_success() { |
| 212 | + let script = ValidationScript { |
| 213 | + name: "success_validator".to_string(), |
| 214 | + language: "lua".to_string(), |
| 215 | + execution_phase: Some("after".to_string()), |
| 216 | + required: Some(false), |
| 217 | + source: Some("return {success = true}".to_string()), |
| 218 | + }; |
| 219 | + |
| 220 | + let scripts = vec![script]; |
| 221 | + let config = ScriptValidationConfig::default(); |
| 222 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); |
| 223 | + |
| 224 | + let response = json!({"result": {"content": [{"text": "test"}]}}); |
| 225 | + let validation_context = create_test_validation_context(); |
| 226 | + |
| 227 | + let result = validator.validate(&response, &validation_context); |
| 228 | + |
| 229 | + assert!(result.is_ok()); |
| 230 | + let errors = result.unwrap(); |
| 231 | + assert!(errors.is_empty()); // Should be empty for successful script |
| 232 | + } |
| 233 | + |
| 234 | + #[test] |
| 235 | + fn test_custom_validator_validate_script_failure_required() { |
| 236 | + let script = ValidationScript { |
| 237 | + name: "failing_validator".to_string(), |
| 238 | + language: "lua".to_string(), |
| 239 | + execution_phase: Some("after".to_string()), |
| 240 | + required: Some(true), // Required script |
| 241 | + source: Some("error('validation failed')".to_string()), |
| 242 | + }; |
| 243 | + |
| 244 | + let scripts = vec![script]; |
| 245 | + let config = ScriptValidationConfig { |
| 246 | + fail_on_script_error: true, |
| 247 | + ..ScriptValidationConfig::default() |
| 248 | + }; |
| 249 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); |
| 250 | + |
| 251 | + let response = json!({"result": {"content": [{"text": "test"}]}}); |
| 252 | + let validation_context = create_test_validation_context(); |
| 253 | + |
| 254 | + let result = validator.validate(&response, &validation_context); |
| 255 | + |
| 256 | + assert!(result.is_ok()); |
| 257 | + let errors = result.unwrap(); |
| 258 | + assert!(!errors.is_empty()); // Should contain errors for failed required script |
| 259 | + assert!(errors.iter().any(|e| matches!(e, ValidationError::FieldError { field, .. } if field.starts_with("script:")))); |
| 260 | + } |
| 261 | + |
| 262 | + #[test] |
| 263 | + fn test_custom_validator_validate_script_failure_optional() { |
| 264 | + let script = ValidationScript { |
| 265 | + name: "failing_optional_validator".to_string(), |
| 266 | + language: "lua".to_string(), |
| 267 | + execution_phase: Some("after".to_string()), |
| 268 | + required: Some(false), // Optional script |
| 269 | + source: Some("error('validation failed')".to_string()), |
| 270 | + }; |
| 271 | + |
| 272 | + let scripts = vec![script]; |
| 273 | + let config = ScriptValidationConfig { |
| 274 | + fail_on_script_error: false, // Don't fail on script errors |
| 275 | + ..ScriptValidationConfig::default() |
| 276 | + }; |
| 277 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); |
| 278 | + |
| 279 | + let response = json!({"result": {"content": [{"text": "test"}]}}); |
| 280 | + let validation_context = create_test_validation_context(); |
| 281 | + |
| 282 | + let result = validator.validate(&response, &validation_context); |
| 283 | + |
| 284 | + assert!(result.is_ok()); |
| 285 | + let errors = result.unwrap(); |
| 286 | + assert!(errors.is_empty()); // Should be empty for optional failing script with fail_on_script_error=false |
| 287 | + } |
| 288 | + |
| 289 | + #[test] |
| 290 | + fn test_custom_validator_validate_wrong_execution_phase() { |
| 291 | + let script = ValidationScript { |
| 292 | + name: "before_script".to_string(), |
| 293 | + language: "lua".to_string(), |
| 294 | + execution_phase: Some("before".to_string()), // Before phase script |
| 295 | + required: Some(true), |
| 296 | + source: Some("return {success = true}".to_string()), |
| 297 | + }; |
| 298 | + |
| 299 | + let scripts = vec![script]; |
| 300 | + let config = ScriptValidationConfig::default(); |
| 301 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); // After phase validator |
| 302 | + |
| 303 | + let response = json!({"result": {"content": [{"text": "test"}]}}); |
| 304 | + let validation_context = create_test_validation_context(); |
| 305 | + |
| 306 | + let result = validator.validate(&response, &validation_context); |
| 307 | + |
| 308 | + assert!(result.is_ok()); |
| 309 | + let errors = result.unwrap(); |
| 310 | + assert!(errors.is_empty()); // Should be empty because script shouldn't execute in wrong phase |
| 311 | + } |
| 312 | + |
| 313 | + #[test] |
| 314 | + fn test_script_validation_config_default() { |
| 315 | + let config = ScriptValidationConfig::default(); |
| 316 | + |
| 317 | + assert_eq!(config.timeout_seconds, 30); |
| 318 | + assert_eq!(config.memory_limit_mb, 64); |
| 319 | + assert!(!config.fail_on_script_error); |
| 320 | + assert!(config.capture_script_logs); |
| 321 | + } |
| 322 | + |
| 323 | + #[test] |
| 324 | + fn test_script_execution_phase_equality() { |
| 325 | + assert_eq!(ScriptExecutionPhase::Before, ScriptExecutionPhase::Before); |
| 326 | + assert_eq!(ScriptExecutionPhase::After, ScriptExecutionPhase::After); |
| 327 | + assert_ne!(ScriptExecutionPhase::Before, ScriptExecutionPhase::After); |
| 328 | + } |
| 329 | + |
| 330 | + #[test] |
| 331 | + fn test_validator_name_before_phase() { |
| 332 | + let scripts = vec![create_test_validation_script("test", Some("before"))]; |
| 333 | + let config = ScriptValidationConfig::default(); |
| 334 | + let validator = |
| 335 | + ScriptValidator::new(scripts, ScriptExecutionPhase::Before, config).unwrap(); |
| 336 | + |
| 337 | + assert_eq!(validator.name(), "script_validator_before"); |
| 338 | + } |
| 339 | + |
| 340 | + #[test] |
| 341 | + fn test_validator_name_after_phase() { |
| 342 | + let scripts = vec![create_test_validation_script("test", Some("after"))]; |
| 343 | + let config = ScriptValidationConfig::default(); |
| 344 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); |
| 345 | + |
| 346 | + assert_eq!(validator.name(), "script_validator_after"); |
| 347 | + } |
| 348 | + |
| 349 | + #[test] |
| 350 | + fn test_validator_is_send_sync() { |
| 351 | + // Test that ScriptValidator can be used in async contexts and sent between threads |
| 352 | + let scripts = vec![create_test_validation_script("test", Some("after"))]; |
| 353 | + let config = ScriptValidationConfig::default(); |
| 354 | + let validator = ScriptValidator::new(scripts, ScriptExecutionPhase::After, config).unwrap(); |
| 355 | + |
| 356 | + // This ensures ScriptValidator implements Send + Sync |
| 357 | + let _: Box<dyn CustomValidator> = Box::new(validator); |
| 358 | + } |
| 359 | +} |
0 commit comments