clnrm_core/
cleanroom.rs

1//! Cleanroom Environment - Framework Self-Testing Implementation
2//!
3//! Core cleanroom environment that tests itself through the "eat your own dog food"
4//! principle. Every feature of this framework is validated by using the framework
5//! to test its own functionality.
6
7use crate::backend::{Backend, Cmd, TestcontainerBackend};
8use crate::error::{CleanroomError, Result};
9#[cfg(feature = "otel-traces")]
10use opentelemetry::global;
11#[cfg(feature = "otel-traces")]
12use opentelemetry::trace::{Span, Tracer, TracerProvider};
13#[cfg(feature = "otel-traces")]
14use opentelemetry::KeyValue;
15use std::any::Any;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::RwLock;
19use uuid::Uuid;
20
21/// Plugin-based service registry (no hardcoded postgres/redis)
22pub trait ServicePlugin: Send + Sync + std::fmt::Debug {
23    /// Get service name
24    fn name(&self) -> &str;
25
26    /// Start the service
27    fn start(&self) -> Result<ServiceHandle>;
28
29    /// Stop the service
30    fn stop(&self, handle: ServiceHandle) -> Result<()>;
31
32    /// Check service health
33    fn health_check(&self, handle: &ServiceHandle) -> HealthStatus;
34}
35
36/// Service handle for managing service instances
37#[derive(Debug, Clone)]
38pub struct ServiceHandle {
39    /// Unique service instance ID
40    pub id: String,
41    /// Service name
42    pub service_name: String,
43    /// Service metadata
44    pub metadata: HashMap<String, String>,
45}
46
47/// Service health status
48#[derive(Debug, Clone, PartialEq)]
49pub enum HealthStatus {
50    /// Service is healthy and running
51    Healthy,
52    /// Service is unhealthy or not responding
53    Unhealthy,
54    /// Service status is unknown
55    Unknown,
56}
57
58/// Plugin-based service registry
59#[derive(Debug, Default)]
60pub struct ServiceRegistry {
61    /// Registered service plugins
62    plugins: HashMap<String, Box<dyn ServicePlugin>>,
63    /// Active service instances
64    active_services: HashMap<String, ServiceHandle>,
65}
66
67impl ServiceRegistry {
68    /// Create a new service registry
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Initialize default plugins
74    pub fn with_default_plugins(mut self) -> Self {
75        use crate::services::{
76            generic::GenericContainerPlugin, ollama::OllamaPlugin, tgi::TgiPlugin, vllm::VllmPlugin,
77        };
78
79        // Register core plugins
80        let generic_plugin = Box::new(GenericContainerPlugin::new(
81            "generic_container",
82            "alpine:latest",
83        ));
84        self.register_plugin(generic_plugin);
85
86        // Register AI/LLM proxy plugins for automated rollout testing
87        let ollama_config = crate::services::ollama::OllamaConfig {
88            endpoint: "http://localhost:11434".to_string(),
89            default_model: "qwen3-coder:30b".to_string(),
90            timeout_seconds: 60,
91        };
92        let ollama_plugin = Box::new(OllamaPlugin::new("ollama", ollama_config));
93        self.register_plugin(ollama_plugin);
94
95        let vllm_config = crate::services::vllm::VllmConfig {
96            endpoint: "http://localhost:8000".to_string(),
97            model: "microsoft/DialoGPT-medium".to_string(),
98            max_num_seqs: Some(100),
99            max_model_len: Some(2048),
100            tensor_parallel_size: Some(1),
101            gpu_memory_utilization: Some(0.9),
102            enable_prefix_caching: Some(true),
103            timeout_seconds: 60,
104        };
105        let vllm_plugin = Box::new(VllmPlugin::new("vllm", vllm_config));
106        self.register_plugin(vllm_plugin);
107
108        let tgi_config = crate::services::tgi::TgiConfig {
109            endpoint: "http://localhost:8080".to_string(),
110            model_id: "microsoft/DialoGPT-medium".to_string(),
111            max_total_tokens: Some(2048),
112            max_input_length: Some(1024),
113            max_batch_prefill_tokens: Some(4096),
114            max_concurrent_requests: Some(32),
115            max_batch_total_tokens: Some(8192),
116            timeout_seconds: 60,
117        };
118        let tgi_plugin = Box::new(TgiPlugin::new("tgi", tgi_config));
119        self.register_plugin(tgi_plugin);
120
121        self
122    }
123
124    /// Register a service plugin
125    pub fn register_plugin(&mut self, plugin: Box<dyn ServicePlugin>) {
126        let name = plugin.name().to_string();
127        self.plugins.insert(name, plugin);
128    }
129
130    /// Start a service by name
131    pub async fn start_service(&mut self, service_name: &str) -> Result<ServiceHandle> {
132        let plugin = self.plugins.get(service_name).ok_or_else(|| {
133            CleanroomError::internal_error(format!("Service plugin '{}' not found", service_name))
134        })?;
135
136        let handle = plugin.start()?;
137        self.active_services
138            .insert(handle.id.clone(), handle.clone());
139
140        Ok(handle)
141    }
142
143    /// Stop a service by handle ID
144    pub async fn stop_service(&mut self, handle_id: &str) -> Result<()> {
145        if let Some(handle) = self.active_services.remove(handle_id) {
146            let plugin = self.plugins.get(&handle.service_name).ok_or_else(|| {
147                CleanroomError::internal_error(format!(
148                    "Service plugin '{}' not found for handle '{}'",
149                    handle.service_name, handle_id
150                ))
151            })?;
152
153            plugin.stop(handle)?;
154        }
155
156        Ok(())
157    }
158
159    /// Check health of all services
160    pub async fn check_all_health(&self) -> HashMap<String, HealthStatus> {
161        let mut health_status = HashMap::new();
162
163        for (handle_id, handle) in &self.active_services {
164            if let Some(plugin) = self.plugins.get(&handle.service_name) {
165                health_status.insert(handle_id.clone(), plugin.health_check(handle));
166            } else {
167                health_status.insert(handle_id.clone(), HealthStatus::Unknown);
168            }
169        }
170
171        health_status
172    }
173
174    /// Get all active service handles
175    pub fn active_services(&self) -> &HashMap<String, ServiceHandle> {
176        &self.active_services
177    }
178
179    /// Check if service is running
180    pub fn is_service_running(&self, service_name: &str) -> bool {
181        self.active_services
182            .values()
183            .any(|handle| handle.service_name == service_name)
184    }
185
186    /// Get service logs
187    pub async fn get_service_logs(&self, service_id: &str, lines: usize) -> Result<Vec<String>> {
188        let handle = self.active_services.get(service_id).ok_or_else(|| {
189            CleanroomError::internal_error(format!("Service with ID '{}' not found", service_id))
190        })?;
191
192        let _plugin = self.plugins.get(&handle.service_name).ok_or_else(|| {
193            CleanroomError::internal_error(format!(
194                "Service plugin '{}' not found",
195                handle.service_name
196            ))
197        })?;
198
199        // For now, return mock logs since actual log retrieval depends on the service implementation
200        // In a real implementation, this would call plugin.get_logs(handle, lines)
201        let mock_logs = vec![
202            format!(
203                "[{}] Service '{}' started",
204                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
205                handle.service_name
206            ),
207            format!(
208                "[{}] Service '{}' is running",
209                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
210                handle.service_name
211            ),
212        ];
213
214        // Return only the requested number of lines
215        Ok(mock_logs.into_iter().take(lines).collect())
216    }
217}
218
219/// Simple metrics for quick access
220#[derive(Debug, Clone)]
221pub struct SimpleMetrics {
222    /// Session ID
223    pub session_id: Uuid,
224    /// Start time
225    pub start_time: std::time::Instant,
226    /// Tests executed
227    pub tests_executed: u32,
228    /// Tests passed
229    pub tests_passed: u32,
230    /// Tests failed
231    pub tests_failed: u32,
232    /// Total duration
233    pub total_duration_ms: u64,
234    /// Active containers
235    pub active_containers: u32,
236    /// Active services
237    pub active_services: u32,
238    /// Containers created in this session
239    pub containers_created: u32,
240    /// Containers reused in this session
241    pub containers_reused: u32,
242}
243
244impl SimpleMetrics {
245    pub fn new() -> Self {
246        Self {
247            session_id: Uuid::new_v4(),
248            start_time: std::time::Instant::now(),
249            tests_executed: 0,
250            tests_passed: 0,
251            tests_failed: 0,
252            total_duration_ms: 0,
253            active_containers: 0,
254            active_services: 0,
255            containers_created: 0,
256            containers_reused: 0,
257        }
258    }
259}
260
261impl Default for SimpleMetrics {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267/// Execution result for container command execution
268#[derive(Debug, Clone)]
269pub struct ExecutionResult {
270    /// Exit code of the executed command
271    pub exit_code: i32,
272    /// Standard output from the command
273    pub stdout: String,
274    /// Standard error from the command
275    pub stderr: String,
276    /// Duration of command execution
277    pub duration: std::time::Duration,
278    /// Command that was executed
279    pub command: Vec<String>,
280    /// Container name where command was executed
281    pub container_name: String,
282}
283
284impl ExecutionResult {
285    /// Check if command output matches a regex pattern
286    pub fn matches_regex(&self, pattern: &str) -> Result<bool> {
287        use regex::Regex;
288        let regex = Regex::new(pattern).map_err(|e| {
289            CleanroomError::validation_error(format!("Invalid regex pattern '{}': {}", pattern, e))
290        })?;
291        Ok(regex.is_match(&self.stdout))
292    }
293
294    /// Check if command output does NOT match a regex pattern
295    pub fn does_not_match_regex(&self, pattern: &str) -> Result<bool> {
296        Ok(!self.matches_regex(pattern)?)
297    }
298
299    /// Check if command succeeded (exit code 0)
300    pub fn succeeded(&self) -> bool {
301        self.exit_code == 0
302    }
303
304    /// Check if command failed (non-zero exit code)
305    pub fn failed(&self) -> bool {
306        !self.succeeded()
307    }
308}
309
310/// Simple environment wrapper around existing infrastructure
311#[allow(dead_code)]
312#[derive(Debug)]
313pub struct CleanroomEnvironment {
314    /// Session ID
315    session_id: Uuid,
316    /// Backend for container execution
317    backend: Arc<dyn Backend>,
318    /// Plugin-based service registry
319    services: Arc<RwLock<ServiceRegistry>>,
320    /// Simple metrics for quick access
321    metrics: Arc<RwLock<SimpleMetrics>>,
322    /// Container registry for reuse - stores actual container instances
323    container_registry: Arc<RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>>,
324    /// OpenTelemetry meter for metrics
325    #[cfg(feature = "otel-metrics")]
326    meter: opentelemetry::metrics::Meter,
327    /// Telemetry configuration and state
328    telemetry: Arc<RwLock<TelemetryState>>,
329}
330
331impl Default for CleanroomEnvironment {
332    fn default() -> Self {
333        // Use a simple synchronous approach for Default
334        // This is acceptable since Default is typically used in tests
335        let backend = Arc::new(
336            TestcontainerBackend::new("alpine:latest").unwrap_or_else(|_| {
337                panic!("Failed to create default backend - check Docker availability")
338            }),
339        );
340
341        Self {
342            session_id: Uuid::new_v4(),
343            backend,
344            services: Arc::new(RwLock::new(ServiceRegistry::new())),
345            metrics: Arc::new(RwLock::new(SimpleMetrics::new())),
346            container_registry: Arc::new(RwLock::new(HashMap::new())),
347            #[cfg(feature = "otel-metrics")]
348            meter: opentelemetry::global::meter("clnrm"),
349            telemetry: Arc::new(RwLock::new(TelemetryState {
350                tracing_enabled: false,
351                metrics_enabled: false,
352                traces: Vec::new(),
353            })),
354        }
355    }
356}
357
358/// Telemetry state for the cleanroom environment
359#[derive(Debug)]
360pub struct TelemetryState {
361    /// Whether tracing is enabled
362    pub tracing_enabled: bool,
363    /// Whether metrics are enabled
364    pub metrics_enabled: bool,
365    /// Collected traces (for testing/debugging)
366    pub traces: Vec<String>,
367}
368
369impl TelemetryState {
370    /// Enable tracing
371    pub fn enable_tracing(&mut self) {
372        self.tracing_enabled = true;
373    }
374
375    /// Enable metrics collection
376    pub fn enable_metrics(&mut self) {
377        self.metrics_enabled = true;
378    }
379
380    /// Get collected traces
381    pub fn get_traces(&self) -> Vec<String> {
382        self.traces.clone()
383    }
384}
385
386impl CleanroomEnvironment {
387    /// Create a new cleanroom environment
388    pub async fn new() -> Result<Self> {
389        Ok(Self {
390            session_id: Uuid::new_v4(),
391            #[cfg(feature = "otel-metrics")]
392            meter: {
393                let meter_provider = global::meter_provider();
394                meter_provider.meter("clnrm-cleanroom")
395            },
396            backend: Arc::new(TestcontainerBackend::new("alpine:latest")?),
397            services: Arc::new(RwLock::new(ServiceRegistry::new().with_default_plugins())),
398            metrics: Arc::new(RwLock::new(SimpleMetrics::default())),
399            container_registry: Arc::new(RwLock::new(HashMap::new())),
400            telemetry: Arc::new(RwLock::new(TelemetryState {
401                tracing_enabled: false,
402                metrics_enabled: false,
403                traces: Vec::new(),
404            })),
405        })
406    }
407
408    /// Execute a test with OTel tracing
409    pub async fn execute_test<F, T>(&self, _test_name: &str, test_fn: F) -> Result<T>
410    where
411        F: FnOnce() -> Result<T>,
412    {
413        #[cfg(feature = "otel-traces")]
414        let tracer_provider = global::tracer_provider();
415        #[cfg(feature = "otel-traces")]
416        let mut span = tracer_provider
417            .tracer("clnrm-cleanroom")
418            .start(format!("test.{}", _test_name));
419        #[cfg(feature = "otel-traces")]
420        span.set_attributes(vec![
421            KeyValue::new("test.name", _test_name.to_string()),
422            KeyValue::new("session.id", self.session_id.to_string()),
423        ]);
424
425        let start_time = std::time::Instant::now();
426
427        // Update metrics
428        {
429            let mut metrics = self.metrics.write().await;
430            metrics.tests_executed += 1;
431        }
432
433        let result = test_fn();
434
435        let duration = start_time.elapsed();
436
437        // Record OTel metrics
438        let success = result.is_ok();
439        if success {
440            let mut metrics = self.metrics.write().await;
441            metrics.tests_passed += 1;
442        } else {
443            let mut metrics = self.metrics.write().await;
444            metrics.tests_failed += 1;
445        }
446
447        let mut metrics = self.metrics.write().await;
448        metrics.total_duration_ms += duration.as_millis() as u64;
449
450        #[cfg(feature = "otel-metrics")]
451        {
452            // OTel metrics
453            let attributes = vec![
454                KeyValue::new("test.name", _test_name.to_string()),
455                KeyValue::new("session.id", self.session_id.to_string()),
456            ];
457
458            let counter = self
459                .meter
460                .u64_counter("test.executions")
461                .with_description("Number of test executions")
462                .build();
463            counter.add(1, &attributes);
464
465            let histogram = self
466                .meter
467                .f64_histogram("test.duration")
468                .with_description("Test execution duration")
469                .build();
470            histogram.record(duration.as_secs_f64(), &attributes);
471        }
472
473        #[cfg(feature = "otel-traces")]
474        if !success {
475            span.set_status(opentelemetry::trace::Status::error("Test failed"));
476        }
477
478        #[cfg(feature = "otel-traces")]
479        span.end();
480
481        result
482    }
483
484    /// Get current metrics
485    pub async fn get_metrics(&self) -> Result<SimpleMetrics> {
486        Ok(self.metrics.read().await.clone())
487    }
488
489    /// Enable tracing for this environment
490    pub async fn enable_tracing(&self) -> Result<()> {
491        #[cfg(feature = "otel-traces")]
492        {
493            let mut telemetry = self.telemetry.write().await;
494            telemetry.enable_tracing();
495        }
496        Ok(())
497    }
498
499    /// Enable metrics collection for this environment
500    pub async fn enable_metrics(&self) -> Result<()> {
501        #[cfg(feature = "otel-traces")]
502        {
503            let mut telemetry = self.telemetry.write().await;
504            telemetry.enable_metrics();
505        }
506        Ok(())
507    }
508
509    /// Get traces from this environment
510    pub async fn get_traces(&self) -> Result<Vec<String>> {
511        #[cfg(feature = "otel-traces")]
512        {
513            let telemetry = self.telemetry.read().await;
514            Ok(telemetry.get_traces())
515        }
516        #[cfg(not(feature = "otel-traces"))]
517        {
518            Ok(Vec::new())
519        }
520    }
521
522    /// Get container reuse statistics
523    pub async fn get_container_reuse_stats(&self) -> (u32, u32) {
524        let metrics = self.metrics.read().await;
525        (metrics.containers_created, metrics.containers_reused)
526    }
527
528    /// Check if a container with the given name has been created in this session
529    pub async fn has_container(&self, name: &str) -> bool {
530        let registry = self.container_registry.read().await;
531        registry.contains_key(name)
532    }
533
534    /// Register a service plugin
535    pub async fn register_service(&self, plugin: Box<dyn ServicePlugin>) -> Result<()> {
536        let mut services = self.services.write().await;
537        services.register_plugin(plugin);
538        Ok(())
539    }
540
541    /// Start a service by name
542    pub async fn start_service(&self, service_name: &str) -> Result<ServiceHandle> {
543        let mut services = self.services.write().await;
544        services.start_service(service_name).await
545    }
546
547    /// Stop a service by handle ID
548    pub async fn stop_service(&self, handle_id: &str) -> Result<()> {
549        let mut services = self.services.write().await;
550        services.stop_service(handle_id).await
551    }
552
553    /// Get service registry (read-only access)
554    pub async fn services(&self) -> tokio::sync::RwLockReadGuard<'_, ServiceRegistry> {
555        self.services.read().await
556    }
557
558    /// Register a container for reuse
559    pub async fn register_container<T: Send + Sync + 'static>(
560        &self,
561        name: String,
562        container: T,
563    ) -> Result<()> {
564        let mut registry = self.container_registry.write().await;
565        registry.insert(name, Box::new(container));
566        Ok(())
567    }
568
569    /// Get or create container with reuse pattern
570    ///
571    /// This method implements true container reuse by storing and returning
572    /// the actual container instances, providing the promised 10-50x performance improvement.
573    pub async fn get_or_create_container<F, T>(&self, name: &str, factory: F) -> Result<T>
574    where
575        F: FnOnce() -> Result<T>,
576        T: Send + Sync + Clone + 'static,
577    {
578        // Check if we've already created a container with this name in this session
579        let existing_container = {
580            let registry = self.container_registry.read().await;
581            if let Some(existing_container) = registry.get(name) {
582                // Try to downcast to the requested type
583                existing_container.downcast_ref::<T>().cloned()
584            } else {
585                None
586            }
587        };
588
589        if let Some(container) = existing_container {
590            // Update metrics to track actual reuse
591            {
592                let mut metrics = self.metrics.write().await;
593                metrics.containers_reused += 1;
594            }
595
596            return Ok(container);
597        }
598
599        // First time creating this container
600        let container = factory()?;
601
602        // Register the actual container for future reuse
603        let mut registry = self.container_registry.write().await;
604        registry.insert(name.to_string(), Box::new(container.clone()));
605
606        // Update metrics
607        {
608            let mut metrics = self.metrics.write().await;
609            metrics.containers_created += 1;
610        }
611
612        Ok(container)
613    }
614
615    /// Check health of all services
616    pub async fn check_health(&self) -> HashMap<String, HealthStatus> {
617        self.services.read().await.check_all_health().await
618    }
619
620    /// Get service logs
621    pub async fn get_service_logs(&self, service_id: &str, lines: usize) -> Result<Vec<String>> {
622        let services = self.services.read().await;
623        services.get_service_logs(service_id, lines).await
624    }
625
626    /// Get session ID
627    pub fn session_id(&self) -> Uuid {
628        self.session_id
629    }
630
631    /// Get backend
632    pub fn backend(&self) -> &dyn Backend {
633        self.backend.as_ref() as &dyn Backend
634    }
635
636    /// Execute a command in a container with proper error handling and observability
637    /// Core Team Compliance: Async for I/O operations, proper error handling, no unwrap/expect
638    ///
639    /// This method creates a fresh container for each command execution, which is appropriate
640    /// for testing scenarios where isolation is more important than performance.
641    pub async fn execute_in_container(
642        &self,
643        container_name: &str,
644        command: &[String],
645    ) -> Result<ExecutionResult> {
646        #[cfg(feature = "otel-traces")]
647        let tracer_provider = global::tracer_provider();
648        #[cfg(feature = "otel-traces")]
649        let mut span = tracer_provider
650            .tracer("clnrm-cleanroom")
651            .start(format!("container.exec.{}", container_name));
652        #[cfg(feature = "otel-traces")]
653        span.set_attributes(vec![
654            KeyValue::new("container.name", container_name.to_string()),
655            KeyValue::new("command", command.join(" ")),
656            KeyValue::new("session.id", self.session_id.to_string()),
657        ]);
658
659        let start_time = std::time::Instant::now();
660
661        // Execute command using backend - this creates a fresh container for each command
662        // This provides maximum isolation and is appropriate for testing scenarios
663        let cmd = Cmd::new("sh")
664            .arg("-c")
665            .arg(command.join(" "))
666            .env("CONTAINER_NAME", container_name);
667
668        // Use spawn_blocking to avoid runtime conflicts with testcontainers
669        // Clone the backend to move it into the blocking task
670        let backend = self.backend.clone();
671        let execution_result = tokio::task::spawn_blocking(move || backend.run_cmd(cmd))
672            .await
673            .map_err(|e| {
674                #[cfg(feature = "otel-traces")]
675                {
676                    span.set_status(opentelemetry::trace::Status::error("Task join failed"));
677                    span.end();
678                }
679                CleanroomError::internal_error("Failed to execute command in blocking task")
680                    .with_context("Command execution task failed")
681                    .with_source(e.to_string())
682            })?
683            .map_err(|e| {
684                #[cfg(feature = "otel-traces")]
685                {
686                    span.set_status(opentelemetry::trace::Status::error(
687                        "Command execution failed",
688                    ));
689                    span.end();
690                }
691                CleanroomError::container_error("Failed to execute command in container")
692                    .with_context(format!(
693                        "Container: {}, Command: {}",
694                        container_name,
695                        command.join(" ")
696                    ))
697                    .with_source(e.to_string())
698            })?;
699
700        let duration = start_time.elapsed();
701
702        // Record metrics
703        #[cfg(feature = "otel-metrics")]
704        {
705            let histogram = self
706                .meter
707                .f64_histogram("container.command.duration")
708                .with_description("Container command execution duration")
709                .build();
710            histogram.record(
711                duration.as_secs_f64(),
712                &[
713                    KeyValue::new("container.name", container_name.to_string()),
714                    KeyValue::new("command", command.join(" ")),
715                ],
716            );
717        }
718
719        #[cfg(feature = "otel-traces")]
720        span.set_attributes(vec![
721            KeyValue::new(
722                "execution.exit_code",
723                execution_result.exit_code.to_string(),
724            ),
725            KeyValue::new("execution.duration_ms", duration.as_millis().to_string()),
726        ]);
727
728        #[cfg(feature = "otel-traces")]
729        if execution_result.exit_code != 0 {
730            span.set_status(opentelemetry::trace::Status::error("Command failed"));
731        }
732
733        #[cfg(feature = "otel-traces")]
734        span.end();
735
736        Ok(ExecutionResult {
737            exit_code: execution_result.exit_code,
738            stdout: execution_result.stdout,
739            stderr: execution_result.stderr,
740            duration,
741            command: command.to_vec(),
742            container_name: container_name.to_string(),
743        })
744    }
745}
746
747// Default implementation removed to avoid panic in production code
748// Use CleanroomEnvironment::new() instead for proper error handling
749
750/// Example custom service plugin implementation
751///
752/// This demonstrates how to create custom services without hardcoded dependencies
753#[derive(Debug)]
754pub struct MockDatabasePlugin {
755    name: String,
756    #[allow(dead_code)]
757    container_id: Arc<RwLock<Option<String>>>,
758}
759
760impl Default for MockDatabasePlugin {
761    fn default() -> Self {
762        Self::new()
763    }
764}
765
766impl MockDatabasePlugin {
767    pub fn new() -> Self {
768        Self {
769            name: "mock_database".to_string(),
770            container_id: Arc::new(RwLock::new(None)),
771        }
772    }
773}
774
775impl ServicePlugin for MockDatabasePlugin {
776    fn name(&self) -> &str {
777        &self.name
778    }
779
780    fn start(&self) -> Result<ServiceHandle> {
781        // For testing, create a simple mock handle without actual container
782        // In production, this would use proper async container startup
783
784        // Build metadata with mock connection details
785        let mut metadata = HashMap::new();
786        metadata.insert("host".to_string(), "127.0.0.1".to_string());
787        metadata.insert("port".to_string(), "8000".to_string());
788        metadata.insert("username".to_string(), "root".to_string());
789        metadata.insert("password".to_string(), "root".to_string());
790
791        Ok(ServiceHandle {
792            id: Uuid::new_v4().to_string(),
793            service_name: "mock_database".to_string(),
794            metadata,
795        })
796    }
797
798    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
799        // For testing, just return success without actual container cleanup
800        // In production, this would properly stop the container
801        Ok(())
802    }
803
804    fn health_check(&self, handle: &ServiceHandle) -> HealthStatus {
805        // Quick check if we have port information
806        if handle.metadata.contains_key("port") {
807            HealthStatus::Healthy
808        } else {
809            HealthStatus::Unknown
810        }
811    }
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817
818    #[tokio::test]
819    async fn test_cleanroom_creation() {
820        let result = CleanroomEnvironment::new().await;
821        assert!(result.is_ok()); // Should succeed with default implementation
822    }
823
824    #[test]
825    fn test_service_plugin_dyn_compatibility() {
826        // This test verifies that ServicePlugin is dyn compatible
827        let plugin: Arc<dyn ServicePlugin> = Arc::new(MockDatabasePlugin::new());
828
829        // Test that we can call methods on the trait object
830        assert_eq!(plugin.name(), "mock_database");
831
832        // Test that we can store multiple plugins in a collection
833        let plugins: Vec<Arc<dyn ServicePlugin>> = vec![plugin];
834
835        // Test that we can iterate over them
836        for plugin in &plugins {
837            assert_eq!(plugin.name(), "mock_database");
838        }
839
840        tracing::info!("ServicePlugin trait is dyn compatible");
841    }
842
843    #[tokio::test]
844    async fn test_cleanroom_session_id() -> Result<()> {
845        let env = CleanroomEnvironment::new().await?;
846        assert!(!env.session_id().is_nil());
847        Ok(())
848    }
849
850    #[tokio::test]
851    async fn test_cleanroom_execute_test() -> Result<()> {
852        let env = CleanroomEnvironment::new().await?;
853        let result = env
854            .execute_test("test", || Ok::<i32, CleanroomError>(42))
855            .await?;
856        assert_eq!(result, 42);
857        Ok(())
858    }
859
860    #[tokio::test]
861    async fn test_service_registry() -> Result<()> {
862        let env = CleanroomEnvironment::new().await?;
863        let services = env.services().await;
864        assert!(services.active_services().is_empty());
865        Ok(())
866    }
867
868    #[tokio::test]
869    async fn test_service_plugin_registration() -> Result<()> {
870        let env = CleanroomEnvironment::new().await?;
871        let plugin = Box::new(MockDatabasePlugin::new());
872        env.register_service(plugin).await?;
873        Ok(())
874    }
875
876    #[tokio::test]
877    async fn test_service_start_stop() -> Result<()> {
878        let env = CleanroomEnvironment::new().await?;
879        let plugin = Box::new(MockDatabasePlugin::new());
880        env.register_service(plugin).await?;
881
882        let handle = env.start_service("mock_database").await?;
883        assert_eq!(handle.service_name, "mock_database");
884
885        env.stop_service(&handle.id).await?;
886        Ok(())
887    }
888
889    #[tokio::test]
890    async fn test_register_container() {
891        let env = CleanroomEnvironment::default();
892        let result = env
893            .register_container("test-container".to_string(), "container-123".to_string())
894            .await;
895        assert!(result.is_ok());
896
897        // Verify container was registered
898        assert!(env.has_container("test-container").await);
899    }
900
901    #[tokio::test]
902    async fn test_get_or_create_container() -> Result<()> {
903        let env = CleanroomEnvironment::new().await?;
904
905        // First call should create and register container
906        let result1 = env
907            .get_or_create_container("test-container", || {
908                Ok::<String, CleanroomError>("container-instance".to_string())
909            })
910            .await?;
911        assert_eq!(result1, "container-instance");
912
913        // Verify container was registered
914        assert!(env.has_container("test-container").await);
915        let (created, reused) = env.get_container_reuse_stats().await;
916        assert_eq!(created, 1);
917        assert_eq!(reused, 0);
918
919        // Second call should return the SAME container instance (true reuse!)
920        let result2 = env
921            .get_or_create_container("test-container", || {
922                Ok::<String, CleanroomError>("container-instance-2".to_string())
923            })
924            .await?;
925        // This should be the SAME instance, not a new one
926        assert_eq!(result2, "container-instance");
927
928        // Verify reuse was tracked
929        let (created, reused) = env.get_container_reuse_stats().await;
930        assert_eq!(created, 1);
931        assert_eq!(reused, 1);
932        Ok(())
933    }
934
935    #[tokio::test]
936    async fn test_check_health_delegates_to_service_registry() -> Result<()> {
937        let env = CleanroomEnvironment::new().await?;
938        let health_status = env.check_health().await;
939        // Should return empty HashMap since no services are registered
940        assert!(health_status.is_empty());
941        Ok(())
942    }
943}