1use 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
21pub trait ServicePlugin: Send + Sync + std::fmt::Debug {
23 fn name(&self) -> &str;
25
26 fn start(&self) -> Result<ServiceHandle>;
28
29 fn stop(&self, handle: ServiceHandle) -> Result<()>;
31
32 fn health_check(&self, handle: &ServiceHandle) -> HealthStatus;
34}
35
36#[derive(Debug, Clone)]
38pub struct ServiceHandle {
39 pub id: String,
41 pub service_name: String,
43 pub metadata: HashMap<String, String>,
45}
46
47#[derive(Debug, Clone, PartialEq)]
49pub enum HealthStatus {
50 Healthy,
52 Unhealthy,
54 Unknown,
56}
57
58#[derive(Debug, Default)]
60pub struct ServiceRegistry {
61 plugins: HashMap<String, Box<dyn ServicePlugin>>,
63 active_services: HashMap<String, ServiceHandle>,
65}
66
67impl ServiceRegistry {
68 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn with_default_plugins(mut self) -> Self {
75 use crate::services::{
76 generic::GenericContainerPlugin, ollama::OllamaPlugin, tgi::TgiPlugin, vllm::VllmPlugin,
77 };
78
79 let generic_plugin = Box::new(GenericContainerPlugin::new(
81 "generic_container",
82 "alpine:latest",
83 ));
84 self.register_plugin(generic_plugin);
85
86 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 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 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 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 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 pub fn active_services(&self) -> &HashMap<String, ServiceHandle> {
176 &self.active_services
177 }
178
179 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 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 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 Ok(mock_logs.into_iter().take(lines).collect())
216 }
217}
218
219#[derive(Debug, Clone)]
221pub struct SimpleMetrics {
222 pub session_id: Uuid,
224 pub start_time: std::time::Instant,
226 pub tests_executed: u32,
228 pub tests_passed: u32,
230 pub tests_failed: u32,
232 pub total_duration_ms: u64,
234 pub active_containers: u32,
236 pub active_services: u32,
238 pub containers_created: u32,
240 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#[derive(Debug, Clone)]
269pub struct ExecutionResult {
270 pub exit_code: i32,
272 pub stdout: String,
274 pub stderr: String,
276 pub duration: std::time::Duration,
278 pub command: Vec<String>,
280 pub container_name: String,
282}
283
284impl ExecutionResult {
285 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 pub fn does_not_match_regex(&self, pattern: &str) -> Result<bool> {
296 Ok(!self.matches_regex(pattern)?)
297 }
298
299 pub fn succeeded(&self) -> bool {
301 self.exit_code == 0
302 }
303
304 pub fn failed(&self) -> bool {
306 !self.succeeded()
307 }
308}
309
310#[allow(dead_code)]
312#[derive(Debug)]
313pub struct CleanroomEnvironment {
314 session_id: Uuid,
316 backend: Arc<dyn Backend>,
318 services: Arc<RwLock<ServiceRegistry>>,
320 metrics: Arc<RwLock<SimpleMetrics>>,
322 container_registry: Arc<RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>>,
324 #[cfg(feature = "otel-metrics")]
326 meter: opentelemetry::metrics::Meter,
327 telemetry: Arc<RwLock<TelemetryState>>,
329}
330
331impl Default for CleanroomEnvironment {
332 fn default() -> Self {
333 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#[derive(Debug)]
360pub struct TelemetryState {
361 pub tracing_enabled: bool,
363 pub metrics_enabled: bool,
365 pub traces: Vec<String>,
367}
368
369impl TelemetryState {
370 pub fn enable_tracing(&mut self) {
372 self.tracing_enabled = true;
373 }
374
375 pub fn enable_metrics(&mut self) {
377 self.metrics_enabled = true;
378 }
379
380 pub fn get_traces(&self) -> Vec<String> {
382 self.traces.clone()
383 }
384}
385
386impl CleanroomEnvironment {
387 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 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 {
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 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 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 pub async fn get_metrics(&self) -> Result<SimpleMetrics> {
486 Ok(self.metrics.read().await.clone())
487 }
488
489 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 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 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 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 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 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 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 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 pub async fn services(&self) -> tokio::sync::RwLockReadGuard<'_, ServiceRegistry> {
555 self.services.read().await
556 }
557
558 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 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 let existing_container = {
580 let registry = self.container_registry.read().await;
581 if let Some(existing_container) = registry.get(name) {
582 existing_container.downcast_ref::<T>().cloned()
584 } else {
585 None
586 }
587 };
588
589 if let Some(container) = existing_container {
590 {
592 let mut metrics = self.metrics.write().await;
593 metrics.containers_reused += 1;
594 }
595
596 return Ok(container);
597 }
598
599 let container = factory()?;
601
602 let mut registry = self.container_registry.write().await;
604 registry.insert(name.to_string(), Box::new(container.clone()));
605
606 {
608 let mut metrics = self.metrics.write().await;
609 metrics.containers_created += 1;
610 }
611
612 Ok(container)
613 }
614
615 pub async fn check_health(&self) -> HashMap<String, HealthStatus> {
617 self.services.read().await.check_all_health().await
618 }
619
620 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 pub fn session_id(&self) -> Uuid {
628 self.session_id
629 }
630
631 pub fn backend(&self) -> &dyn Backend {
633 self.backend.as_ref() as &dyn Backend
634 }
635
636 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 let cmd = Cmd::new("sh")
664 .arg("-c")
665 .arg(command.join(" "))
666 .env("CONTAINER_NAME", container_name);
667
668 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 #[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#[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 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 Ok(())
802 }
803
804 fn health_check(&self, handle: &ServiceHandle) -> HealthStatus {
805 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()); }
823
824 #[test]
825 fn test_service_plugin_dyn_compatibility() {
826 let plugin: Arc<dyn ServicePlugin> = Arc::new(MockDatabasePlugin::new());
828
829 assert_eq!(plugin.name(), "mock_database");
831
832 let plugins: Vec<Arc<dyn ServicePlugin>> = vec![plugin];
834
835 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 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 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 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 let result2 = env
921 .get_or_create_container("test-container", || {
922 Ok::<String, CleanroomError>("container-instance-2".to_string())
923 })
924 .await?;
925 assert_eq!(result2, "container-instance");
927
928 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 assert!(health_status.is_empty());
941 Ok(())
942 }
943}