Skip to main content

mockforge_intelligence/intelligent_behavior/
mockai.rs

1//! Unified MockAI interface
2//!
3//! This module provides a unified interface for all MockAI features, including
4//! auto-configuration from OpenAPI or examples, intelligent response generation,
5//! and context-aware behavior orchestration.
6
7use super::config::IntelligentBehaviorConfig;
8use super::context::StatefulAiContext;
9use super::mutation_analyzer::MutationAnalyzer;
10use super::pagination_intelligence::{
11    PaginationIntelligence, PaginationMetadata, PaginationRequest,
12};
13use super::rule_generator::{ExamplePair, RuleGenerator};
14use super::types::BehaviorRules;
15use super::validation_generator::{RequestContext, ValidationGenerator};
16use mockforge_foundation::Result;
17use mockforge_openapi::OpenApiSpec;
18use serde_json::Value;
19use std::collections::HashMap;
20use uuid;
21
22// `Request` and `Response` are re-exported from `mockforge_foundation::intelligent_behavior`.
23pub use mockforge_foundation::intelligent_behavior::{Request, Response};
24
25/// MockAI unified interface
26pub struct MockAI {
27    /// Behavior rules
28    rules: BehaviorRules,
29    /// Rule generator for learning
30    rule_generator: RuleGenerator,
31    /// Mutation analyzer
32    mutation_analyzer: MutationAnalyzer,
33    /// Validation generator
34    validation_generator: ValidationGenerator,
35    /// Pagination intelligence
36    pagination_intelligence: PaginationIntelligence,
37    /// Configuration
38    config: IntelligentBehaviorConfig,
39    /// Session contexts for stateful behavior across requests
40    session_contexts: std::sync::Arc<tokio::sync::RwLock<HashMap<String, StatefulAiContext>>>,
41}
42
43#[async_trait::async_trait]
44impl mockforge_foundation::intelligent_behavior::MockAiBehavior for MockAI {
45    fn as_any(&self) -> &dyn std::any::Any {
46        self
47    }
48
49    async fn process_request(&self, request: &Request) -> Result<Response> {
50        // Delegate to the inherent implementation below.
51        Self::process_request(self, request).await
52    }
53}
54
55impl MockAI {
56    /// Create MockAI from OpenAPI specification
57    ///
58    /// Automatically generates behavioral rules from the OpenAPI spec.
59    pub async fn from_openapi(
60        spec: &OpenApiSpec,
61        config: IntelligentBehaviorConfig,
62    ) -> Result<Self> {
63        // Extract examples from OpenAPI spec
64        let examples = Self::extract_examples_from_openapi(spec)?;
65
66        // Generate rules from examples
67        let behavior_config = config.behavior_model.clone();
68        let rule_generator = RuleGenerator::new(behavior_config.clone());
69        let rules = rule_generator.generate_rules_from_examples(examples).await?;
70
71        // Create components
72        let mutation_analyzer = MutationAnalyzer::new().with_rules(rules.clone());
73        let validation_generator = ValidationGenerator::new(behavior_config.clone());
74        let pagination_intelligence = PaginationIntelligence::new(behavior_config);
75
76        Ok(Self {
77            rules,
78            rule_generator,
79            mutation_analyzer,
80            validation_generator,
81            pagination_intelligence,
82            config,
83            session_contexts: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
84        })
85    }
86
87    /// Create MockAI from example pairs
88    ///
89    /// Learns behavioral patterns from provided examples.
90    pub async fn from_examples(
91        examples: Vec<ExamplePair>,
92        config: IntelligentBehaviorConfig,
93    ) -> Result<Self> {
94        // Generate rules from examples
95        let behavior_config = config.behavior_model.clone();
96        let rule_generator = RuleGenerator::new(behavior_config.clone());
97        let rules = rule_generator.generate_rules_from_examples(examples).await?;
98
99        // Create components
100        let mutation_analyzer = MutationAnalyzer::new().with_rules(rules.clone());
101        let validation_generator = ValidationGenerator::new(behavior_config.clone());
102        let pagination_intelligence = PaginationIntelligence::new(behavior_config);
103
104        Ok(Self {
105            rules,
106            rule_generator,
107            mutation_analyzer,
108            validation_generator,
109            pagination_intelligence,
110            config,
111            session_contexts: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
112        })
113    }
114
115    /// Create a new MockAI instance (for testing or manual creation)
116    pub fn new(config: IntelligentBehaviorConfig) -> Self {
117        let behavior_config = config.behavior_model.clone();
118        let rule_generator = RuleGenerator::new(behavior_config.clone());
119        let rules = BehaviorRules::default();
120        let mutation_analyzer = MutationAnalyzer::new().with_rules(rules.clone());
121        let validation_generator = ValidationGenerator::new(behavior_config.clone());
122        let pagination_intelligence = PaginationIntelligence::new(behavior_config);
123
124        Self {
125            rules,
126            rule_generator,
127            mutation_analyzer,
128            validation_generator,
129            pagination_intelligence,
130            config,
131            session_contexts: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
132        }
133    }
134
135    /// Process a request and generate a response
136    ///
137    /// Convenience method that gets or creates a session context and generates a response.
138    /// This is the main entry point for processing HTTP requests.
139    /// Session ID is extracted from headers (X-Session-ID or Cookie) or generated if not present.
140    pub async fn process_request(&self, request: &Request) -> Result<Response> {
141        // Extract session ID from request headers
142        let session_id = self.extract_session_id(request);
143
144        // Get or create session context
145        let session_context = self.get_or_create_session_context(session_id).await?;
146
147        // Generate response using the session context
148        let response = self.generate_response(request, &session_context).await?;
149
150        // Record interaction in session history
151        // Since record_interaction now takes &self (uses internal RwLock),
152        // we can call it directly on the cloned context
153        if let Err(e) = session_context
154            .record_interaction(
155                request.method.clone(),
156                request.path.clone(),
157                request.body.clone(),
158                Some(response.body.clone()),
159            )
160            .await
161        {
162            tracing::warn!("Failed to record interaction: {}", e);
163        }
164
165        Ok(response)
166    }
167
168    /// Extract session ID from request headers
169    fn extract_session_id(&self, request: &Request) -> Option<String> {
170        // Try header first (X-Session-ID)
171        if let Some(session_id) = request.headers.get("X-Session-ID") {
172            return Some(session_id.clone());
173        }
174
175        // Try cookie (mockforge_session)
176        if let Some(cookie_header) = request.headers.get("Cookie") {
177            for part in cookie_header.split(';') {
178                let part = part.trim();
179                if let Some((key, value)) = part.split_once('=') {
180                    if key.trim() == "mockforge_session" {
181                        return Some(value.trim().to_string());
182                    }
183                }
184            }
185        }
186
187        None
188    }
189
190    /// Get or create a session context
191    async fn get_or_create_session_context(
192        &self,
193        session_id: Option<String>,
194    ) -> Result<StatefulAiContext> {
195        let session_id = session_id.unwrap_or_else(|| format!("session_{}", uuid::Uuid::new_v4()));
196
197        // Try to get existing context
198        {
199            let contexts = self.session_contexts.read().await;
200            if let Some(context) = contexts.get(&session_id) {
201                return Ok(context.clone());
202            }
203        }
204
205        // Create new context
206        let new_context = StatefulAiContext::new(session_id.clone(), self.config.clone());
207
208        // Store it
209        {
210            let mut contexts = self.session_contexts.write().await;
211            contexts.insert(session_id, new_context.clone());
212        }
213
214        Ok(new_context)
215    }
216
217    /// Generate response for a request
218    ///
219    /// Uses intelligent behavior to generate contextually appropriate responses
220    /// based on request mutations, validation, and pagination needs.
221    pub async fn generate_response(
222        &self,
223        request: &Request,
224        session_context: &StatefulAiContext,
225    ) -> Result<Response> {
226        // CRITICAL FIX: GET, HEAD, and OPTIONS requests should NEVER be analyzed as mutations
227        // These are idempotent methods that don't mutate state. Only POST, PUT, PATCH, DELETE are mutations.
228        let method_upper = request.method.to_uppercase();
229        let is_mutation_method =
230            matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
231
232        // Get previous request from session history
233        let history = session_context.get_history().await;
234        let previous_request = history.last().and_then(|interaction| interaction.request.clone());
235
236        // Only analyze mutations for mutation methods (POST, PUT, PATCH, DELETE)
237        // GET, HEAD, OPTIONS should use standard OpenAPI response generation, not mutation responses
238        let mutation_analysis = if is_mutation_method {
239            // Analyze mutation for mutation methods
240            let current_body = request.body.clone().unwrap_or(serde_json::json!({}));
241            self.mutation_analyzer
242                .analyze_mutation(&current_body, previous_request.as_ref(), session_context)
243                .await?
244        } else {
245            // For non-mutation methods (GET, HEAD, OPTIONS), create a dummy analysis
246            // that won't trigger mutation-based response generation
247            // This ensures GET requests use OpenAPI examples/schemas, not mutation responses
248            super::mutation_analyzer::MutationAnalysis {
249                mutation_type: super::mutation_analyzer::MutationType::NoChange, // Read operations are not mutations
250                changed_fields: Vec::new(),
251                added_fields: Vec::new(),
252                removed_fields: Vec::new(),
253                validation_issues: Vec::new(),
254                confidence: 1.0,
255            }
256        };
257
258        // Check for validation issues
259        if !mutation_analysis.validation_issues.is_empty() {
260            // Generate validation error response
261            let issue = &mutation_analysis.validation_issues[0];
262            let request_context = RequestContext {
263                method: request.method.clone(),
264                path: request.path.clone(),
265                request_body: request.body.clone(),
266                query_params: request.query_params.clone(),
267                headers: request.headers.clone(),
268            };
269
270            let error_response = self
271                .validation_generator
272                .generate_validation_error(issue, &request_context)
273                .await?;
274
275            return Ok(Response {
276                status_code: error_response.status_code,
277                body: error_response.body,
278                headers: HashMap::new(),
279            });
280        }
281
282        // Check if this is a paginated request
283        if self.is_paginated_request(request) {
284            let pagination_meta =
285                self.generate_pagination_metadata(request, session_context).await?;
286
287            let body = self.build_paginated_response(&pagination_meta, request).await?;
288
289            return Ok(Response {
290                status_code: 200,
291                body,
292                headers: HashMap::new(),
293            });
294        }
295
296        // Round 31 (#79) — `generate_response_body` now ALWAYS returns
297        // an empty object so the calling site (mockforge-openapi's
298        // router) falls back to spec-driven OpenAPI response
299        // generation. We still call it for mutation methods so any
300        // future mutation-side-effects (state tracking, drift
301        // detection) keep firing; we just don't let it shape the
302        // response body any more.
303        let response_body = if is_mutation_method {
304            self.generate_response_body(&mutation_analysis, request, session_context)
305                .await?
306        } else {
307            tracing::debug!(
308                "Skipping mutation-based response generation for {} request - using OpenAPI response generation",
309                method_upper
310            );
311            serde_json::json!({})
312        };
313
314        Ok(Response {
315            status_code: 200,
316            body: response_body,
317            headers: HashMap::new(),
318        })
319    }
320
321    /// Learn from an example pair
322    ///
323    /// Updates behavioral rules based on a new example.
324    pub async fn learn_from_example(&mut self, example: ExamplePair) -> Result<()> {
325        // Regenerate rules with new example
326        let examples = vec![example];
327        let new_rules = self.rule_generator.generate_rules_from_examples(examples).await?;
328
329        // Merge with existing rules
330        self.merge_rules(new_rules);
331
332        Ok(())
333    }
334
335    /// Get current behavior rules
336    pub fn rules(&self) -> &BehaviorRules {
337        &self.rules
338    }
339
340    /// Update behavior rules
341    pub fn update_rules(&mut self, rules: BehaviorRules) {
342        self.rules = rules;
343        // Update mutation analyzer with new rules
344        self.mutation_analyzer = MutationAnalyzer::new().with_rules(self.rules.clone());
345    }
346
347    /// Update configuration at runtime
348    ///
349    /// This allows changing MockAI configuration without recreating the instance.
350    /// Useful for hot-reloading reality level configurations.
351    ///
352    /// Note: This updates the configuration but does not regenerate rules.
353    /// For rule updates, use `update_rules()` or `learn_from_example()`.
354    pub fn update_config(&mut self, config: IntelligentBehaviorConfig) {
355        self.config = config.clone();
356
357        // Update components that depend on config
358        let behavior_config = self.config.behavior_model.clone();
359        self.validation_generator = ValidationGenerator::new(behavior_config.clone());
360        self.pagination_intelligence = PaginationIntelligence::new(behavior_config);
361
362        // Note: We don't recreate rule_generator or mutation_analyzer
363        // as they may have learned rules that should be preserved
364    }
365
366    /// Update configuration (async version for Arc<RwLock>)
367    ///
368    /// Convenience method for updating a MockAI instance wrapped in Arc<RwLock>.
369    /// This is the recommended way to update MockAI configuration at runtime.
370    ///
371    /// # Returns
372    /// `Ok(())` on success, or an error if the update fails.
373    pub async fn update_config_async(
374        this: &std::sync::Arc<tokio::sync::RwLock<Self>>,
375        config: IntelligentBehaviorConfig,
376    ) -> Result<()> {
377        let mut mockai = this.write().await;
378        mockai.update_config(config);
379        Ok(())
380    }
381
382    /// Get current configuration
383    ///
384    /// Primarily for testing purposes to verify configuration updates.
385    pub fn get_config(&self) -> &IntelligentBehaviorConfig {
386        &self.config
387    }
388
389    // ===== Private helper methods =====
390
391    /// Extract examples from OpenAPI spec
392    pub fn extract_examples_from_openapi(spec: &OpenApiSpec) -> Result<Vec<ExamplePair>> {
393        let mut examples = Vec::new();
394
395        // Use the all_paths_and_operations method
396        let path_operations = spec.all_paths_and_operations();
397
398        for (path, operations) in path_operations {
399            for (method, operation) in operations {
400                // Extract request example
401                let request = operation
402                    .request_body
403                    .as_ref()
404                    .and_then(|rb| rb.as_item())
405                    .and_then(|rb| rb.content.get("application/json"))
406                    .and_then(|media| media.example.clone());
407
408                // Extract response example
409                let response = operation.responses.responses.iter().find_map(|(status, resp)| {
410                    if let openapiv3::StatusCode::Code(200) = status {
411                        resp.as_item()
412                            .and_then(|r| r.content.get("application/json"))
413                            .and_then(|media| media.example.clone())
414                    } else {
415                        None
416                    }
417                });
418
419                examples.push(ExamplePair {
420                    method: method.clone(),
421                    path: path.clone(),
422                    request,
423                    status: 200,
424                    response,
425                    query_params: HashMap::new(),
426                    headers: HashMap::new(),
427                    metadata: HashMap::new(),
428                });
429            }
430        }
431
432        Ok(examples)
433    }
434
435    /// Check if request is paginated
436    fn is_paginated_request(&self, request: &Request) -> bool {
437        // Check for pagination parameters
438        request.query_params.keys().any(|key| {
439            matches!(
440                key.to_lowercase().as_str(),
441                "page" | "limit" | "per_page" | "offset" | "cursor"
442            )
443        })
444    }
445
446    /// Generate pagination metadata
447    async fn generate_pagination_metadata(
448        &self,
449        request: &Request,
450        session_context: &StatefulAiContext,
451    ) -> Result<PaginationMetadata> {
452        let pagination_request = PaginationRequest {
453            path: request.path.clone(),
454            query_params: request.query_params.clone(),
455            request_body: request.body.clone(),
456        };
457
458        self.pagination_intelligence
459            .generate_pagination_metadata(&pagination_request, session_context)
460            .await
461    }
462
463    /// Build paginated response
464    async fn build_paginated_response(
465        &self,
466        meta: &PaginationMetadata,
467        _request: &Request,
468    ) -> Result<Value> {
469        // Build standard paginated response
470        Ok(serde_json::json!({
471            "data": [], // Would be populated with actual data
472            "pagination": {
473                "page": meta.page,
474                "page_size": meta.page_size,
475                "total": meta.total,
476                "total_pages": meta.total_pages,
477                "has_next": meta.has_next,
478                "has_prev": meta.has_prev,
479                "offset": meta.offset,
480                "next_cursor": meta.next_cursor,
481                "prev_cursor": meta.prev_cursor,
482            }
483        }))
484    }
485
486    /// Generate response body based on mutation analysis.
487    ///
488    /// Issue #79 round 31 — Srikanth on 0.3.174 hit the vCenter
489    /// `Appliance.Recovery.Backup.SystemName.Archive_get` route (a
490    /// POST that vCenter's spec models as a Create-style mutation) and
491    /// saw the response come back as the hardcoded envelope
492    /// `{id: "generated_id", status: "created", data: <echoed body>}`,
493    /// even though the spec's 200 response promised
494    /// `Archive.Info` with six required fields. The hardcoded envelope
495    /// here was a holdover from before the OpenAPI ResponseGenerator
496    /// could shape Create/Update/Delete responses on its own — but
497    /// keeping it means MockAI silently overrides the spec for every
498    /// write request and the body Srikanth got back was missing
499    /// required fields like `comment` / `parts` / `timestamp` /
500    /// `version`.
501    ///
502    /// The fix: for any mutation kind, return an empty object. The
503    /// calling site in `mockforge-openapi`'s router (the
504    /// `if is_empty { fall through to OpenAPI response generation }`
505    /// branch) already treats `{}` as "use the spec-driven response".
506    /// So the body becomes spec-shape automatically and required
507    /// fields are populated by the same path that already handles
508    /// `NoChange` (GET/HEAD/OPTIONS). MockAI's mutation analysis still
509    /// runs (state tracking, drift detection, etc.); only the
510    /// response-body shape changes.
511    async fn generate_response_body(
512        &self,
513        _mutation: &super::mutation_analyzer::MutationAnalysis,
514        _request: &Request,
515        _session_context: &StatefulAiContext,
516    ) -> Result<Value> {
517        tracing::debug!(
518            "MockAI mutation response: returning empty object so spec-driven OpenAPI response generation populates the body (#79 r31)"
519        );
520        Ok(serde_json::json!({}))
521    }
522
523    /// Merge new rules with existing rules
524    fn merge_rules(&mut self, new_rules: BehaviorRules) {
525        // Merge consistency rules
526        self.rules.consistency_rules.extend(new_rules.consistency_rules);
527
528        // Merge schemas
529        for (key, value) in new_rules.schemas {
530            self.rules.schemas.insert(key, value);
531        }
532
533        // Merge state machines
534        for (key, value) in new_rules.state_transitions {
535            self.rules.state_transitions.insert(key, value);
536        }
537
538        // Update system prompt if new one is more descriptive
539        if new_rules.system_prompt.len() > self.rules.system_prompt.len() {
540            self.rules.system_prompt = new_rules.system_prompt;
541        }
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use serde_json::json;
549
550    #[tokio::test]
551    async fn test_is_paginated_request() {
552        // Skip test if API key is not available
553        if std::env::var("OPENAI_API_KEY").is_err() && std::env::var("ANTHROPIC_API_KEY").is_err() {
554            eprintln!("Skipping test: No API key found");
555            return;
556        }
557
558        let config = IntelligentBehaviorConfig::default();
559        let examples = vec![ExamplePair {
560            method: "GET".to_string(),
561            path: "/api/users".to_string(),
562            request: None,
563            status: 200,
564            response: Some(json!({})),
565            query_params: HashMap::new(),
566            headers: HashMap::new(),
567            metadata: HashMap::new(),
568        }];
569
570        let mockai = match MockAI::from_examples(examples, config).await {
571            Ok(m) => m,
572            Err(e) => {
573                eprintln!("Skipping test: Failed to create MockAI: {}", e);
574                return;
575            }
576        };
577
578        let mut query_params = HashMap::new();
579        query_params.insert("page".to_string(), "1".to_string());
580
581        let request = Request {
582            method: "GET".to_string(),
583            path: "/api/users".to_string(),
584            body: None,
585            query_params,
586            headers: HashMap::new(),
587        };
588
589        assert!(mockai.is_paginated_request(&request));
590    }
591
592    #[tokio::test]
593    async fn test_process_request() {
594        // Skip test if API key is not available
595        if std::env::var("OPENAI_API_KEY").is_err() && std::env::var("ANTHROPIC_API_KEY").is_err() {
596            eprintln!("Skipping test: No API key found");
597            return;
598        }
599
600        let config = IntelligentBehaviorConfig::default();
601        let examples = vec![ExamplePair {
602            method: "GET".to_string(),
603            path: "/api/users".to_string(),
604            request: None,
605            status: 200,
606            response: Some(json!({
607                "users": [],
608                "total": 0
609            })),
610            query_params: HashMap::new(),
611            headers: HashMap::new(),
612            metadata: HashMap::new(),
613        }];
614
615        let mockai = match MockAI::from_examples(examples, config).await {
616            Ok(m) => m,
617            Err(e) => {
618                eprintln!("Skipping test: Failed to create MockAI: {}", e);
619                return;
620            }
621        };
622
623        let request = Request {
624            method: "GET".to_string(),
625            path: "/api/users".to_string(),
626            body: None,
627            query_params: HashMap::new(),
628            headers: HashMap::new(),
629        };
630
631        let response = match mockai.process_request(&request).await {
632            Ok(r) => r,
633            Err(e) => {
634                eprintln!("Skipping test: Failed to process request: {}", e);
635                return;
636            }
637        };
638
639        // Verify response structure
640        assert_eq!(response.status_code, 200);
641        assert!(response.body.is_object() || response.body.is_array());
642    }
643
644    #[tokio::test]
645    async fn test_process_request_with_body() {
646        // Skip test if API key is not available
647        if std::env::var("OPENAI_API_KEY").is_err() && std::env::var("ANTHROPIC_API_KEY").is_err() {
648            eprintln!("Skipping test: No API key found");
649            return;
650        }
651
652        let config = IntelligentBehaviorConfig::default();
653        let examples = vec![ExamplePair {
654            method: "POST".to_string(),
655            path: "/api/users".to_string(),
656            request: Some(json!({
657                "name": "John Doe",
658                "email": "john@example.com"
659            })),
660            status: 201,
661            response: Some(json!({
662                "id": "123",
663                "name": "John Doe",
664                "email": "john@example.com"
665            })),
666            query_params: HashMap::new(),
667            headers: HashMap::new(),
668            metadata: HashMap::new(),
669        }];
670
671        let mockai = match MockAI::from_examples(examples, config).await {
672            Ok(m) => m,
673            Err(e) => {
674                eprintln!("Skipping test: Failed to create MockAI: {}", e);
675                return;
676            }
677        };
678
679        let request = Request {
680            method: "POST".to_string(),
681            path: "/api/users".to_string(),
682            body: Some(json!({
683                "name": "Jane Doe",
684                "email": "jane@example.com"
685            })),
686            query_params: HashMap::new(),
687            headers: HashMap::new(),
688        };
689
690        let response = match mockai.process_request(&request).await {
691            Ok(r) => r,
692            Err(e) => {
693                eprintln!("Skipping test: Failed to process request: {}", e);
694                return;
695            }
696        };
697
698        // Verify response structure
699        assert_eq!(response.status_code, 201);
700        assert!(response.body.is_object());
701    }
702
703    #[test]
704    fn test_request_creation() {
705        let mut query_params = HashMap::new();
706        query_params.insert("page".to_string(), "1".to_string());
707
708        let mut headers = HashMap::new();
709        headers.insert("Content-Type".to_string(), "application/json".to_string());
710
711        let request = Request {
712            method: "GET".to_string(),
713            path: "/api/users".to_string(),
714            body: Some(json!({"id": 1})),
715            query_params,
716            headers,
717        };
718
719        assert_eq!(request.method, "GET");
720        assert_eq!(request.path, "/api/users");
721        assert!(request.body.is_some());
722    }
723
724    #[test]
725    fn test_response_creation() {
726        let mut headers = HashMap::new();
727        headers.insert("Content-Type".to_string(), "application/json".to_string());
728
729        let response = Response {
730            status_code: 200,
731            body: json!({"message": "success"}),
732            headers,
733        };
734
735        assert_eq!(response.status_code, 200);
736        assert!(response.body.is_object());
737    }
738
739    #[test]
740    fn test_mockai_new() {
741        let config = IntelligentBehaviorConfig::default();
742        let mockai = MockAI::new(config);
743        // Just verify it can be created
744        let _ = mockai;
745    }
746
747    #[test]
748    fn test_mockai_rules() {
749        let config = IntelligentBehaviorConfig::default();
750        let mockai = MockAI::new(config);
751        let rules = mockai.rules();
752        // Just verify we can access rules
753        let _ = rules;
754    }
755
756    #[test]
757    fn test_mockai_update_rules() {
758        let config = IntelligentBehaviorConfig::default();
759        let mut mockai = MockAI::new(config);
760        let new_rules = BehaviorRules::default();
761        mockai.update_rules(new_rules);
762        // Just verify it doesn't panic
763    }
764
765    #[test]
766    fn test_mockai_get_config() {
767        let config = IntelligentBehaviorConfig::default();
768        let mockai = MockAI::new(config.clone());
769        let retrieved_config = mockai.get_config();
770        // Just verify we can access config
771        let _ = retrieved_config;
772    }
773
774    #[test]
775    fn test_mockai_update_config() {
776        let config = IntelligentBehaviorConfig::default();
777        let mut mockai = MockAI::new(config.clone());
778        let new_config = IntelligentBehaviorConfig::default();
779        mockai.update_config(new_config);
780        // Just verify it doesn't panic
781    }
782
783    #[test]
784    fn test_extract_examples_from_openapi_empty_spec() {
785        let spec_json = json!({
786            "openapi": "3.0.0",
787            "info": {
788                "title": "Test API",
789                "version": "1.0.0"
790            },
791            "paths": {}
792        });
793        let spec = OpenApiSpec::from_json(spec_json).unwrap();
794        let examples = MockAI::extract_examples_from_openapi(&spec).unwrap();
795        assert!(examples.is_empty());
796    }
797
798    #[test]
799    fn test_request_with_all_fields() {
800        let mut headers = HashMap::new();
801        headers.insert("Authorization".to_string(), "Bearer token".to_string());
802        let mut query_params = HashMap::new();
803        query_params.insert("limit".to_string(), "10".to_string());
804
805        let request = Request {
806            method: "POST".to_string(),
807            path: "/api/data".to_string(),
808            body: Some(json!({"key": "value"})),
809            query_params: query_params.clone(),
810            headers: headers.clone(),
811        };
812
813        assert_eq!(request.method, "POST");
814        assert_eq!(request.path, "/api/data");
815        assert!(request.body.is_some());
816        assert_eq!(request.query_params.get("limit"), Some(&"10".to_string()));
817        assert_eq!(request.headers.get("Authorization"), Some(&"Bearer token".to_string()));
818    }
819
820    #[test]
821    fn test_response_with_headers() {
822        let mut headers = HashMap::new();
823        headers.insert("X-Total-Count".to_string(), "100".to_string());
824        headers.insert("Content-Type".to_string(), "application/json".to_string());
825
826        let response = Response {
827            status_code: 201,
828            body: json!({"id": "123", "created": true}),
829            headers: headers.clone(),
830        };
831
832        assert_eq!(response.status_code, 201);
833        assert!(response.body.is_object());
834        assert_eq!(response.headers.len(), 2);
835        assert_eq!(response.headers.get("X-Total-Count"), Some(&"100".to_string()));
836    }
837
838    #[test]
839    fn test_request_clone() {
840        let request1 = Request {
841            method: "GET".to_string(),
842            path: "/api/test".to_string(),
843            body: Some(json!({"id": 1})),
844            query_params: HashMap::new(),
845            headers: HashMap::new(),
846        };
847        let request2 = request1.clone();
848        assert_eq!(request1.method, request2.method);
849        assert_eq!(request1.path, request2.path);
850    }
851
852    #[test]
853    fn test_request_debug() {
854        let request = Request {
855            method: "POST".to_string(),
856            path: "/api/users".to_string(),
857            body: None,
858            query_params: HashMap::new(),
859            headers: HashMap::new(),
860        };
861        let debug_str = format!("{:?}", request);
862        assert!(debug_str.contains("Request"));
863    }
864
865    #[test]
866    fn test_response_clone() {
867        let response1 = Response {
868            status_code: 200,
869            body: json!({"status": "ok"}),
870            headers: HashMap::new(),
871        };
872        let response2 = response1.clone();
873        assert_eq!(response1.status_code, response2.status_code);
874    }
875
876    #[test]
877    fn test_response_debug() {
878        let response = Response {
879            status_code: 404,
880            body: json!({"error": "Not found"}),
881            headers: HashMap::new(),
882        };
883        let debug_str = format!("{:?}", response);
884        assert!(debug_str.contains("Response"));
885    }
886
887    #[test]
888    fn test_request_with_empty_fields() {
889        let request = Request {
890            method: "GET".to_string(),
891            path: "/api/test".to_string(),
892            body: None,
893            query_params: HashMap::new(),
894            headers: HashMap::new(),
895        };
896        assert!(request.body.is_none());
897        assert!(request.query_params.is_empty());
898        assert!(request.headers.is_empty());
899    }
900
901    #[test]
902    fn test_response_with_empty_headers() {
903        let response = Response {
904            status_code: 200,
905            body: json!({"data": []}),
906            headers: HashMap::new(),
907        };
908        assert!(response.headers.is_empty());
909        assert!(response.body.is_object());
910    }
911
912    #[test]
913    fn test_request_with_complex_body() {
914        let request = Request {
915            method: "PUT".to_string(),
916            path: "/api/users/123".to_string(),
917            body: Some(json!({
918                "name": "John Doe",
919                "email": "john@example.com",
920                "metadata": {
921                    "role": "admin",
922                    "permissions": ["read", "write"]
923                }
924            })),
925            query_params: HashMap::new(),
926            headers: HashMap::new(),
927        };
928        assert!(request.body.is_some());
929        let body = request.body.unwrap();
930        assert!(body.is_object());
931        assert!(body.get("metadata").is_some());
932    }
933
934    #[test]
935    fn test_response_with_array_body() {
936        let response = Response {
937            status_code: 200,
938            body: json!([
939                {"id": 1, "name": "Alice"},
940                {"id": 2, "name": "Bob"},
941                {"id": 3, "name": "Charlie"}
942            ]),
943            headers: HashMap::new(),
944        };
945        assert!(response.body.is_array());
946        let array = response.body.as_array().unwrap();
947        assert_eq!(array.len(), 3);
948    }
949
950    #[test]
951    fn test_request_with_multiple_query_params() {
952        let mut query_params = HashMap::new();
953        query_params.insert("page".to_string(), "1".to_string());
954        query_params.insert("limit".to_string(), "20".to_string());
955        query_params.insert("sort".to_string(), "name".to_string());
956        query_params.insert("order".to_string(), "asc".to_string());
957
958        let request = Request {
959            method: "GET".to_string(),
960            path: "/api/users".to_string(),
961            body: None,
962            query_params: query_params.clone(),
963            headers: HashMap::new(),
964        };
965
966        assert_eq!(request.query_params.len(), 4);
967        assert_eq!(request.query_params.get("page"), Some(&"1".to_string()));
968        assert_eq!(request.query_params.get("limit"), Some(&"20".to_string()));
969    }
970
971    #[test]
972    fn test_response_with_multiple_headers() {
973        let mut headers = HashMap::new();
974        headers.insert("Content-Type".to_string(), "application/json".to_string());
975        headers.insert("X-Request-ID".to_string(), "req-123".to_string());
976        headers.insert("X-Rate-Limit-Remaining".to_string(), "99".to_string());
977        headers.insert("Cache-Control".to_string(), "no-cache".to_string());
978
979        let response = Response {
980            status_code: 200,
981            body: json!({"data": "test"}),
982            headers: headers.clone(),
983        };
984
985        assert_eq!(response.headers.len(), 4);
986        assert_eq!(response.headers.get("X-Request-ID"), Some(&"req-123".to_string()));
987    }
988
989    #[test]
990    fn test_request_different_methods() {
991        let methods = vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
992        for method in methods {
993            let request = Request {
994                method: method.to_string(),
995                path: "/api/test".to_string(),
996                body: None,
997                query_params: HashMap::new(),
998                headers: HashMap::new(),
999            };
1000            assert_eq!(request.method, method);
1001        }
1002    }
1003
1004    #[test]
1005    fn test_response_different_status_codes() {
1006        let status_codes = vec![200, 201, 204, 400, 401, 403, 404, 500, 503];
1007        for status_code in status_codes {
1008            let response = Response {
1009                status_code,
1010                body: json!({"status": status_code}),
1011                headers: HashMap::new(),
1012            };
1013            assert_eq!(response.status_code, status_code);
1014        }
1015    }
1016
1017    /// Round 31 (#79) — Srikanth on 0.3.174: MockAI was returning
1018    /// `{id: "generated_id", status: "created", data: ...}` for POST
1019    /// (and similar envelopes for PUT / PATCH / DELETE), overriding
1020    /// the response body the OpenAPI spec described. The vCenter
1021    /// `Archive.Info` schema requires six fields; MockAI's three-field
1022    /// envelope was missing five of them, leading to
1023    /// `response body root: required field missing: "comment"` reports
1024    /// from the bench. Fixed by making `generate_response_body` return
1025    /// `{}` for every mutation kind so the calling site's
1026    /// `is_empty → use OpenAPI ResponseGenerator` fall-through kicks
1027    /// in. Regression guard: every variant must return an empty
1028    /// object.
1029    #[tokio::test]
1030    async fn generate_response_body_returns_empty_for_all_mutation_kinds() {
1031        use crate::intelligent_behavior::mutation_analyzer::{MutationAnalysis, MutationType};
1032        let mockai = MockAI::new(IntelligentBehaviorConfig::default());
1033        let req = Request {
1034            method: "POST".to_string(),
1035            path: "/api/anything".to_string(),
1036            body: Some(json!({"name": "X"})),
1037            query_params: HashMap::new(),
1038            headers: HashMap::new(),
1039        };
1040        let ctx = StatefulAiContext::new("test-session", IntelligentBehaviorConfig::default());
1041
1042        for kind in [
1043            MutationType::NoChange,
1044            MutationType::Create,
1045            MutationType::Update,
1046            MutationType::PartialUpdate,
1047            MutationType::Delete,
1048        ] {
1049            let kind_label = format!("{:?}", kind);
1050            let mutation = MutationAnalysis {
1051                mutation_type: kind,
1052                changed_fields: Vec::new(),
1053                added_fields: Vec::new(),
1054                removed_fields: Vec::new(),
1055                validation_issues: Vec::new(),
1056                confidence: 1.0,
1057            };
1058            let body = mockai
1059                .generate_response_body(&mutation, &req, &ctx)
1060                .await
1061                .expect("generate_response_body should not error");
1062            assert_eq!(
1063                body,
1064                json!({}),
1065                "MockAI must return empty object for mutation kind {} so the calling \
1066                 site falls back to spec-driven response generation (regression #79 r31). \
1067                 Got: {:?}",
1068                kind_label,
1069                body
1070            );
1071        }
1072    }
1073}