1use crate::plugins::core::{
9 ClientPlugin, PluginConfig, PluginContext, PluginResult, RequestContext, ResponseContext,
10};
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use serde_json::{Value, json};
15use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18use tracing::{debug, info, warn};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct MetricsData {
27 pub total_requests: u64,
29
30 pub successful_responses: u64,
32
33 pub failed_responses: u64,
35
36 pub avg_response_time_ms: f64,
38
39 pub min_response_time_ms: u64,
41
42 pub max_response_time_ms: u64,
44
45 pub requests_per_minute: f64,
47
48 pub method_metrics: HashMap<String, MethodMetrics>,
50
51 pub start_time: DateTime<Utc>,
53
54 pub last_reset: DateTime<Utc>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct MethodMetrics {
60 pub count: u64,
61 pub avg_duration_ms: f64,
62 pub success_count: u64,
63 pub error_count: u64,
64}
65
66impl Default for MetricsData {
67 fn default() -> Self {
68 let now = Utc::now();
69 Self {
70 total_requests: 0,
71 successful_responses: 0,
72 failed_responses: 0,
73 avg_response_time_ms: 0.0,
74 min_response_time_ms: u64::MAX,
75 max_response_time_ms: 0,
76 requests_per_minute: 0.0,
77 method_metrics: HashMap::new(),
78 start_time: now,
79 last_reset: now,
80 }
81 }
82}
83
84#[derive(Debug)]
86pub struct MetricsPlugin {
87 metrics: Arc<Mutex<MetricsData>>,
89
90 request_times: Arc<Mutex<HashMap<String, Instant>>>,
92
93 recent_requests: Arc<Mutex<Vec<Instant>>>,
95}
96
97impl MetricsPlugin {
98 #[must_use]
100 pub fn new(_config: PluginConfig) -> Self {
101 Self {
102 metrics: Arc::new(Mutex::new(MetricsData::default())),
103 request_times: Arc::new(Mutex::new(HashMap::new())),
104 recent_requests: Arc::new(Mutex::new(Vec::new())),
105 }
106 }
107
108 #[must_use]
110 pub fn get_metrics(&self) -> MetricsData {
111 self.metrics.lock().unwrap().clone()
112 }
113
114 pub fn reset_metrics(&self) {
116 let mut metrics = self.metrics.lock().unwrap();
117 let now = Utc::now();
118 *metrics = MetricsData {
119 start_time: metrics.start_time,
120 last_reset: now,
121 ..MetricsData::default()
122 };
123 self.request_times.lock().unwrap().clear();
124 self.recent_requests.lock().unwrap().clear();
125 }
126
127 fn update_request_rate(&self) {
128 let mut recent = self.recent_requests.lock().unwrap();
129 let now = Instant::now();
130
131 recent.retain(|×tamp| now.duration_since(timestamp).as_secs() < 60);
133
134 recent.push(now);
136
137 let mut metrics = self.metrics.lock().unwrap();
139 metrics.requests_per_minute = recent.len() as f64;
140 }
141
142 fn update_method_metrics(&self, method: &str, duration: Duration, is_success: bool) {
143 let mut metrics = self.metrics.lock().unwrap();
144 let entry = metrics
145 .method_metrics
146 .entry(method.to_string())
147 .or_insert(MethodMetrics {
148 count: 0,
149 avg_duration_ms: 0.0,
150 success_count: 0,
151 error_count: 0,
152 });
153
154 entry.count += 1;
155 if is_success {
156 entry.success_count += 1;
157 } else {
158 entry.error_count += 1;
159 }
160
161 let duration_ms = duration.as_millis() as f64;
163 entry.avg_duration_ms =
164 (entry.avg_duration_ms * (entry.count - 1) as f64 + duration_ms) / entry.count as f64;
165 }
166}
167
168#[async_trait]
169impl ClientPlugin for MetricsPlugin {
170 fn name(&self) -> &str {
171 "metrics"
172 }
173
174 fn version(&self) -> &str {
175 "1.0.0"
176 }
177
178 fn description(&self) -> Option<&str> {
179 Some("Collects request/response metrics and performance data")
180 }
181
182 async fn initialize(&self, context: &PluginContext) -> PluginResult<()> {
183 info!(
184 "Metrics plugin initialized for client: {}",
185 context.client_name
186 );
187 Ok(())
188 }
189
190 async fn before_request(&self, context: &mut RequestContext) -> PluginResult<()> {
191 let request_id = context.request.id.to_string();
192
193 self.request_times
195 .lock()
196 .unwrap()
197 .insert(request_id.clone(), Instant::now());
198
199 {
201 let mut metrics = self.metrics.lock().unwrap();
202 metrics.total_requests += 1;
203 }
204
205 self.update_request_rate();
206
207 context.add_metadata("metrics.request_id".to_string(), json!(request_id));
209 context.add_metadata(
210 "metrics.start_time".to_string(),
211 json!(Utc::now().to_rfc3339()),
212 );
213
214 debug!(
215 "Metrics: Recorded request start for method: {}",
216 context.method()
217 );
218 Ok(())
219 }
220
221 async fn after_response(&self, context: &mut ResponseContext) -> PluginResult<()> {
222 let request_id = context.request_context.request.id.to_string();
223
224 let duration =
226 if let Some(start_time) = self.request_times.lock().unwrap().remove(&request_id) {
227 start_time.elapsed()
228 } else {
229 context.duration
230 };
231
232 let is_success = context.is_success();
233 let method = context.method().to_string();
234 let duration_ms = duration.as_millis();
235
236 {
238 let mut metrics = self.metrics.lock().unwrap();
239
240 if is_success {
241 metrics.successful_responses += 1;
242 } else {
243 metrics.failed_responses += 1;
244 }
245
246 let duration_ms_u64 = duration_ms as u64;
247
248 if duration_ms_u64 < metrics.min_response_time_ms {
250 metrics.min_response_time_ms = duration_ms_u64;
251 }
252 if duration_ms_u64 > metrics.max_response_time_ms {
253 metrics.max_response_time_ms = duration_ms_u64;
254 }
255
256 let total_responses = metrics.successful_responses + metrics.failed_responses;
258 metrics.avg_response_time_ms =
259 (metrics.avg_response_time_ms * (total_responses - 1) as f64 + duration_ms as f64)
260 / total_responses as f64;
261 }
262
263 self.update_method_metrics(&method, duration, is_success);
265
266 context.add_metadata("metrics.duration_ms".to_string(), json!(duration_ms));
268 context.add_metadata("metrics.success".to_string(), json!(is_success));
269
270 debug!(
271 "Metrics: Recorded response for method: {} ({}ms, success: {})",
272 method, duration_ms, is_success
273 );
274
275 Ok(())
276 }
277
278 async fn handle_custom(
279 &self,
280 method: &str,
281 params: Option<Value>,
282 ) -> PluginResult<Option<Value>> {
283 match method {
284 "metrics.get_stats" => {
285 let metrics = self.get_metrics();
286 Ok(Some(serde_json::to_value(metrics).unwrap()))
287 }
288 "metrics.reset" => {
289 self.reset_metrics();
290 info!("Metrics reset");
291 Ok(Some(json!({"status": "reset"})))
292 }
293 "metrics.get_method_stats" => {
294 if let Some(params) = params {
295 if let Some(method_name) = params.get("method").and_then(|v| v.as_str()) {
296 let metrics = self.metrics.lock().unwrap();
297 if let Some(method_metrics) = metrics.method_metrics.get(method_name) {
298 Ok(Some(serde_json::to_value(method_metrics).unwrap()))
299 } else {
300 Ok(Some(json!({"error": "Method not found"})))
301 }
302 } else {
303 Ok(Some(json!({"error": "Method parameter required"})))
304 }
305 } else {
306 Ok(Some(json!({"error": "Parameters required"})))
307 }
308 }
309 _ => Ok(None),
310 }
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct RetryConfig {
321 pub max_retries: u32,
323
324 pub base_delay_ms: u64,
326
327 pub max_delay_ms: u64,
329
330 pub backoff_multiplier: f64,
332
333 pub retry_on_timeout: bool,
335
336 pub retry_on_connection_error: bool,
338}
339
340impl Default for RetryConfig {
341 fn default() -> Self {
342 Self {
343 max_retries: 3,
344 base_delay_ms: 100,
345 max_delay_ms: 5000,
346 backoff_multiplier: 2.0,
347 retry_on_timeout: true,
348 retry_on_connection_error: true,
349 }
350 }
351}
352
353#[derive(Debug)]
355pub struct RetryPlugin {
356 config: RetryConfig,
357 retry_stats: Arc<Mutex<HashMap<String, u32>>>,
358}
359
360impl RetryPlugin {
361 #[must_use]
363 pub fn new(config: PluginConfig) -> Self {
364 let retry_config = match config {
365 PluginConfig::Retry(config) => config,
366 _ => RetryConfig::default(),
367 };
368
369 Self {
370 config: retry_config,
371 retry_stats: Arc::new(Mutex::new(HashMap::new())),
372 }
373 }
374
375 fn should_retry(&self, error: &turbomcp_protocol::Error) -> bool {
376 let error_string = error.to_string().to_lowercase();
377
378 if self.config.retry_on_connection_error
379 && (error_string.contains("transport") || error_string.contains("connection"))
380 {
381 return true;
382 }
383
384 if self.config.retry_on_timeout && error_string.contains("timeout") {
385 return true;
386 }
387
388 false
389 }
390
391 fn calculate_delay(&self, attempt: u32) -> Duration {
392 let delay_ms = (self.config.base_delay_ms as f64
393 * self.config.backoff_multiplier.powi(attempt as i32)) as u64;
394 Duration::from_millis(delay_ms.min(self.config.max_delay_ms))
395 }
396
397 fn get_retry_count(&self, request_id: &str) -> u32 {
398 self.retry_stats
399 .lock()
400 .unwrap()
401 .get(request_id)
402 .copied()
403 .unwrap_or(0)
404 }
405
406 fn increment_retry_count(&self, request_id: &str) {
407 let mut stats = self.retry_stats.lock().unwrap();
408 let count = stats.entry(request_id.to_string()).or_insert(0);
409 *count += 1;
410 }
411
412 fn clear_retry_count(&self, request_id: &str) {
413 self.retry_stats.lock().unwrap().remove(request_id);
414 }
415}
416
417#[async_trait]
418impl ClientPlugin for RetryPlugin {
419 fn name(&self) -> &str {
420 "retry"
421 }
422
423 fn version(&self) -> &str {
424 "1.0.0"
425 }
426
427 fn description(&self) -> Option<&str> {
428 Some("Automatic retry with exponential backoff for failed requests")
429 }
430
431 async fn initialize(&self, context: &PluginContext) -> PluginResult<()> {
432 info!(
433 "Retry plugin initialized for client: {} (max_retries: {}, base_delay: {}ms)",
434 context.client_name, self.config.max_retries, self.config.base_delay_ms
435 );
436 Ok(())
437 }
438
439 async fn before_request(&self, context: &mut RequestContext) -> PluginResult<()> {
440 let request_id = context.request.id.to_string();
441 let retry_count = self.get_retry_count(&request_id);
442
443 context.add_metadata("retry.attempt".to_string(), json!(retry_count + 1));
445 context.add_metadata(
446 "retry.max_attempts".to_string(),
447 json!(self.config.max_retries + 1),
448 );
449
450 if retry_count > 0 {
451 debug!(
452 "Retry: Attempt {} for request {} (method: {})",
453 retry_count + 1,
454 request_id,
455 context.method()
456 );
457 }
458
459 Ok(())
460 }
461
462 async fn after_response(&self, context: &mut ResponseContext) -> PluginResult<()> {
463 let request_id = context.request_context.request.id.to_string();
464
465 if context.is_success() {
466 self.clear_retry_count(&request_id);
468 debug!("Retry: Request {} succeeded", request_id);
469 } else if let Some(error) = &context.error {
470 let retry_count = self.get_retry_count(&request_id);
471
472 if self.should_retry(error) && retry_count < self.config.max_retries {
473 self.increment_retry_count(&request_id);
475
476 let delay = self.calculate_delay(retry_count);
477 warn!(
478 "Retry: Request {} failed (attempt {}), will retry after {:?}",
479 request_id,
480 retry_count + 1,
481 delay
482 );
483
484 context.add_metadata("retry.will_retry".to_string(), json!(true));
486 context.add_metadata("retry.delay_ms".to_string(), json!(delay.as_millis()));
487 context.add_metadata("retry.next_attempt".to_string(), json!(retry_count + 2));
488
489 context.add_metadata("retry.should_retry".to_string(), json!(true));
492 context.add_metadata(
493 "retry.recommended_action".to_string(),
494 json!("retry_request"),
495 );
496 } else {
497 self.clear_retry_count(&request_id);
499 if retry_count >= self.config.max_retries {
500 warn!("Retry: Request {} exhausted all retry attempts", request_id);
501 } else {
502 debug!("Retry: Error not retryable for request {}", request_id);
503 }
504 context.add_metadata("retry.will_retry".to_string(), json!(false));
505 context.add_metadata(
506 "retry.reason".to_string(),
507 json!(if retry_count >= self.config.max_retries {
508 "max_retries_reached"
509 } else {
510 "error_not_retryable"
511 }),
512 );
513 }
514 }
515
516 Ok(())
517 }
518
519 async fn handle_custom(
520 &self,
521 method: &str,
522 _params: Option<Value>,
523 ) -> PluginResult<Option<Value>> {
524 match method {
525 "retry.get_config" => Ok(Some(serde_json::to_value(&self.config).unwrap())),
526 "retry.get_stats" => {
527 let stats = self.retry_stats.lock().unwrap();
528 Ok(Some(json!({
529 "active_retries": stats.len(),
530 "retry_counts": stats.clone()
531 })))
532 }
533 "retry.clear_stats" => {
534 self.retry_stats.lock().unwrap().clear();
535 info!("Retry stats cleared");
536 Ok(Some(json!({"status": "cleared"})))
537 }
538 _ => Ok(None),
539 }
540 }
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct CacheConfig {
550 pub max_entries: usize,
552
553 pub ttl_seconds: u64,
555
556 pub cache_responses: bool,
558
559 pub cache_resources: bool,
561
562 pub cache_tools: bool,
564}
565
566impl Default for CacheConfig {
567 fn default() -> Self {
568 Self {
569 max_entries: 1000,
570 ttl_seconds: 300, cache_responses: true,
572 cache_resources: true,
573 cache_tools: true,
574 }
575 }
576}
577
578#[derive(Debug, Clone)]
579struct CacheEntry {
580 data: Value,
581 timestamp: Instant,
582 access_count: u64,
583}
584
585impl CacheEntry {
586 fn new(data: Value) -> Self {
587 Self {
588 data,
589 timestamp: Instant::now(),
590 access_count: 0,
591 }
592 }
593
594 fn is_expired(&self, ttl: Duration) -> bool {
595 self.timestamp.elapsed() > ttl
596 }
597
598 fn access(&mut self) -> &Value {
599 self.access_count += 1;
600 &self.data
601 }
602}
603
604#[derive(Debug)]
606pub struct CachePlugin {
607 config: CacheConfig,
608 cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
609 stats: Arc<Mutex<CacheStats>>,
610}
611
612#[derive(Debug, Default)]
613struct CacheStats {
614 hits: u64,
615 misses: u64,
616 evictions: u64,
617 total_entries: u64,
618}
619
620impl CachePlugin {
621 #[must_use]
623 pub fn new(config: PluginConfig) -> Self {
624 let cache_config = match config {
625 PluginConfig::Cache(config) => config,
626 _ => CacheConfig::default(),
627 };
628
629 Self {
630 config: cache_config,
631 cache: Arc::new(Mutex::new(HashMap::new())),
632 stats: Arc::new(Mutex::new(CacheStats::default())),
633 }
634 }
635
636 fn should_cache_method(&self, method: &str) -> bool {
637 match method {
638 m if m.starts_with("tools/") && self.config.cache_tools => true,
639 m if m.starts_with("resources/") && self.config.cache_resources => true,
640 _ if self.config.cache_responses => true,
641 _ => false,
642 }
643 }
644
645 fn generate_cache_key(&self, context: &RequestContext) -> String {
646 let params_hash = if let Some(params) = &context.request.params {
648 format!("{:x}", fxhash::hash64(params))
649 } else {
650 "no_params".to_string()
651 };
652 format!("{}:{}", context.method(), params_hash)
653 }
654
655 fn get_cached(&self, key: &str) -> Option<Value> {
656 let mut cache = self.cache.lock().unwrap();
657 let ttl = Duration::from_secs(self.config.ttl_seconds);
658
659 if let Some(entry) = cache.get_mut(key) {
660 if !entry.is_expired(ttl) {
661 let mut stats = self.stats.lock().unwrap();
662 stats.hits += 1;
663 return Some(entry.access().clone());
664 } else {
665 cache.remove(key);
667 let mut stats = self.stats.lock().unwrap();
668 stats.evictions += 1;
669 }
670 }
671
672 let mut stats = self.stats.lock().unwrap();
673 stats.misses += 1;
674 None
675 }
676
677 fn store_cached(&self, key: String, data: Value) {
678 let mut cache = self.cache.lock().unwrap();
679
680 if cache.len() >= self.config.max_entries {
682 let evict_keys: Vec<_> = {
684 let mut entries: Vec<_> = cache
685 .iter()
686 .map(|(k, v)| (k.clone(), v.timestamp))
687 .collect();
688 entries.sort_by_key(|(_, timestamp)| *timestamp);
689
690 let evict_count = (cache.len() - self.config.max_entries + 1).min(cache.len() / 2);
691 entries
692 .into_iter()
693 .take(evict_count)
694 .map(|(key, _)| key)
695 .collect()
696 };
697
698 let evict_count = evict_keys.len();
699 for key in evict_keys {
700 cache.remove(&key);
701 }
702
703 let mut stats = self.stats.lock().unwrap();
704 stats.evictions += evict_count as u64;
705 }
706
707 cache.insert(key, CacheEntry::new(data));
708 let mut stats = self.stats.lock().unwrap();
709 stats.total_entries += 1;
710 }
711
712 fn cleanup_expired(&self) {
713 let mut cache = self.cache.lock().unwrap();
714 let ttl = Duration::from_secs(self.config.ttl_seconds);
715
716 let expired_keys: Vec<_> = cache
717 .iter()
718 .filter(|(_, entry)| entry.is_expired(ttl))
719 .map(|(key, _)| key.clone())
720 .collect();
721
722 let eviction_count = expired_keys.len();
723 for key in expired_keys {
724 cache.remove(&key);
725 }
726
727 if eviction_count > 0 {
728 let mut stats = self.stats.lock().unwrap();
729 stats.evictions += eviction_count as u64;
730 debug!("Cache: Cleaned up {} expired entries", eviction_count);
731 }
732 }
733}
734
735#[async_trait]
736impl ClientPlugin for CachePlugin {
737 fn name(&self) -> &str {
738 "cache"
739 }
740
741 fn version(&self) -> &str {
742 "1.0.0"
743 }
744
745 fn description(&self) -> Option<&str> {
746 Some("Response caching with TTL and LRU eviction")
747 }
748
749 async fn initialize(&self, context: &PluginContext) -> PluginResult<()> {
750 info!(
751 "Cache plugin initialized for client: {} (max_entries: {}, ttl: {}s)",
752 context.client_name, self.config.max_entries, self.config.ttl_seconds
753 );
754 Ok(())
755 }
756
757 async fn before_request(&self, context: &mut RequestContext) -> PluginResult<()> {
758 if !self.should_cache_method(context.method()) {
759 return Ok(());
760 }
761
762 let cache_key = self.generate_cache_key(context);
763
764 if let Some(cached_data) = self.get_cached(&cache_key) {
765 debug!(
766 "Cache: Hit for method {} (key: {})",
767 context.method(),
768 cache_key
769 );
770 context.add_metadata("cache.hit".to_string(), json!(true));
771 context.add_metadata("cache.key".to_string(), json!(cache_key));
772 context.add_metadata("cache.response_source".to_string(), json!("cache"));
773 context.add_metadata("cache.response_data".to_string(), cached_data.clone());
775 context.add_metadata("cache.should_skip_request".to_string(), json!(true));
776 } else {
777 debug!(
778 "Cache: Miss for method {} (key: {})",
779 context.method(),
780 cache_key
781 );
782 context.add_metadata("cache.hit".to_string(), json!(false));
783 context.add_metadata("cache.key".to_string(), json!(cache_key));
784 context.add_metadata("cache.should_skip_request".to_string(), json!(false));
785 }
786
787 Ok(())
788 }
789
790 async fn after_response(&self, context: &mut ResponseContext) -> PluginResult<()> {
791 if let Some(cached_response_data) =
793 context.request_context.get_metadata("cache.response_data")
794 {
795 context.response = Some(cached_response_data.clone());
796 debug!(
797 "Cache: Used cached response for method {}",
798 context.method()
799 );
800 return Ok(());
801 }
802
803 if !self.should_cache_method(context.method()) || !context.is_success() {
804 return Ok(());
805 }
806
807 if let Some(cache_key) = context
808 .request_context
809 .get_metadata("cache.key")
810 .and_then(|v| v.as_str())
811 && let Some(response_data) = &context.response
812 {
813 self.store_cached(cache_key.to_string(), response_data.clone());
814 debug!(
815 "Cache: Stored response for method {} (key: {})",
816 context.method(),
817 cache_key
818 );
819 context.add_metadata("cache.stored".to_string(), json!(true));
820 }
821
822 self.cleanup_expired();
824
825 Ok(())
826 }
827
828 async fn handle_custom(
829 &self,
830 method: &str,
831 params: Option<Value>,
832 ) -> PluginResult<Option<Value>> {
833 match method {
834 "cache.get_stats" => {
835 let stats = self.stats.lock().unwrap();
836 let cache_size = self.cache.lock().unwrap().len();
837
838 Ok(Some(json!({
839 "hits": stats.hits,
840 "misses": stats.misses,
841 "evictions": stats.evictions,
842 "total_entries": stats.total_entries,
843 "current_size": cache_size,
844 "hit_rate": if stats.hits + stats.misses > 0 {
845 stats.hits as f64 / (stats.hits + stats.misses) as f64
846 } else {
847 0.0
848 }
849 })))
850 }
851 "cache.clear" => {
852 let mut cache = self.cache.lock().unwrap();
853 let cleared_count = cache.len();
854 cache.clear();
855 info!("Cache: Cleared {} entries", cleared_count);
856 Ok(Some(json!({"cleared_entries": cleared_count})))
857 }
858 "cache.get_config" => Ok(Some(serde_json::to_value(&self.config).unwrap())),
859 "cache.cleanup" => {
860 self.cleanup_expired();
861 let cache_size = self.cache.lock().unwrap().len();
862 Ok(Some(json!({"remaining_entries": cache_size})))
863 }
864 "cache.get" => {
865 if let Some(params) = params {
866 if let Some(key) = params.get("key").and_then(|v| v.as_str()) {
867 if let Some(data) = self.get_cached(key) {
868 Ok(Some(json!({"found": true, "data": data})))
869 } else {
870 Ok(Some(json!({"found": false})))
871 }
872 } else {
873 Ok(Some(json!({"error": "Key parameter required"})))
874 }
875 } else {
876 Ok(Some(json!({"error": "Parameters required"})))
877 }
878 }
879 _ => Ok(None),
880 }
881 }
882}
883
884mod fxhash {
886 use serde_json::Value;
887 use std::collections::hash_map::DefaultHasher;
888 use std::hash::{Hash, Hasher};
889
890 pub fn hash64(value: &Value) -> u64 {
891 let mut hasher = DefaultHasher::new();
892
893 let json_str = value.to_string();
895 json_str.hash(&mut hasher);
896
897 hasher.finish()
898 }
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904
905 #[tokio::test]
906 async fn test_metrics_plugin_creation() {
907 let plugin = MetricsPlugin::new(PluginConfig::Metrics);
908 assert_eq!(plugin.name(), "metrics");
909
910 let metrics = plugin.get_metrics();
911 assert_eq!(metrics.total_requests, 0);
912 assert_eq!(metrics.successful_responses, 0);
913 }
914
915 #[tokio::test]
916 async fn test_retry_plugin_creation() {
917 let config = RetryConfig {
918 max_retries: 5,
919 base_delay_ms: 200,
920 max_delay_ms: 2000,
921 backoff_multiplier: 1.5,
922 retry_on_timeout: true,
923 retry_on_connection_error: false,
924 };
925
926 let plugin = RetryPlugin::new(PluginConfig::Retry(config.clone()));
927 assert_eq!(plugin.name(), "retry");
928 assert_eq!(plugin.config.max_retries, 5);
929 assert_eq!(plugin.config.base_delay_ms, 200);
930 }
931
932 #[tokio::test]
933 async fn test_cache_plugin_creation() {
934 let config = CacheConfig {
935 max_entries: 500,
936 ttl_seconds: 600,
937 cache_responses: true,
938 cache_resources: false,
939 cache_tools: true,
940 };
941
942 let plugin = CachePlugin::new(PluginConfig::Cache(config.clone()));
943 assert_eq!(plugin.name(), "cache");
944 assert_eq!(plugin.config.max_entries, 500);
945 assert_eq!(plugin.config.ttl_seconds, 600);
946 }
947
948 #[test]
949 fn test_retry_delay_calculation() {
950 let config = RetryConfig {
951 max_retries: 3,
952 base_delay_ms: 100,
953 max_delay_ms: 1000,
954 backoff_multiplier: 2.0,
955 retry_on_timeout: true,
956 retry_on_connection_error: true,
957 };
958
959 let plugin = RetryPlugin::new(PluginConfig::Retry(config));
960
961 assert_eq!(plugin.calculate_delay(0), Duration::from_millis(100));
962 assert_eq!(plugin.calculate_delay(1), Duration::from_millis(200));
963 assert_eq!(plugin.calculate_delay(2), Duration::from_millis(400));
964 assert_eq!(plugin.calculate_delay(3), Duration::from_millis(800));
965 assert_eq!(plugin.calculate_delay(4), Duration::from_millis(1000)); }
967
968 #[test]
969 fn test_cache_entry_expiration() {
970 let entry = CacheEntry::new(json!({"test": "data"}));
971 assert!(!entry.is_expired(Duration::from_secs(1)));
972
973 }
975}