Skip to main content

batuta/stack/
types.rs

1//! Core types for PAIML Stack Orchestration
2//!
3//! These types represent the domain model for dependency management,
4//! health checking, and coordinated releases.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10/// Represents a crate in the PAIML stack
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct CrateInfo {
13    /// Crate name (e.g., "trueno")
14    pub name: String,
15
16    /// Local version from Cargo.toml
17    pub local_version: semver::Version,
18
19    /// Version published on crates.io (None if not published)
20    pub crates_io_version: Option<semver::Version>,
21
22    /// Path to the crate's Cargo.toml
23    pub manifest_path: PathBuf,
24
25    /// Dependencies on other PAIML crates
26    pub paiml_dependencies: Vec<DependencyInfo>,
27
28    /// External dependencies (non-PAIML)
29    pub external_dependencies: Vec<DependencyInfo>,
30
31    /// Health status
32    pub status: CrateStatus,
33
34    /// List of issues found
35    pub issues: Vec<CrateIssue>,
36}
37
38impl CrateInfo {
39    /// Create a new CrateInfo with minimal data
40    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    /// Check if crate has any path dependencies
54    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    /// Check if crate version is ahead of crates.io
60    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, // Not published yet
64        }
65    }
66
67    /// Check if crate is in sync with crates.io
68    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/// Information about a dependency
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct DependencyInfo {
79    /// Dependency name
80    pub name: String,
81
82    /// Version requirement (e.g., "^1.0", ">=0.5")
83    pub version_req: String,
84
85    /// Whether this is a path dependency
86    pub is_path: bool,
87
88    /// Path if path dependency
89    pub path: Option<PathBuf>,
90
91    /// Whether this is a PAIML crate
92    pub is_paiml: bool,
93
94    /// Dependency kind (normal, dev, build)
95    pub kind: DependencyKind,
96}
97
98impl DependencyInfo {
99    /// Create a new dependency info
100    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    /// Create a path dependency
114    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/// Kind of dependency
129#[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/// Health status of a crate
139#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
140#[serde(rename_all = "lowercase")]
141pub enum CrateStatus {
142    /// Crate is healthy - all checks pass
143    Healthy,
144
145    /// Crate has warnings but is functional
146    Warning,
147
148    /// Crate has errors that block release
149    Error,
150
151    /// Status not yet determined
152    #[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/// Issue found during health check
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169pub struct CrateIssue {
170    /// Issue severity
171    pub severity: IssueSeverity,
172
173    /// Issue type
174    pub issue_type: IssueType,
175
176    /// Human-readable message
177    pub message: String,
178
179    /// Suggested fix
180    pub suggestion: Option<String>,
181}
182
183impl CrateIssue {
184    /// Create a new issue
185    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    /// Add a suggestion for fixing the issue
190    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
191        self.suggestion = Some(suggestion.into());
192        self
193    }
194}
195
196/// Severity of an issue
197#[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/// Type of issue
206#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
207#[serde(rename_all = "snake_case")]
208pub enum IssueType {
209    /// Path dependency that should be crates.io
210    PathDependency,
211
212    /// Version conflict between crates
213    VersionConflict,
214
215    /// Crate not published to crates.io
216    NotPublished,
217
218    /// Local version behind crates.io
219    VersionBehind,
220
221    /// Circular dependency detected
222    CircularDependency,
223
224    /// Missing dependency
225    MissingDependency,
226
227    /// Quality gate failure (lint, coverage)
228    QualityGate,
229
230    /// Uncommitted changes in git
231    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/// Result of a stack health check
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct StackHealthReport {
252    /// Timestamp of the check
253    pub timestamp: chrono::DateTime<chrono::Utc>,
254
255    /// All crates analyzed
256    pub crates: Vec<CrateInfo>,
257
258    /// Version conflicts detected
259    pub conflicts: Vec<VersionConflict>,
260
261    /// Summary statistics
262    pub summary: HealthSummary,
263}
264
265impl StackHealthReport {
266    /// Create a new health report
267    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    /// Check if the stack is healthy (no errors)
273    pub fn is_healthy(&self) -> bool {
274        self.summary.error_count == 0 && self.conflicts.is_empty()
275    }
276}
277
278/// Version conflict between crates
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
280pub struct VersionConflict {
281    /// Name of the conflicting dependency
282    pub dependency: String,
283
284    /// Crates involved and their required versions
285    pub usages: Vec<ConflictUsage>,
286
287    /// Recommended version to align on
288    pub recommendation: Option<String>,
289}
290
291/// Usage in a version conflict
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293pub struct ConflictUsage {
294    /// Crate that has the dependency
295    pub crate_name: String,
296
297    /// Version required
298    pub version_req: String,
299}
300
301/// Summary of stack health
302#[derive(Debug, Clone, Serialize, Deserialize, Default)]
303pub struct HealthSummary {
304    /// Total number of crates
305    pub total_crates: usize,
306
307    /// Number of healthy crates
308    pub healthy_count: usize,
309
310    /// Number of crates with warnings
311    pub warning_count: usize,
312
313    /// Number of crates with errors
314    pub error_count: usize,
315
316    /// Number of path dependencies found
317    pub path_dependency_count: usize,
318
319    /// Number of version conflicts
320    pub conflict_count: usize,
321}
322
323impl HealthSummary {
324    /// Create summary from crate list
325    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/// Release plan for coordinated release
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ReleasePlan {
348    /// Releases in topological order
349    pub releases: Vec<PlannedRelease>,
350
351    /// Whether this is a dry run
352    pub dry_run: bool,
353
354    /// Pre-flight check results
355    pub preflight_results: HashMap<String, PreflightResult>,
356}
357
358/// A planned release for a single crate
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct PlannedRelease {
361    /// Crate name
362    pub crate_name: String,
363
364    /// Current version
365    pub current_version: semver::Version,
366
367    /// New version to release
368    pub new_version: semver::Version,
369
370    /// Crates that depend on this release
371    pub dependents: Vec<String>,
372
373    /// Whether release is ready (all checks passed)
374    pub ready: bool,
375}
376
377/// Result of pre-flight checks
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct PreflightResult {
380    /// Crate name
381    pub crate_name: String,
382
383    /// Individual check results
384    pub checks: Vec<PreflightCheck>,
385
386    /// Overall pass/fail
387    pub passed: bool,
388}
389
390impl PreflightResult {
391    /// Create a new preflight result
392    pub fn new(crate_name: impl Into<String>) -> Self {
393        Self { crate_name: crate_name.into(), checks: Vec::new(), passed: true }
394    }
395
396    /// Add a check result
397    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/// Individual pre-flight check
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct PreflightCheck {
408    /// Check name
409    pub name: String,
410
411    /// Whether check passed
412    pub passed: bool,
413
414    /// Details/message
415    pub message: String,
416}
417
418impl PreflightCheck {
419    /// Create a passing check
420    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    /// Create a failing check
425    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/// Output format for stack commands
431#[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        // Not published yet
477        assert!(info.is_ahead_of_crates_io());
478        assert!(!info.is_synced());
479
480        // Same version
481        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        // Local ahead
486        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    // ============================================================================
603    // TYPES-001: OutputFormat tests
604    // ============================================================================
605
606    /// RED PHASE: Test OutputFormat default
607    #[test]
608    fn test_TYPES_001_output_format_default() {
609        let format = OutputFormat::default();
610        assert_eq!(format, OutputFormat::Text);
611    }
612
613    /// RED PHASE: Test OutputFormat equality
614    #[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    /// RED PHASE: Test OutputFormat debug
623    #[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    /// RED PHASE: Test OutputFormat clone
631    #[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    // ============================================================================
639    // TYPES-002: PreflightCheck tests
640    // ============================================================================
641
642    /// RED PHASE: Test PreflightCheck::pass
643    #[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    /// RED PHASE: Test PreflightCheck::fail
653    #[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    /// RED PHASE: Test PreflightCheck serialization
663    #[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    /// RED PHASE: Test PreflightResult serialization
674    #[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    // ============================================================================
685    // TYPES-003: CrateStatus tests
686    // ============================================================================
687
688    /// RED PHASE: Test CrateStatus variants
689    #[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    /// RED PHASE: Test CrateStatus debug
699    #[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    /// RED PHASE: Test CrateStatus default
708    #[test]
709    fn test_TYPES_003_crate_status_default() {
710        let status = CrateStatus::default();
711        assert_eq!(status, CrateStatus::Unknown);
712    }
713
714    // ============================================================================
715    // TYPES-004: IssueSeverity and IssueType tests
716    // ============================================================================
717
718    /// RED PHASE: Test IssueSeverity variants
719    #[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    /// RED PHASE: Test IssueType variants
728    #[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    /// RED PHASE: Test CrateIssue without suggestion
740    #[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    // ============================================================================
750    // TYPES-005: CrateInfo edge cases
751    // ============================================================================
752
753    /// RED PHASE: Test CrateInfo clone
754    #[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    /// RED PHASE: Test CrateInfo debug
765    #[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    /// RED PHASE: Test CrateInfo serialization
779    #[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    // ============================================================================
793    // TYPES-006: DependencyInfo edge cases
794    // ============================================================================
795
796    /// RED PHASE: Test DependencyInfo clone
797    #[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    /// RED PHASE: Test DependencyInfo debug
807    #[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    /// RED PHASE: Test non-PAIML dependency
817    #[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    // ============================================================================
827    // TYPES-007: StackHealthReport edge cases
828    // ============================================================================
829
830    /// RED PHASE: Test StackHealthReport with warnings (still healthy)
831    #[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        // Warnings don't prevent healthy status (only errors and conflicts do)
842        assert!(report.is_healthy());
843        assert_eq!(report.summary.warning_count, 1);
844    }
845
846    /// RED PHASE: Test StackHealthReport with errors
847    #[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        // Errors make the report unhealthy
858        assert!(!report.is_healthy());
859        assert_eq!(report.summary.error_count, 1);
860    }
861
862    /// RED PHASE: Test StackHealthReport with conflicts
863    #[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    // ============================================================================
883    // TYPES-008: PlannedRelease and ReleasePlan edge cases
884    // ============================================================================
885
886    /// RED PHASE: Test PlannedRelease not ready
887    #[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    /// RED PHASE: Test ReleasePlan clone
902    #[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    /// RED PHASE: Test HealthSummary serialization
922    #[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}