Skip to main content

plugin_packager/
health_check.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Plugin health check and verification framework
5///
6/// This module provides comprehensive health verification for plugins including:
7/// - Symbol/export presence verification
8/// - Binary compatibility checking (OS/architecture)
9/// - Performance baseline establishment
10/// - Health scoring system
11/// - Detailed diagnostics and recommendations
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::Path;
15
16/// Health check severity levels
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum HealthSeverity {
20    Info,
21    Warning,
22    Error,
23    Critical,
24}
25
26impl HealthSeverity {
27    pub fn as_str(&self) -> &'static str {
28        match self {
29            HealthSeverity::Info => "info",
30            HealthSeverity::Warning => "warning",
31            HealthSeverity::Error => "error",
32            HealthSeverity::Critical => "critical",
33        }
34    }
35
36    pub fn try_parse(s: &str) -> Option<Self> {
37        match s.to_lowercase().as_str() {
38            "info" => Some(HealthSeverity::Info),
39            "warning" => Some(HealthSeverity::Warning),
40            "error" => Some(HealthSeverity::Error),
41            "critical" => Some(HealthSeverity::Critical),
42            _ => None,
43        }
44    }
45}
46
47/// Platform/architecture identifier
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum Platform {
51    Linux,
52    MacOS,
53    Windows,
54    Unknown(String),
55}
56
57impl Platform {
58    pub fn current() -> Self {
59        if cfg!(target_os = "linux") {
60            Platform::Linux
61        } else if cfg!(target_os = "macos") {
62            Platform::MacOS
63        } else if cfg!(target_os = "windows") {
64            Platform::Windows
65        } else {
66            Platform::Unknown(std::env::consts::OS.to_string())
67        }
68    }
69
70    pub fn as_str(&self) -> &str {
71        match self {
72            Platform::Linux => "linux",
73            Platform::MacOS => "macos",
74            Platform::Windows => "windows",
75            Platform::Unknown(s) => s,
76        }
77    }
78}
79
80/// Architecture identifier
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "lowercase")]
83pub enum Architecture {
84    X86_64,
85    Arm64,
86    X86,
87    Arm,
88    Unknown(String),
89}
90
91impl Architecture {
92    pub fn current() -> Self {
93        if cfg!(target_arch = "x86_64") {
94            Architecture::X86_64
95        } else if cfg!(target_arch = "aarch64") {
96            Architecture::Arm64
97        } else if cfg!(target_arch = "x86") {
98            Architecture::X86
99        } else if cfg!(target_arch = "arm") {
100            Architecture::Arm
101        } else {
102            Architecture::Unknown(std::env::consts::ARCH.to_string())
103        }
104    }
105
106    pub fn as_str(&self) -> &str {
107        match self {
108            Architecture::X86_64 => "x86_64",
109            Architecture::Arm64 => "arm64",
110            Architecture::X86 => "x86",
111            Architecture::Arm => "arm",
112            Architecture::Unknown(s) => s,
113        }
114    }
115}
116
117/// Symbol export requirement
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct SymbolRequirement {
120    pub name: String,
121    pub required: bool,
122    pub description: String,
123}
124
125/// Binary compatibility information
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct BinaryCompatibility {
128    pub platform: Platform,
129    pub architecture: Architecture,
130    pub min_libc_version: Option<String>,
131    pub required_symbols: Vec<SymbolRequirement>,
132}
133
134/// Health check result for a single check
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct HealthCheckResult {
137    pub check_name: String,
138    pub passed: bool,
139    pub severity: HealthSeverity,
140    pub message: String,
141    pub details: Option<String>,
142    pub suggestion: Option<String>,
143}
144
145/// Overall plugin health report
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct HealthReport {
148    pub plugin_id: String,
149    pub plugin_version: String,
150    pub check_timestamp: String,
151    pub overall_health: HealthScore,
152    pub checks: Vec<HealthCheckResult>,
153    pub binary_compatibility: Option<BinaryCompatibility>,
154    pub performance_baseline: Option<PerformanceBaseline>,
155    pub recommendations: Vec<String>,
156}
157
158/// Health score (0-100)
159#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
160pub struct HealthScore {
161    pub score: u32,
162    pub status: HealthStatus,
163}
164
165/// Overall health status
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "PascalCase")]
168pub enum HealthStatus {
169    Excellent, // 90-100
170    Good,      // 75-89
171    Fair,      // 60-74
172    Poor,      // 40-59
173    Critical,  // 0-39
174}
175
176impl HealthStatus {
177    pub fn from_score(score: u32) -> Self {
178        match score {
179            90..=100 => HealthStatus::Excellent,
180            75..=89 => HealthStatus::Good,
181            60..=74 => HealthStatus::Fair,
182            40..=59 => HealthStatus::Poor,
183            _ => HealthStatus::Critical,
184        }
185    }
186
187    pub fn as_str(&self) -> &'static str {
188        match self {
189            HealthStatus::Excellent => "Excellent",
190            HealthStatus::Good => "Good",
191            HealthStatus::Fair => "Fair",
192            HealthStatus::Poor => "Poor",
193            HealthStatus::Critical => "Critical",
194        }
195    }
196}
197
198/// Performance baseline for plugin
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct PerformanceBaseline {
201    pub init_time_ms: f64,
202    pub shutdown_time_ms: f64,
203    pub memory_usage_mb: f64,
204    pub max_concurrent_calls: u32,
205}
206
207/// Plugin health checker
208pub struct PluginHealthChecker {
209    required_symbols: HashMap<String, SymbolRequirement>,
210    performance_thresholds: PerformanceThresholds,
211}
212
213/// Performance thresholds for scoring
214#[derive(Debug, Clone)]
215pub struct PerformanceThresholds {
216    pub max_init_time_ms: f64,
217    pub max_shutdown_time_ms: f64,
218    pub max_memory_usage_mb: f64,
219    pub min_concurrent_calls: u32,
220}
221
222impl Default for PerformanceThresholds {
223    fn default() -> Self {
224        Self {
225            max_init_time_ms: 5000.0,
226            max_shutdown_time_ms: 1000.0,
227            max_memory_usage_mb: 500.0,
228            min_concurrent_calls: 10,
229        }
230    }
231}
232
233impl PluginHealthChecker {
234    /// Create a new health checker with default configuration
235    pub fn new() -> Self {
236        Self {
237            required_symbols: Self::default_required_symbols(),
238            performance_thresholds: PerformanceThresholds::default(),
239        }
240    }
241
242    /// Get default required symbols for plugins
243    fn default_required_symbols() -> HashMap<String, SymbolRequirement> {
244        let mut symbols = HashMap::new();
245
246        symbols.insert(
247            "plugin_init".to_string(),
248            SymbolRequirement {
249                name: "plugin_init".to_string(),
250                required: true,
251                description: "Plugin initialization entry point".to_string(),
252            },
253        );
254
255        symbols.insert(
256            "plugin_get_info".to_string(),
257            SymbolRequirement {
258                name: "plugin_get_info".to_string(),
259                required: true,
260                description: "Plugin metadata provider".to_string(),
261            },
262        );
263
264        symbols.insert(
265            "plugin_shutdown".to_string(),
266            SymbolRequirement {
267                name: "plugin_shutdown".to_string(),
268                required: false,
269                description: "Plugin cleanup on shutdown".to_string(),
270            },
271        );
272
273        symbols
274    }
275
276    /// Check if binary file exists and is readable
277    pub fn check_binary_exists(&self, binary_path: &Path) -> HealthCheckResult {
278        let exists = binary_path.exists();
279        HealthCheckResult {
280            check_name: "Binary Existence".to_string(),
281            passed: exists,
282            severity: if exists {
283                HealthSeverity::Info
284            } else {
285                HealthSeverity::Critical
286            },
287            message: if exists {
288                format!("Plugin binary found at {}", binary_path.display())
289            } else {
290                format!("Plugin binary not found at {}", binary_path.display())
291            },
292            details: None,
293            suggestion: if !exists {
294                Some("Ensure plugin.so/.dll/.dylib is built and included in package".to_string())
295            } else {
296                None
297            },
298        }
299    }
300
301    /// Check if binary is readable and has expected size
302    pub fn check_binary_validity(&self, binary_path: &Path) -> HealthCheckResult {
303        match std::fs::metadata(binary_path) {
304            Ok(metadata) => {
305                let size = metadata.len();
306                let passed = size > 0;
307                let readonly = metadata.permissions().readonly();
308                HealthCheckResult {
309                    check_name: "Binary Validity".to_string(),
310                    passed,
311                    severity: if passed {
312                        HealthSeverity::Info
313                    } else {
314                        HealthSeverity::Error
315                    },
316                    message: format!("Binary size: {} bytes", size),
317                    details: Some(format!(
318                        "Readable: {}, Size check: {}",
319                        if readonly { "No" } else { "Yes" },
320                        if size > 0 { "Pass" } else { "Fail" }
321                    )),
322                    suggestion: if size == 0 {
323                        Some("Binary file is empty. Rebuild the plugin.".to_string())
324                    } else {
325                        None
326                    },
327                }
328            }
329            Err(e) => HealthCheckResult {
330                check_name: "Binary Validity".to_string(),
331                passed: false,
332                severity: HealthSeverity::Critical,
333                message: format!("Cannot read binary metadata: {}", e),
334                details: Some(e.to_string()),
335                suggestion: Some("Check file permissions and ensure binary exists".to_string()),
336            },
337        }
338    }
339
340    /// Verify platform compatibility
341    pub fn check_platform_compatibility(
342        &self,
343        binary_path: &Path,
344        expected_platform: Platform,
345    ) -> HealthCheckResult {
346        // Basic file magic number check
347        if !binary_path.exists() {
348            return HealthCheckResult {
349                check_name: "Platform Compatibility".to_string(),
350                passed: false,
351                severity: HealthSeverity::Critical,
352                message: "Binary file not found".to_string(),
353                details: None,
354                suggestion: Some("Build and package the plugin correctly".to_string()),
355            };
356        }
357
358        let platform_match = match expected_platform {
359            Platform::Linux => binary_path.to_string_lossy().contains(".so"),
360            Platform::MacOS => binary_path.to_string_lossy().contains(".dylib"),
361            Platform::Windows => binary_path.to_string_lossy().contains(".dll"),
362            Platform::Unknown(_) => false,
363        };
364
365        HealthCheckResult {
366            check_name: "Platform Compatibility".to_string(),
367            passed: platform_match,
368            severity: if platform_match {
369                HealthSeverity::Info
370            } else {
371                HealthSeverity::Warning
372            },
373            message: if platform_match {
374                format!("Binary appears to be for {}", expected_platform.as_str())
375            } else {
376                format!(
377                    "Binary may not be for {} platform",
378                    expected_platform.as_str()
379                )
380            },
381            details: Some(format!("Expected platform: {}", expected_platform.as_str())),
382            suggestion: if !platform_match {
383                Some(format!(
384                    "Rebuild plugin for {} platform",
385                    expected_platform.as_str()
386                ))
387            } else {
388                None
389            },
390        }
391    }
392
393    /// Check if required symbols are documented (simulated check)
394    pub fn check_required_symbols(&self) -> HealthCheckResult {
395        let required_count = self
396            .required_symbols
397            .iter()
398            .filter(|(_, req)| req.required)
399            .count();
400
401        let optional_count = self.required_symbols.len() - required_count;
402
403        HealthCheckResult {
404            check_name: "Required Symbols".to_string(),
405            passed: required_count > 0,
406            severity: if required_count > 0 {
407                HealthSeverity::Info
408            } else {
409                HealthSeverity::Warning
410            },
411            message: format!(
412                "Required symbols check: {} symbols required",
413                self.required_symbols.len()
414            ),
415            details: Some(format!(
416                "Total required: {}, Optional: {}",
417                required_count, optional_count
418            )),
419            suggestion: None,
420        }
421    }
422
423    /// Check performance baseline
424    pub fn check_performance_baseline(&self, baseline: &PerformanceBaseline) -> HealthCheckResult {
425        let init_ok = baseline.init_time_ms <= self.performance_thresholds.max_init_time_ms;
426        let shutdown_ok =
427            baseline.shutdown_time_ms <= self.performance_thresholds.max_shutdown_time_ms;
428        let memory_ok = baseline.memory_usage_mb <= self.performance_thresholds.max_memory_usage_mb;
429        let concurrency_ok =
430            baseline.max_concurrent_calls >= self.performance_thresholds.min_concurrent_calls;
431
432        let all_passed = init_ok && shutdown_ok && memory_ok && concurrency_ok;
433
434        let mut issues = Vec::new();
435        if !init_ok {
436            issues.push(format!(
437                "Init time: {} ms (threshold: {} ms)",
438                baseline.init_time_ms, self.performance_thresholds.max_init_time_ms
439            ));
440        }
441        if !shutdown_ok {
442            issues.push(format!(
443                "Shutdown time: {} ms (threshold: {} ms)",
444                baseline.shutdown_time_ms, self.performance_thresholds.max_shutdown_time_ms
445            ));
446        }
447        if !memory_ok {
448            issues.push(format!(
449                "Memory usage: {} MB (threshold: {} MB)",
450                baseline.memory_usage_mb, self.performance_thresholds.max_memory_usage_mb
451            ));
452        }
453        if !concurrency_ok {
454            issues.push(format!(
455                "Concurrency: {} calls (threshold: {})",
456                baseline.max_concurrent_calls, self.performance_thresholds.min_concurrent_calls
457            ));
458        }
459
460        HealthCheckResult {
461            check_name: "Performance Baseline".to_string(),
462            passed: all_passed,
463            severity: if all_passed {
464                HealthSeverity::Info
465            } else {
466                HealthSeverity::Warning
467            },
468            message: if all_passed {
469                "Performance baseline within acceptable thresholds".to_string()
470            } else {
471                format!("Performance issues detected: {} found", issues.len())
472            },
473            details: if issues.is_empty() {
474                None
475            } else {
476                Some(issues.join(", "))
477            },
478            suggestion: if !all_passed {
479                Some("Review performance metrics and optimize hot paths".to_string())
480            } else {
481                None
482            },
483        }
484    }
485
486    /// Calculate overall health score from check results
487    pub fn calculate_health_score(checks: &[HealthCheckResult]) -> HealthScore {
488        if checks.is_empty() {
489            return HealthScore {
490                score: 50,
491                status: HealthStatus::Poor,
492            };
493        }
494
495        let mut score = 100u32;
496        let mut critical_found = false;
497
498        for check in checks {
499            match check.severity {
500                HealthSeverity::Critical => {
501                    critical_found = true;
502                    score = score.saturating_sub(20);
503                }
504                HealthSeverity::Error => {
505                    score = score.saturating_sub(10);
506                }
507                HealthSeverity::Warning => {
508                    score = score.saturating_sub(5);
509                }
510                HealthSeverity::Info => {
511                    // Info doesn't penalize score
512                }
513            }
514
515            if !check.passed {
516                score = score.saturating_sub(5);
517            }
518        }
519
520        // Apply additional penalty for critical checks
521        if critical_found {
522            score = score.min(30);
523        }
524
525        // Ensure score is in valid range
526        score = score.min(100);
527
528        HealthScore {
529            score,
530            status: HealthStatus::from_score(score),
531        }
532    }
533
534    /// Generate health recommendations based on check results
535    pub fn generate_recommendations(
536        checks: &[HealthCheckResult],
537        score: HealthScore,
538    ) -> Vec<String> {
539        let mut recommendations = Vec::new();
540
541        // Add recommendations based on failed checks
542        for check in checks {
543            if !check.passed {
544                if let Some(suggestion) = &check.suggestion {
545                    recommendations.push(suggestion.clone());
546                }
547            }
548        }
549
550        // Add score-based recommendations
551        match score.status {
552            HealthStatus::Critical => {
553                recommendations.push("URGENT: Plugin has critical health issues that must be addressed before deployment".to_string());
554            }
555            HealthStatus::Poor => {
556                recommendations.push("Plugin has multiple issues. Review all failed checks and fix high-priority items.".to_string());
557            }
558            HealthStatus::Fair => {
559                recommendations.push(
560                    "Plugin has some issues. Address warnings and errors to improve health score."
561                        .to_string(),
562                );
563            }
564            HealthStatus::Good => {
565                recommendations
566                    .push("Plugin is healthy. Continue monitoring for regressions.".to_string());
567            }
568            HealthStatus::Excellent => {
569                recommendations.push(
570                    "Excellent plugin health. Maintain current quality standards.".to_string(),
571                );
572            }
573        }
574
575        recommendations
576    }
577}
578
579impl Default for PluginHealthChecker {
580    fn default() -> Self {
581        Self::new()
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    #[test]
590    fn test_health_severity_to_str() {
591        assert_eq!(HealthSeverity::Info.as_str(), "info");
592        assert_eq!(HealthSeverity::Warning.as_str(), "warning");
593        assert_eq!(HealthSeverity::Error.as_str(), "error");
594        assert_eq!(HealthSeverity::Critical.as_str(), "critical");
595    }
596
597    #[test]
598    fn test_health_severity_try_parse() {
599        assert_eq!(
600            HealthSeverity::try_parse("info"),
601            Some(HealthSeverity::Info)
602        );
603        assert_eq!(
604            HealthSeverity::try_parse("warning"),
605            Some(HealthSeverity::Warning)
606        );
607        assert_eq!(
608            HealthSeverity::try_parse("error"),
609            Some(HealthSeverity::Error)
610        );
611        assert_eq!(
612            HealthSeverity::try_parse("critical"),
613            Some(HealthSeverity::Critical)
614        );
615        assert_eq!(HealthSeverity::try_parse("unknown"), None);
616    }
617
618    #[test]
619    fn test_health_severity_ordering() {
620        assert!(HealthSeverity::Info < HealthSeverity::Warning);
621        assert!(HealthSeverity::Warning < HealthSeverity::Error);
622        assert!(HealthSeverity::Error < HealthSeverity::Critical);
623    }
624
625    #[test]
626    fn test_platform_current() {
627        let platform = Platform::current();
628        assert!(!matches!(platform, Platform::Unknown(_)));
629    }
630
631    #[test]
632    fn test_platform_to_str() {
633        assert_eq!(Platform::Linux.as_str(), "linux");
634        assert_eq!(Platform::MacOS.as_str(), "macos");
635        assert_eq!(Platform::Windows.as_str(), "windows");
636    }
637
638    #[test]
639    fn test_architecture_current() {
640        let arch = Architecture::current();
641        assert!(!matches!(arch, Architecture::Unknown(_)));
642    }
643
644    #[test]
645    fn test_architecture_to_str() {
646        assert_eq!(Architecture::X86_64.as_str(), "x86_64");
647        assert_eq!(Architecture::Arm64.as_str(), "arm64");
648        assert_eq!(Architecture::X86.as_str(), "x86");
649        assert_eq!(Architecture::Arm.as_str(), "arm");
650    }
651
652    #[test]
653    fn test_health_status_from_score() {
654        assert_eq!(HealthStatus::from_score(95), HealthStatus::Excellent);
655        assert_eq!(HealthStatus::from_score(80), HealthStatus::Good);
656        assert_eq!(HealthStatus::from_score(65), HealthStatus::Fair);
657        assert_eq!(HealthStatus::from_score(50), HealthStatus::Poor);
658        assert_eq!(HealthStatus::from_score(20), HealthStatus::Critical);
659    }
660
661    #[test]
662    fn test_health_status_to_str() {
663        assert_eq!(HealthStatus::Excellent.as_str(), "Excellent");
664        assert_eq!(HealthStatus::Good.as_str(), "Good");
665        assert_eq!(HealthStatus::Fair.as_str(), "Fair");
666        assert_eq!(HealthStatus::Poor.as_str(), "Poor");
667        assert_eq!(HealthStatus::Critical.as_str(), "Critical");
668    }
669
670    #[test]
671    fn test_checker_creation() {
672        let checker = PluginHealthChecker::new();
673        assert!(!checker.required_symbols.is_empty());
674    }
675
676    #[test]
677    fn test_checker_default_symbols() {
678        let checker = PluginHealthChecker::new();
679        assert!(checker.required_symbols.contains_key("plugin_init"));
680        assert!(checker.required_symbols.contains_key("plugin_get_info"));
681        assert!(checker.required_symbols.contains_key("plugin_shutdown"));
682    }
683
684    #[test]
685    fn test_required_symbol_properties() {
686        let checker = PluginHealthChecker::new();
687        let init_sym = checker.required_symbols.get("plugin_init").unwrap();
688        assert!(init_sym.required);
689
690        let shutdown_sym = checker.required_symbols.get("plugin_shutdown").unwrap();
691        assert!(!shutdown_sym.required);
692    }
693
694    #[test]
695    fn test_check_binary_exists_nonexistent() {
696        let checker = PluginHealthChecker::new();
697        let result = checker.check_binary_exists(Path::new("/nonexistent/plugin.so"));
698        assert!(!result.passed);
699        assert_eq!(result.severity, HealthSeverity::Critical);
700    }
701
702    #[test]
703    fn test_performance_baseline_check() {
704        let checker = PluginHealthChecker::new();
705        let baseline = PerformanceBaseline {
706            init_time_ms: 100.0,
707            shutdown_time_ms: 50.0,
708            memory_usage_mb: 100.0,
709            max_concurrent_calls: 50,
710        };
711        let result = checker.check_performance_baseline(&baseline);
712        assert!(result.passed);
713    }
714
715    #[test]
716    fn test_performance_baseline_check_failure() {
717        let checker = PluginHealthChecker::new();
718        let baseline = PerformanceBaseline {
719            init_time_ms: 10000.0, // Exceeds threshold
720            shutdown_time_ms: 50.0,
721            memory_usage_mb: 100.0,
722            max_concurrent_calls: 50,
723        };
724        let result = checker.check_performance_baseline(&baseline);
725        assert!(!result.passed);
726        assert!(result.details.is_some());
727    }
728
729    #[test]
730    fn test_calculate_health_score_all_passed() {
731        let checks = vec![
732            HealthCheckResult {
733                check_name: "Check 1".to_string(),
734                passed: true,
735                severity: HealthSeverity::Info,
736                message: "Passed".to_string(),
737                details: None,
738                suggestion: None,
739            },
740            HealthCheckResult {
741                check_name: "Check 2".to_string(),
742                passed: true,
743                severity: HealthSeverity::Info,
744                message: "Passed".to_string(),
745                details: None,
746                suggestion: None,
747            },
748        ];
749        let score = PluginHealthChecker::calculate_health_score(&checks);
750        assert_eq!(score.score, 100);
751        assert_eq!(score.status, HealthStatus::Excellent);
752    }
753
754    #[test]
755    fn test_calculate_health_score_with_errors() {
756        let checks = vec![
757            HealthCheckResult {
758                check_name: "Check 1".to_string(),
759                passed: true,
760                severity: HealthSeverity::Info,
761                message: "Passed".to_string(),
762                details: None,
763                suggestion: None,
764            },
765            HealthCheckResult {
766                check_name: "Check 2".to_string(),
767                passed: false,
768                severity: HealthSeverity::Error,
769                message: "Failed".to_string(),
770                details: None,
771                suggestion: None,
772            },
773        ];
774        let score = PluginHealthChecker::calculate_health_score(&checks);
775        assert!(score.score < 100);
776    }
777
778    #[test]
779    fn test_calculate_health_score_with_critical() {
780        let checks = vec![HealthCheckResult {
781            check_name: "Check 1".to_string(),
782            passed: false,
783            severity: HealthSeverity::Critical,
784            message: "Critical failure".to_string(),
785            details: None,
786            suggestion: None,
787        }];
788        let score = PluginHealthChecker::calculate_health_score(&checks);
789        assert!(score.score <= 30);
790        assert_eq!(score.status, HealthStatus::Critical);
791    }
792
793    #[test]
794    fn test_generate_recommendations_excellent() {
795        let checks = vec![];
796        let score = HealthScore {
797            score: 95,
798            status: HealthStatus::Excellent,
799        };
800        let recommendations = PluginHealthChecker::generate_recommendations(&checks, score);
801        assert!(!recommendations.is_empty());
802        assert!(recommendations[0].contains("Excellent"));
803    }
804
805    #[test]
806    fn test_generate_recommendations_critical() {
807        let checks = vec![];
808        let score = HealthScore {
809            score: 15,
810            status: HealthStatus::Critical,
811        };
812        let recommendations = PluginHealthChecker::generate_recommendations(&checks, score);
813        assert!(!recommendations.is_empty());
814        assert!(recommendations[0].to_uppercase().contains("URGENT"));
815    }
816
817    #[test]
818    fn test_required_symbols_check() {
819        let checker = PluginHealthChecker::new();
820        let result = checker.check_required_symbols();
821        assert!(result.passed);
822    }
823
824    #[test]
825    fn test_platform_compatibility_check() {
826        let checker = PluginHealthChecker::new();
827        let result =
828            checker.check_platform_compatibility(Path::new("/tmp/test.so"), Platform::Linux);
829        // File doesn't exist, but platform extension is checked
830        assert!(!result.passed);
831    }
832
833    #[test]
834    fn test_health_check_result_serialization() {
835        let result = HealthCheckResult {
836            check_name: "Test Check".to_string(),
837            passed: true,
838            severity: HealthSeverity::Info,
839            message: "Test message".to_string(),
840            details: Some("Details".to_string()),
841            suggestion: Some("Suggestion".to_string()),
842        };
843
844        let json = serde_json::to_string(&result).unwrap();
845        let deserialized: HealthCheckResult = serde_json::from_str(&json).unwrap();
846
847        assert_eq!(deserialized.check_name, result.check_name);
848        assert_eq!(deserialized.passed, result.passed);
849        assert_eq!(deserialized.severity, result.severity);
850    }
851
852    #[test]
853    fn test_health_report_serialization() {
854        let report = HealthReport {
855            plugin_id: "test-plugin".to_string(),
856            plugin_version: "1.0.0".to_string(),
857            check_timestamp: "2024-01-01T00:00:00Z".to_string(),
858            overall_health: HealthScore {
859                score: 85,
860                status: HealthStatus::Good,
861            },
862            checks: vec![],
863            binary_compatibility: None,
864            performance_baseline: None,
865            recommendations: vec![],
866        };
867
868        let json = serde_json::to_string(&report).unwrap();
869        let deserialized: HealthReport = serde_json::from_str(&json).unwrap();
870
871        assert_eq!(deserialized.plugin_id, report.plugin_id);
872        assert_eq!(deserialized.plugin_version, report.plugin_version);
873    }
874
875    #[test]
876    fn test_performance_baseline_serialization() {
877        let baseline = PerformanceBaseline {
878            init_time_ms: 100.0,
879            shutdown_time_ms: 50.0,
880            memory_usage_mb: 250.0,
881            max_concurrent_calls: 100,
882        };
883
884        let json = serde_json::to_string(&baseline).unwrap();
885        let deserialized: PerformanceBaseline = serde_json::from_str(&json).unwrap();
886
887        assert_eq!(deserialized.init_time_ms, baseline.init_time_ms);
888        assert_eq!(
889            deserialized.max_concurrent_calls,
890            baseline.max_concurrent_calls
891        );
892    }
893}