1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct CrateInfo {
13 pub name: String,
15
16 pub local_version: semver::Version,
18
19 pub crates_io_version: Option<semver::Version>,
21
22 pub manifest_path: PathBuf,
24
25 pub paiml_dependencies: Vec<DependencyInfo>,
27
28 pub external_dependencies: Vec<DependencyInfo>,
30
31 pub status: CrateStatus,
33
34 pub issues: Vec<CrateIssue>,
36}
37
38impl CrateInfo {
39 pub fn new(name: impl Into<String>, version: semver::Version, manifest_path: PathBuf) -> Self {
41 Self {
42 name: name.into(),
43 local_version: version,
44 crates_io_version: None,
45 manifest_path,
46 paiml_dependencies: Vec::new(),
47 external_dependencies: Vec::new(),
48 status: CrateStatus::Unknown,
49 issues: Vec::new(),
50 }
51 }
52
53 pub fn has_path_dependencies(&self) -> bool {
55 self.paiml_dependencies.iter().any(|d| d.is_path)
56 || self.external_dependencies.iter().any(|d| d.is_path)
57 }
58
59 pub fn is_ahead_of_crates_io(&self) -> bool {
61 match &self.crates_io_version {
62 Some(remote) => self.local_version > *remote,
63 None => true, }
65 }
66
67 pub fn is_synced(&self) -> bool {
69 match &self.crates_io_version {
70 Some(remote) => self.local_version == *remote,
71 None => false,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct DependencyInfo {
79 pub name: String,
81
82 pub version_req: String,
84
85 pub is_path: bool,
87
88 pub path: Option<PathBuf>,
90
91 pub is_paiml: bool,
93
94 pub kind: DependencyKind,
96}
97
98impl DependencyInfo {
99 pub fn new(name: impl Into<String>, version_req: impl Into<String>) -> Self {
101 let name = name.into();
102 let is_paiml = super::is_paiml_crate(&name);
103 Self {
104 name,
105 version_req: version_req.into(),
106 is_path: false,
107 path: None,
108 is_paiml,
109 kind: DependencyKind::Normal,
110 }
111 }
112
113 pub fn path(name: impl Into<String>, path: PathBuf) -> Self {
115 let name = name.into();
116 let is_paiml = super::is_paiml_crate(&name);
117 Self {
118 name,
119 version_req: String::new(),
120 is_path: true,
121 path: Some(path),
122 is_paiml,
123 kind: DependencyKind::Normal,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
130#[serde(rename_all = "lowercase")]
131pub enum DependencyKind {
132 #[default]
133 Normal,
134 Dev,
135 Build,
136}
137
138#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
140#[serde(rename_all = "lowercase")]
141pub enum CrateStatus {
142 Healthy,
144
145 Warning,
147
148 Error,
150
151 #[default]
153 Unknown,
154}
155
156impl std::fmt::Display for CrateStatus {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 CrateStatus::Healthy => write!(f, "healthy"),
160 CrateStatus::Warning => write!(f, "warning"),
161 CrateStatus::Error => write!(f, "error"),
162 CrateStatus::Unknown => write!(f, "unknown"),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169pub struct CrateIssue {
170 pub severity: IssueSeverity,
172
173 pub issue_type: IssueType,
175
176 pub message: String,
178
179 pub suggestion: Option<String>,
181}
182
183impl CrateIssue {
184 pub fn new(severity: IssueSeverity, issue_type: IssueType, message: impl Into<String>) -> Self {
186 Self { severity, issue_type, message: message.into(), suggestion: None }
187 }
188
189 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
191 self.suggestion = Some(suggestion.into());
192 self
193 }
194}
195
196#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
198#[serde(rename_all = "lowercase")]
199pub enum IssueSeverity {
200 Info,
201 Warning,
202 Error,
203}
204
205#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
207#[serde(rename_all = "snake_case")]
208pub enum IssueType {
209 PathDependency,
211
212 VersionConflict,
214
215 NotPublished,
217
218 VersionBehind,
220
221 CircularDependency,
223
224 MissingDependency,
226
227 QualityGate,
229
230 UncommittedChanges,
232}
233
234impl std::fmt::Display for IssueType {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 match self {
237 IssueType::PathDependency => write!(f, "path dependency"),
238 IssueType::VersionConflict => write!(f, "version conflict"),
239 IssueType::NotPublished => write!(f, "not published"),
240 IssueType::VersionBehind => write!(f, "version behind"),
241 IssueType::CircularDependency => write!(f, "circular dependency"),
242 IssueType::MissingDependency => write!(f, "missing dependency"),
243 IssueType::QualityGate => write!(f, "quality gate"),
244 IssueType::UncommittedChanges => write!(f, "uncommitted changes"),
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct StackHealthReport {
252 pub timestamp: chrono::DateTime<chrono::Utc>,
254
255 pub crates: Vec<CrateInfo>,
257
258 pub conflicts: Vec<VersionConflict>,
260
261 pub summary: HealthSummary,
263}
264
265impl StackHealthReport {
266 pub fn new(crates: Vec<CrateInfo>, conflicts: Vec<VersionConflict>) -> Self {
268 let summary = HealthSummary::from_crates(&crates);
269 Self { timestamp: chrono::Utc::now(), crates, conflicts, summary }
270 }
271
272 pub fn is_healthy(&self) -> bool {
274 self.summary.error_count == 0 && self.conflicts.is_empty()
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
280pub struct VersionConflict {
281 pub dependency: String,
283
284 pub usages: Vec<ConflictUsage>,
286
287 pub recommendation: Option<String>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293pub struct ConflictUsage {
294 pub crate_name: String,
296
297 pub version_req: String,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize, Default)]
303pub struct HealthSummary {
304 pub total_crates: usize,
306
307 pub healthy_count: usize,
309
310 pub warning_count: usize,
312
313 pub error_count: usize,
315
316 pub path_dependency_count: usize,
318
319 pub conflict_count: usize,
321}
322
323impl HealthSummary {
324 pub fn from_crates(crates: &[CrateInfo]) -> Self {
326 let mut summary = Self { total_crates: crates.len(), ..Default::default() };
327
328 for crate_info in crates {
329 match crate_info.status {
330 CrateStatus::Healthy => summary.healthy_count += 1,
331 CrateStatus::Warning => summary.warning_count += 1,
332 CrateStatus::Error => summary.error_count += 1,
333 CrateStatus::Unknown => {}
334 }
335
336 if crate_info.has_path_dependencies() {
337 summary.path_dependency_count += 1;
338 }
339 }
340
341 summary
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ReleasePlan {
348 pub releases: Vec<PlannedRelease>,
350
351 pub dry_run: bool,
353
354 pub preflight_results: HashMap<String, PreflightResult>,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct PlannedRelease {
361 pub crate_name: String,
363
364 pub current_version: semver::Version,
366
367 pub new_version: semver::Version,
369
370 pub dependents: Vec<String>,
372
373 pub ready: bool,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct PreflightResult {
380 pub crate_name: String,
382
383 pub checks: Vec<PreflightCheck>,
385
386 pub passed: bool,
388}
389
390impl PreflightResult {
391 pub fn new(crate_name: impl Into<String>) -> Self {
393 Self { crate_name: crate_name.into(), checks: Vec::new(), passed: true }
394 }
395
396 pub fn add_check(&mut self, check: PreflightCheck) {
398 if !check.passed {
399 self.passed = false;
400 }
401 self.checks.push(check);
402 }
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct PreflightCheck {
408 pub name: String,
410
411 pub passed: bool,
413
414 pub message: String,
416}
417
418impl PreflightCheck {
419 pub fn pass(name: impl Into<String>, message: impl Into<String>) -> Self {
421 Self { name: name.into(), passed: true, message: message.into() }
422 }
423
424 pub fn fail(name: impl Into<String>, message: impl Into<String>) -> Self {
426 Self { name: name.into(), passed: false, message: message.into() }
427 }
428}
429
430#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
432pub enum OutputFormat {
433 #[default]
434 Text,
435 Json,
436 Markdown,
437}
438
439#[cfg(test)]
440#[allow(non_snake_case)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_crate_info_new() {
446 let info = CrateInfo::new(
447 "trueno",
448 semver::Version::new(1, 2, 0),
449 PathBuf::from("/path/to/Cargo.toml"),
450 );
451
452 assert_eq!(info.name, "trueno");
453 assert_eq!(info.local_version, semver::Version::new(1, 2, 0));
454 assert_eq!(info.status, CrateStatus::Unknown);
455 assert!(info.issues.is_empty());
456 }
457
458 #[test]
459 fn test_crate_info_path_dependencies() {
460 let mut info =
461 CrateInfo::new("entrenar", semver::Version::new(0, 2, 2), PathBuf::from("Cargo.toml"));
462
463 assert!(!info.has_path_dependencies());
464
465 info.paiml_dependencies
466 .push(DependencyInfo::path("alimentar", PathBuf::from("../alimentar")));
467
468 assert!(info.has_path_dependencies());
469 }
470
471 #[test]
472 fn test_crate_info_version_comparison() {
473 let mut info =
474 CrateInfo::new("trueno", semver::Version::new(1, 2, 0), PathBuf::from("Cargo.toml"));
475
476 assert!(info.is_ahead_of_crates_io());
478 assert!(!info.is_synced());
479
480 info.crates_io_version = Some(semver::Version::new(1, 2, 0));
482 assert!(!info.is_ahead_of_crates_io());
483 assert!(info.is_synced());
484
485 info.local_version = semver::Version::new(1, 3, 0);
487 assert!(info.is_ahead_of_crates_io());
488 assert!(!info.is_synced());
489 }
490
491 #[test]
492 fn test_dependency_info_paiml_detection() {
493 let dep = DependencyInfo::new("trueno", "^1.0");
494 assert!(dep.is_paiml);
495 assert!(!dep.is_path);
496
497 let dep = DependencyInfo::new("serde", "1.0");
498 assert!(!dep.is_paiml);
499
500 let dep = DependencyInfo::path("aprender", PathBuf::from("../aprender"));
501 assert!(dep.is_paiml);
502 assert!(dep.is_path);
503 }
504
505 #[test]
506 fn test_crate_issue_creation() {
507 let issue = CrateIssue::new(
508 IssueSeverity::Error,
509 IssueType::PathDependency,
510 "alimentar uses path dependency",
511 )
512 .with_suggestion("Change to: alimentar = \"0.3.0\"");
513
514 assert_eq!(issue.severity, IssueSeverity::Error);
515 assert_eq!(issue.issue_type, IssueType::PathDependency);
516 assert!(issue.suggestion.is_some());
517 }
518
519 #[test]
520 fn test_health_summary_from_crates() {
521 let crates = vec![
522 {
523 let mut c = CrateInfo::new("trueno", semver::Version::new(1, 0, 0), PathBuf::new());
524 c.status = CrateStatus::Healthy;
525 c
526 },
527 {
528 let mut c =
529 CrateInfo::new("aprender", semver::Version::new(0, 8, 0), PathBuf::new());
530 c.status = CrateStatus::Warning;
531 c
532 },
533 {
534 let mut c =
535 CrateInfo::new("entrenar", semver::Version::new(0, 2, 0), PathBuf::new());
536 c.status = CrateStatus::Error;
537 c.paiml_dependencies
538 .push(DependencyInfo::path("alimentar", PathBuf::from("../alimentar")));
539 c
540 },
541 ];
542
543 let summary = HealthSummary::from_crates(&crates);
544
545 assert_eq!(summary.total_crates, 3);
546 assert_eq!(summary.healthy_count, 1);
547 assert_eq!(summary.warning_count, 1);
548 assert_eq!(summary.error_count, 1);
549 assert_eq!(summary.path_dependency_count, 1);
550 }
551
552 #[test]
553 fn test_preflight_result() {
554 let mut result = PreflightResult::new("trueno");
555 assert!(result.passed);
556
557 result.add_check(PreflightCheck::pass("lint", "No errors"));
558 assert!(result.passed);
559
560 result.add_check(PreflightCheck::fail("coverage", "Coverage 85% < 90%"));
561 assert!(!result.passed);
562
563 assert_eq!(result.checks.len(), 2);
564 }
565
566 #[test]
567 fn test_stack_health_report() {
568 let crates = vec![{
569 let mut c = CrateInfo::new("trueno", semver::Version::new(1, 0, 0), PathBuf::new());
570 c.status = CrateStatus::Healthy;
571 c
572 }];
573
574 let report = StackHealthReport::new(crates, vec![]);
575
576 assert!(report.is_healthy());
577 assert_eq!(report.summary.total_crates, 1);
578 assert_eq!(report.summary.healthy_count, 1);
579 }
580
581 #[test]
582 fn test_version_conflict() {
583 let conflict = VersionConflict {
584 dependency: "arrow".to_string(),
585 usages: vec![
586 ConflictUsage {
587 crate_name: "renacer".to_string(),
588 version_req: "54.0".to_string(),
589 },
590 ConflictUsage {
591 crate_name: "trueno-graph".to_string(),
592 version_req: "53.0".to_string(),
593 },
594 ],
595 recommendation: Some("Upgrade to arrow 54.0".to_string()),
596 };
597
598 assert_eq!(conflict.usages.len(), 2);
599 assert!(conflict.recommendation.is_some());
600 }
601
602 #[test]
608 fn test_TYPES_001_output_format_default() {
609 let format = OutputFormat::default();
610 assert_eq!(format, OutputFormat::Text);
611 }
612
613 #[test]
615 fn test_TYPES_001_output_format_equality() {
616 assert_eq!(OutputFormat::Text, OutputFormat::Text);
617 assert_eq!(OutputFormat::Json, OutputFormat::Json);
618 assert_eq!(OutputFormat::Markdown, OutputFormat::Markdown);
619 assert_ne!(OutputFormat::Text, OutputFormat::Json);
620 }
621
622 #[test]
624 fn test_TYPES_001_output_format_debug() {
625 assert!(format!("{:?}", OutputFormat::Text).contains("Text"));
626 assert!(format!("{:?}", OutputFormat::Json).contains("Json"));
627 assert!(format!("{:?}", OutputFormat::Markdown).contains("Markdown"));
628 }
629
630 #[test]
632 fn test_TYPES_001_output_format_clone() {
633 let format = OutputFormat::Json;
634 let cloned = format;
635 assert_eq!(format, cloned);
636 }
637
638 #[test]
644 fn test_TYPES_002_preflight_check_pass() {
645 let check = PreflightCheck::pass("test_check", "All good");
646
647 assert!(check.passed);
648 assert_eq!(check.name, "test_check");
649 assert_eq!(check.message, "All good");
650 }
651
652 #[test]
654 fn test_TYPES_002_preflight_check_fail() {
655 let check = PreflightCheck::fail("lint_check", "Found 5 errors");
656
657 assert!(!check.passed);
658 assert_eq!(check.name, "lint_check");
659 assert_eq!(check.message, "Found 5 errors");
660 }
661
662 #[test]
664 fn test_TYPES_002_preflight_check_serialization() {
665 let check = PreflightCheck::pass("git", "clean");
666 let json = serde_json::to_string(&check).expect("json serialize failed");
667
668 assert!(json.contains("git"));
669 assert!(json.contains("clean"));
670 assert!(json.contains("true"));
671 }
672
673 #[test]
675 fn test_TYPES_002_preflight_result_serialization() {
676 let mut result = PreflightResult::new("test-crate");
677 result.add_check(PreflightCheck::pass("a", "ok"));
678
679 let json = serde_json::to_string(&result).expect("json serialize failed");
680 assert!(json.contains("test-crate"));
681 assert!(json.contains("passed"));
682 }
683
684 #[test]
690 fn test_TYPES_003_crate_status_variants() {
691 assert_eq!(CrateStatus::Unknown, CrateStatus::Unknown);
692 assert_eq!(CrateStatus::Healthy, CrateStatus::Healthy);
693 assert_eq!(CrateStatus::Warning, CrateStatus::Warning);
694 assert_eq!(CrateStatus::Error, CrateStatus::Error);
695 assert_ne!(CrateStatus::Healthy, CrateStatus::Error);
696 }
697
698 #[test]
700 fn test_TYPES_003_crate_status_debug() {
701 assert!(format!("{:?}", CrateStatus::Unknown).contains("Unknown"));
702 assert!(format!("{:?}", CrateStatus::Healthy).contains("Healthy"));
703 assert!(format!("{:?}", CrateStatus::Warning).contains("Warning"));
704 assert!(format!("{:?}", CrateStatus::Error).contains("Error"));
705 }
706
707 #[test]
709 fn test_TYPES_003_crate_status_default() {
710 let status = CrateStatus::default();
711 assert_eq!(status, CrateStatus::Unknown);
712 }
713
714 #[test]
720 fn test_TYPES_004_issue_severity_variants() {
721 assert_eq!(IssueSeverity::Info, IssueSeverity::Info);
722 assert_eq!(IssueSeverity::Warning, IssueSeverity::Warning);
723 assert_eq!(IssueSeverity::Error, IssueSeverity::Error);
724 assert_ne!(IssueSeverity::Info, IssueSeverity::Error);
725 }
726
727 #[test]
729 fn test_TYPES_004_issue_type_variants() {
730 assert_eq!(IssueType::PathDependency, IssueType::PathDependency);
731 assert_eq!(IssueType::VersionConflict, IssueType::VersionConflict);
732 assert_eq!(IssueType::NotPublished, IssueType::NotPublished);
733 assert_eq!(IssueType::VersionBehind, IssueType::VersionBehind);
734 assert_eq!(IssueType::CircularDependency, IssueType::CircularDependency);
735 assert_eq!(IssueType::MissingDependency, IssueType::MissingDependency);
736 assert_eq!(IssueType::QualityGate, IssueType::QualityGate);
737 }
738
739 #[test]
741 fn test_TYPES_004_crate_issue_no_suggestion() {
742 let issue =
743 CrateIssue::new(IssueSeverity::Info, IssueType::NotPublished, "Crate not on crates.io");
744
745 assert_eq!(issue.severity, IssueSeverity::Info);
746 assert!(issue.suggestion.is_none());
747 }
748
749 #[test]
755 fn test_TYPES_005_crate_info_clone() {
756 let info =
757 CrateInfo::new("test", semver::Version::new(1, 0, 0), PathBuf::from("Cargo.toml"));
758 let cloned = info.clone();
759
760 assert_eq!(info.name, cloned.name);
761 assert_eq!(info.local_version, cloned.local_version);
762 }
763
764 #[test]
766 fn test_TYPES_005_crate_info_debug() {
767 let info = CrateInfo::new(
768 "debug-test",
769 semver::Version::new(2, 0, 0),
770 PathBuf::from("Cargo.toml"),
771 );
772 let debug = format!("{:?}", info);
773
774 assert!(debug.contains("CrateInfo"));
775 assert!(debug.contains("debug-test"));
776 }
777
778 #[test]
780 fn test_TYPES_005_crate_info_serialization() {
781 let info = CrateInfo::new(
782 "serializable",
783 semver::Version::new(1, 2, 3),
784 PathBuf::from("path/Cargo.toml"),
785 );
786 let json = serde_json::to_string(&info).expect("json serialize failed");
787
788 assert!(json.contains("serializable"));
789 assert!(json.contains("1.2.3"));
790 }
791
792 #[test]
798 fn test_TYPES_006_dependency_info_clone() {
799 let dep = DependencyInfo::new("trueno", "^1.0");
800 let cloned = dep.clone();
801
802 assert_eq!(dep.name, cloned.name);
803 assert_eq!(dep.is_paiml, cloned.is_paiml);
804 }
805
806 #[test]
808 fn test_TYPES_006_dependency_info_debug() {
809 let dep = DependencyInfo::path("aprender", PathBuf::from("../aprender"));
810 let debug = format!("{:?}", dep);
811
812 assert!(debug.contains("DependencyInfo"));
813 assert!(debug.contains("aprender"));
814 }
815
816 #[test]
818 fn test_TYPES_006_non_paiml_dependency() {
819 let dep = DependencyInfo::new("tokio", "1.0");
820
821 assert!(!dep.is_paiml);
822 assert!(!dep.is_path);
823 assert_eq!(dep.version_req, "1.0");
824 }
825
826 #[test]
832 fn test_TYPES_007_health_report_with_warnings() {
833 let crates = vec![{
834 let mut c = CrateInfo::new("test", semver::Version::new(1, 0, 0), PathBuf::new());
835 c.status = CrateStatus::Warning;
836 c
837 }];
838
839 let report = StackHealthReport::new(crates, vec![]);
840
841 assert!(report.is_healthy());
843 assert_eq!(report.summary.warning_count, 1);
844 }
845
846 #[test]
848 fn test_TYPES_007_health_report_with_errors() {
849 let crates = vec![{
850 let mut c = CrateInfo::new("test", semver::Version::new(1, 0, 0), PathBuf::new());
851 c.status = CrateStatus::Error;
852 c
853 }];
854
855 let report = StackHealthReport::new(crates, vec![]);
856
857 assert!(!report.is_healthy());
859 assert_eq!(report.summary.error_count, 1);
860 }
861
862 #[test]
864 fn test_TYPES_007_health_report_with_conflicts() {
865 let crates = vec![{
866 let mut c = CrateInfo::new("test", semver::Version::new(1, 0, 0), PathBuf::new());
867 c.status = CrateStatus::Healthy;
868 c
869 }];
870
871 let conflicts = vec![VersionConflict {
872 dependency: "arrow".to_string(),
873 usages: vec![],
874 recommendation: None,
875 }];
876
877 let report = StackHealthReport::new(crates, conflicts);
878
879 assert!(!report.conflicts.is_empty());
880 }
881
882 #[test]
888 fn test_TYPES_008_planned_release_not_ready() {
889 let release = PlannedRelease {
890 crate_name: "broken".to_string(),
891 current_version: semver::Version::new(1, 0, 0),
892 new_version: semver::Version::new(1, 0, 1),
893 dependents: vec!["downstream".to_string()],
894 ready: false,
895 };
896
897 assert!(!release.ready);
898 assert_eq!(release.dependents.len(), 1);
899 }
900
901 #[test]
903 fn test_TYPES_008_release_plan_clone() {
904 let plan = ReleasePlan {
905 releases: vec![PlannedRelease {
906 crate_name: "test".to_string(),
907 current_version: semver::Version::new(0, 1, 0),
908 new_version: semver::Version::new(0, 1, 1),
909 dependents: vec![],
910 ready: true,
911 }],
912 dry_run: true,
913 preflight_results: std::collections::HashMap::new(),
914 };
915
916 let cloned = plan.clone();
917 assert_eq!(plan.releases.len(), cloned.releases.len());
918 assert_eq!(plan.dry_run, cloned.dry_run);
919 }
920
921 #[test]
923 fn test_TYPES_008_health_summary_serialization() {
924 let crates = vec![{
925 let mut c = CrateInfo::new("test", semver::Version::new(1, 0, 0), PathBuf::new());
926 c.status = CrateStatus::Healthy;
927 c
928 }];
929 let summary = HealthSummary::from_crates(&crates);
930
931 let json = serde_json::to_string(&summary).expect("json serialize failed");
932 assert!(json.contains("total_crates"));
933 assert!(json.contains("healthy_count"));
934 }
935}