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