Skip to main content

lau_shell_lifecycle/
lib.rs

1//! Shell lifecycle manager — from spawn to death, with self-assembling DNA.
2//!
3//! This crate provides a full lifecycle state machine for shell instances,
4//! including self-assembling DNA pathways that grow with use and decay with disuse.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9// ---------------------------------------------------------------------------
10// ShellType
11// ---------------------------------------------------------------------------
12
13/// The type of shell being managed.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(tag = "type", content = "value")]
16pub enum ShellType {
17    Hermes,
18    ZeroClaw,
19    CUDAClaw,
20    GitNative,
21    Remote { address: String },
22    Custom(String),
23}
24
25// ---------------------------------------------------------------------------
26// ShellConfig
27// ---------------------------------------------------------------------------
28
29/// Configuration for spawning a new shell.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct ShellConfig {
32    pub id: String,
33    pub name: String,
34    pub shell_type: ShellType,
35    pub universe_path: String,
36    pub conservation_budget: f64,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub parent_id: Option<String>,
39    pub max_children: usize,
40    pub capabilities: Vec<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub model: Option<String>,
43}
44
45impl ShellConfig {
46    pub fn to_json(&self) -> String {
47        serde_json::to_string(self).expect("ShellConfig serialization should not fail")
48    }
49
50    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
51        serde_json::from_str(json)
52    }
53}
54
55// ---------------------------------------------------------------------------
56// ShellState
57// ---------------------------------------------------------------------------
58
59/// Lifecycle states a shell can be in.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "PascalCase")]
62pub enum ShellState {
63    Conceived,
64    Spawning,
65    Bootstrapping,
66    Running,
67    Suspended,
68    Migrating,
69    Dying,
70    Dead,
71}
72
73impl ShellState {
74    /// Returns the set of states that are legal targets from this state.
75    fn legal_transitions(&self) -> &'static [ShellState] {
76        match self {
77            ShellState::Conceived => &[ShellState::Spawning],
78            ShellState::Spawning => &[ShellState::Bootstrapping],
79            ShellState::Bootstrapping => &[ShellState::Running],
80            ShellState::Running => &[
81                ShellState::Suspended,
82                ShellState::Migrating,
83                ShellState::Dying,
84            ],
85            ShellState::Suspended => &[ShellState::Running],
86            ShellState::Migrating => &[ShellState::Running],
87            ShellState::Dying => &[ShellState::Dead],
88            ShellState::Dead => &[],
89        }
90    }
91}
92
93// ---------------------------------------------------------------------------
94// LifecycleEvent
95// ---------------------------------------------------------------------------
96
97/// Events that can trigger lifecycle transitions.
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99#[serde(tag = "event", content = "payload")]
100pub enum LifecycleEvent {
101    Spawn {
102        parent: String,
103        config: ShellConfig,
104    },
105    Bootstrap,
106    Ready,
107    Suspend {
108        reason: String,
109    },
110    Resume,
111    Migrate {
112        target: String,
113    },
114    Kill {
115        reason: String,
116    },
117    HeartbeatReceived,
118    Timeout,
119    Error {
120        message: String,
121    },
122}
123
124// ---------------------------------------------------------------------------
125// LifecycleError
126// ---------------------------------------------------------------------------
127
128/// Errors that can occur during lifecycle operations.
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub enum LifecycleError {
131    InvalidTransition {
132        from: ShellState,
133        to: ShellState,
134    },
135    ShellNotFound(String),
136    AlreadyExists(String),
137    MaxChildrenReached(usize),
138    ParentNotRunning(String),
139}
140
141impl std::fmt::Display for LifecycleError {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            LifecycleError::InvalidTransition { from, to } => {
145                write!(f, "invalid transition from {:?} to {:?}", from, to)
146            }
147            LifecycleError::ShellNotFound(id) => write!(f, "shell not found: {}", id),
148            LifecycleError::AlreadyExists(id) => write!(f, "shell already exists: {}", id),
149            LifecycleError::MaxChildrenReached(max) => {
150                write!(f, "max children reached: {}", max)
151            }
152            LifecycleError::ParentNotRunning(id) => {
153                write!(f, "parent not running: {}", id)
154            }
155        }
156    }
157}
158
159impl std::error::Error for LifecycleError {}
160
161// ---------------------------------------------------------------------------
162// ShellLifecycle
163// ---------------------------------------------------------------------------
164
165/// The lifecycle state machine for a single shell.
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct ShellLifecycle {
168    pub state: ShellState,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub kill_reason: Option<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub suspend_reason: Option<String>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub migration_target: Option<String>,
175}
176
177impl ShellLifecycle {
178    pub fn new() -> Self {
179        Self {
180            state: ShellState::Conceived,
181            kill_reason: None,
182            suspend_reason: None,
183            migration_target: None,
184        }
185    }
186
187    pub fn can_transition_to(&self, target: &ShellState) -> bool {
188        self.state
189            .legal_transitions()
190            .contains(target)
191    }
192
193    /// Attempt a state transition based on the given event.
194    pub fn transition(&mut self, event: LifecycleEvent) -> Result<(), LifecycleError> {
195        let target = Self::target_state_for_event(&event);
196
197        // Special case: Kill can be applied from several states, not just Running.
198        // Timeout/Error map to Dead and can transition from Dying.
199        let is_kill_to_dying = matches!(&event, LifecycleEvent::Kill { .. }) && target == ShellState::Dying;
200        let is_terminal = matches!(&event, LifecycleEvent::Timeout | LifecycleEvent::Error { .. })
201            && self.state == ShellState::Dying;
202
203        if is_kill_to_dying {
204            // Allow Kill from: Running, Suspended, Spawning, Bootstrapping, Migrating
205            match self.state {
206                ShellState::Running
207                | ShellState::Suspended
208                | ShellState::Spawning
209                | ShellState::Bootstrapping
210                | ShellState::Migrating => {}
211                _ => {
212                    return Err(LifecycleError::InvalidTransition {
213                        from: self.state,
214                        to: target,
215                    });
216                }
217            }
218        } else if is_terminal {
219            // Allow Dying → Dead
220        } else if !self.can_transition_to(&target) {
221            return Err(LifecycleError::InvalidTransition {
222                from: self.state,
223                to: target,
224            });
225        }
226
227        // Apply side-effects
228        match event {
229            LifecycleEvent::Suspend { reason } => self.suspend_reason = Some(reason),
230            LifecycleEvent::Kill { reason } => self.kill_reason = Some(reason),
231            LifecycleEvent::Migrate { target: t } => self.migration_target = Some(t),
232            _ => {}
233        }
234
235        self.state = target;
236        Ok(())
237    }
238
239    fn target_state_for_event(event: &LifecycleEvent) -> ShellState {
240        match event {
241            LifecycleEvent::Spawn { .. } => ShellState::Spawning,
242            LifecycleEvent::Bootstrap => ShellState::Bootstrapping,
243            LifecycleEvent::Ready => ShellState::Running,
244            LifecycleEvent::Suspend { .. } => ShellState::Suspended,
245            LifecycleEvent::Resume => ShellState::Running,
246            LifecycleEvent::Migrate { .. } => ShellState::Migrating,
247            LifecycleEvent::Kill { .. } => ShellState::Dying,
248            LifecycleEvent::HeartbeatReceived => ShellState::Running,
249            LifecycleEvent::Timeout => ShellState::Dead,
250            LifecycleEvent::Error { .. } => ShellState::Dead,
251        }
252    }
253}
254
255impl Default for ShellLifecycle {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261// ---------------------------------------------------------------------------
262// Pathway
263// ---------------------------------------------------------------------------
264
265/// A single DNA pathway — grows with use, decays with disuse.
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267pub struct Pathway {
268    pub name: String,
269    pub use_count: u64,
270    pub strength: f64,
271    pub last_used: u64,
272    pub decay_rate: f64,
273    pub growth_rate: f64,
274    pub category: String,
275}
276
277impl Pathway {
278    pub fn new(name: &str, category: &str) -> Self {
279        Self {
280            name: name.to_string(),
281            use_count: 0,
282            strength: 0.0,
283            last_used: 0,
284            decay_rate: 0.01,
285            growth_rate: 0.1,
286            category: category.to_string(),
287        }
288    }
289
290    /// Record a single use — asymptotic growth toward 1.0.
291    pub fn use_once(&mut self) {
292        let growth = self.growth_rate * (1.0 - self.strength);
293        if growth > 0.0 {
294            self.strength += growth;
295            if self.strength > 1.0 {
296                self.strength = 1.0;
297            }
298        }
299        self.use_count += 1;
300    }
301
302    /// Apply one tick of linear decay.
303    pub fn decay(&mut self) {
304        self.strength -= self.decay_rate;
305        if self.strength < 0.0 {
306            self.strength = 0.0;
307        }
308    }
309}
310
311// ---------------------------------------------------------------------------
312// DNA
313// ---------------------------------------------------------------------------
314
315/// Self-assembling pathway collection — used pathways grow, unused get pruned.
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
317pub struct DNA {
318    pub pathways: HashMap<String, Pathway>,
319    pub tick_count: u64,
320    pub prune_threshold: f64,
321}
322
323impl DNA {
324    pub fn new() -> Self {
325        Self {
326            pathways: HashMap::new(),
327            tick_count: 0,
328            prune_threshold: 0.01,
329        }
330    }
331
332    /// Record use of a pathway, creating it if it doesn't exist.
333    pub fn record_use(&mut self, pathway: &str) {
334        let category = infer_category(pathway);
335        let p = self
336            .pathways
337            .entry(pathway.to_string())
338            .or_insert_with(|| Pathway::new(pathway, &category));
339        p.use_once();
340        p.last_used = self.tick_count;
341    }
342
343    /// Tick all pathways — decay and prune.
344    pub fn tick(&mut self) {
345        self.tick_count += 1;
346        for p in self.pathways.values_mut() {
347            p.decay();
348        }
349        self.pathways
350            .retain(|_, p| p.strength >= self.prune_threshold);
351    }
352
353    /// Get the top N strongest pathways.
354    pub fn strongest(&self, n: usize) -> Vec<(&String, &Pathway)> {
355        let mut v: Vec<_> = self.pathways.iter().collect();
356        v.sort_by(|a, b| b.1.strength.partial_cmp(&a.1.strength).unwrap_or(std::cmp::Ordering::Equal));
357        v.truncate(n);
358        v
359    }
360
361    /// Sum of all pathway strengths.
362    pub fn total_strength(&self) -> f64 {
363        self.pathways.values().map(|p| p.strength).sum()
364    }
365
366    /// Shannon entropy of the pathway strength distribution.
367    pub fn diversity(&self) -> f64 {
368        if self.pathways.is_empty() {
369            return 0.0;
370        }
371        let total = self.total_strength();
372        if total <= 0.0 {
373            return 0.0;
374        }
375        self.pathways
376            .values()
377            .map(|p| {
378                let prob = p.strength / total;
379                if prob > 0.0 {
380                    -prob * prob.log2()
381                } else {
382                    0.0
383                }
384            })
385            .sum()
386    }
387}
388
389impl Default for DNA {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395fn infer_category(pathway: &str) -> String {
396    let low = pathway.to_lowercase();
397    if low.contains("rout") || low.contains("dispatch") {
398        "routing".to_string()
399    } else if low.contains("provider") || low.contains("model") {
400        "provider".to_string()
401    } else if low.contains("room") || low.contains("channel") {
402        "room".to_string()
403    } else if low.contains("tile") || low.contains("block") {
404        "tile".to_string()
405    } else if low.contains("circuit") || low.contains("wire") {
406        "circuit".to_string()
407    } else {
408        "general".to_string()
409    }
410}
411
412// ---------------------------------------------------------------------------
413// ShellProfile
414// ---------------------------------------------------------------------------
415
416/// Accumulated identity and stats for a shell.
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub struct ShellProfile {
419    pub shell_id: String,
420    pub lifecycle: ShellLifecycle,
421    pub dna: DNA,
422    pub born_at: u64,
423    pub total_ticks: u64,
424    pub total_energy_used: f64,
425    pub children_spawned: u64,
426    pub messages_sent: u64,
427    pub messages_received: u64,
428    pub config: ShellConfig,
429}
430
431impl ShellProfile {
432    pub fn new(config: ShellConfig, now: u64) -> Self {
433        let id = config.id.clone();
434        Self {
435            shell_id: id,
436            lifecycle: ShellLifecycle::new(),
437            dna: DNA::new(),
438            born_at: now,
439            total_ticks: 0,
440            total_energy_used: 0.0,
441            children_spawned: 0,
442            messages_sent: 0,
443            messages_received: 0,
444            config,
445        }
446    }
447
448    pub fn pathway_count(&self) -> usize {
449        self.dna.pathways.len()
450    }
451
452    pub fn age_seconds(&self, now: u64) -> u64 {
453        now.saturating_sub(self.born_at)
454    }
455
456    pub fn efficiency(&self) -> f64 {
457        if self.total_energy_used <= 0.0 {
458            return 0.0;
459        }
460        (self.messages_sent + self.messages_received) as f64 / self.total_energy_used
461    }
462
463    /// Adaptation score: DNA diversity × total strength.
464    pub fn adaptation_score(&self) -> f64 {
465        self.dna.diversity() * self.dna.total_strength()
466    }
467}
468
469// ---------------------------------------------------------------------------
470// ShellNursery
471// ---------------------------------------------------------------------------
472
473/// Manages all shell profiles.
474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
475pub struct ShellNursery {
476    pub shells: HashMap<String, ShellProfile>,
477}
478
479impl ShellNursery {
480    pub fn new() -> Self {
481        Self {
482            shells: HashMap::new(),
483        }
484    }
485
486    /// Spawn a new shell from the given config.
487    pub fn spawn(&mut self, config: ShellConfig) -> Result<&mut ShellProfile, LifecycleError> {
488        if self.shells.contains_key(&config.id) {
489            return Err(LifecycleError::AlreadyExists(config.id.clone()));
490        }
491
492        // Check parent constraints if there is a parent
493        if let Some(ref parent_id) = config.parent_id {
494            let parent = self
495                .shells
496                .get(parent_id)
497                .ok_or_else(|| LifecycleError::ParentNotRunning(parent_id.clone()))?;
498
499            if parent.lifecycle.state != ShellState::Running {
500                return Err(LifecycleError::ParentNotRunning(parent_id.clone()));
501            }
502
503            if parent.children_spawned >= parent.config.max_children as u64 {
504                return Err(LifecycleError::MaxChildrenReached(
505                    parent.config.max_children,
506                ));
507            }
508        }
509
510        let id = config.id.clone();
511        let now = 0u64; // Nursery uses tick-based time
512        let mut profile = ShellProfile::new(config, now);
513        profile
514            .lifecycle
515            .transition(LifecycleEvent::Spawn {
516                parent: profile.config.parent_id.clone().unwrap_or_default(),
517                config: profile.config.clone(),
518            })
519            .map_err(|_e| LifecycleError::InvalidTransition {
520                from: ShellState::Conceived,
521                to: ShellState::Spawning,
522            })?;
523
524        // Increment parent's children count
525        if let Some(ref parent_id) = profile.config.parent_id {
526            if let Some(parent) = self.shells.get_mut(parent_id) {
527                parent.children_spawned += 1;
528            }
529        }
530
531        self.shells.insert(id.clone(), profile);
532        Ok(self.shells.get_mut(&id).unwrap())
533    }
534
535    /// Kill a shell by id.
536    pub fn kill(&mut self, id: &str, reason: &str) -> Result<ShellProfile, LifecycleError> {
537        let profile = self
538            .shells
539            .get_mut(id)
540            .ok_or_else(|| LifecycleError::ShellNotFound(id.to_string()))?;
541
542        profile
543            .lifecycle
544            .transition(LifecycleEvent::Kill {
545                reason: reason.to_string(),
546            })
547            .map_err(|_| LifecycleError::InvalidTransition {
548                from: profile.lifecycle.state,
549                to: ShellState::Dying,
550            })?;
551
552        // Now move to Dead
553        profile
554            .lifecycle
555            .transition(LifecycleEvent::Timeout)
556            .map_err(|_| LifecycleError::InvalidTransition {
557                from: profile.lifecycle.state,
558                to: ShellState::Dead,
559            })?;
560
561        Ok(self.shells.remove(id).unwrap())
562    }
563
564    /// Get a shell profile by id.
565    pub fn get(&self, id: &str) -> Option<&ShellProfile> {
566        self.shells.get(id)
567    }
568
569    /// Get a mutable reference to a shell profile by id.
570    pub fn get_mut(&mut self, id: &str) -> Option<&mut ShellProfile> {
571        self.shells.get_mut(id)
572    }
573
574    /// Tick DNA for all running shells.
575    pub fn tick_all(&mut self) {
576        for profile in self.shells.values_mut() {
577            if profile.lifecycle.state == ShellState::Running {
578                profile.dna.tick();
579                profile.total_ticks += 1;
580            }
581        }
582    }
583
584    /// Get all running shell profiles.
585    pub fn running(&self) -> Vec<&ShellProfile> {
586        self.shells
587            .values()
588            .filter(|p| p.lifecycle.state == ShellState::Running)
589            .collect()
590    }
591
592    /// Get all children of a given parent.
593    pub fn children_of(&self, parent_id: &str) -> Vec<&ShellProfile> {
594        self.shells
595            .values()
596            .filter(|p| p.config.parent_id.as_deref() == Some(parent_id))
597            .collect()
598    }
599
600    /// Trace ancestry from a shell to the root.
601    pub fn lineage(&self, id: &str) -> Vec<String> {
602        let mut lineage = Vec::new();
603        let mut current_id = id.to_string();
604        while let Some(profile) = self.shells.get(&current_id) {
605            lineage.push(current_id.clone());
606            match profile.config.parent_id {
607                Some(ref pid) => current_id = pid.clone(),
608                None => break,
609            }
610        }
611        lineage
612    }
613}
614
615// ===========================================================================
616// Tests
617// ===========================================================================
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    fn config(id: &str) -> ShellConfig {
624        ShellConfig {
625            id: id.to_string(),
626            name: format!("shell-{}", id),
627            shell_type: ShellType::Hermes,
628            universe_path: "/universe".to_string(),
629            conservation_budget: 100.0,
630            parent_id: None,
631            max_children: 10,
632            capabilities: vec!["read".to_string(), "write".to_string()],
633            model: None,
634        }
635    }
636
637    fn config_with_parent(id: &str, parent_id: &str) -> ShellConfig {
638        let mut c = config(id);
639        c.parent_id = Some(parent_id.to_string());
640        c
641    }
642
643    // --- ShellState tests ---
644
645    #[test]
646    fn test_state_legal_transitions_conceived() {
647        let s = ShellState::Conceived;
648        assert!(s.legal_transitions().contains(&ShellState::Spawning));
649        assert!(!s.legal_transitions().contains(&ShellState::Running));
650    }
651
652    #[test]
653    fn test_state_legal_transitions_running() {
654        let s = ShellState::Running;
655        assert!(s.legal_transitions().contains(&ShellState::Suspended));
656        assert!(s.legal_transitions().contains(&ShellState::Migrating));
657        assert!(s.legal_transitions().contains(&ShellState::Dying));
658        assert!(!s.legal_transitions().contains(&ShellState::Conceived));
659    }
660
661    #[test]
662    fn test_state_legal_transitions_dead() {
663        let s = ShellState::Dead;
664        assert!(s.legal_transitions().is_empty());
665    }
666
667    #[test]
668    fn test_state_legal_transitions_suspended() {
669        let s = ShellState::Suspended;
670        assert!(s.legal_transitions().contains(&ShellState::Running));
671        assert_eq!(s.legal_transitions().len(), 1);
672    }
673
674    #[test]
675    fn test_state_legal_transitions_migrating() {
676        let s = ShellState::Migrating;
677        assert!(s.legal_transitions().contains(&ShellState::Running));
678        assert_eq!(s.legal_transitions().len(), 1);
679    }
680
681    #[test]
682    fn test_state_legal_transitions_spawning() {
683        let s = ShellState::Spawning;
684        assert!(s.legal_transitions().contains(&ShellState::Bootstrapping));
685        assert_eq!(s.legal_transitions().len(), 1);
686    }
687
688    #[test]
689    fn test_state_legal_transitions_bootstrapping() {
690        let s = ShellState::Bootstrapping;
691        assert!(s.legal_transitions().contains(&ShellState::Running));
692        assert_eq!(s.legal_transitions().len(), 1);
693    }
694
695    #[test]
696    fn test_state_legal_transitions_dying() {
697        let s = ShellState::Dying;
698        assert!(s.legal_transitions().contains(&ShellState::Dead));
699        assert_eq!(s.legal_transitions().len(), 1);
700    }
701
702    // --- ShellLifecycle tests ---
703
704    #[test]
705    fn test_lifecycle_new() {
706        let lc = ShellLifecycle::new();
707        assert_eq!(lc.state, ShellState::Conceived);
708    }
709
710    #[test]
711    fn test_lifecycle_can_transition_to() {
712        let lc = ShellLifecycle::new();
713        assert!(lc.can_transition_to(&ShellState::Spawning));
714        assert!(!lc.can_transition_to(&ShellState::Running));
715    }
716
717    #[test]
718    fn test_lifecycle_full_happy_path() {
719        let mut lc = ShellLifecycle::new();
720        lc.transition(LifecycleEvent::Spawn {
721            parent: "root".to_string(),
722            config: config("test"),
723        })
724        .unwrap();
725        assert_eq!(lc.state, ShellState::Spawning);
726
727        lc.transition(LifecycleEvent::Bootstrap).unwrap();
728        assert_eq!(lc.state, ShellState::Bootstrapping);
729
730        lc.transition(LifecycleEvent::Ready).unwrap();
731        assert_eq!(lc.state, ShellState::Running);
732    }
733
734    #[test]
735    fn test_lifecycle_invalid_transition() {
736        let mut lc = ShellLifecycle::new();
737        let result = lc.transition(LifecycleEvent::Ready);
738        assert!(result.is_err());
739        assert_eq!(
740            result.unwrap_err(),
741            LifecycleError::InvalidTransition {
742                from: ShellState::Conceived,
743                to: ShellState::Running
744            }
745        );
746    }
747
748    #[test]
749    fn test_lifecycle_suspend_resume() {
750        let mut lc = ShellLifecycle::new();
751        lc.transition(LifecycleEvent::Spawn {
752            parent: "root".into(),
753            config: config("x"),
754        })
755        .unwrap();
756        lc.transition(LifecycleEvent::Bootstrap).unwrap();
757        lc.transition(LifecycleEvent::Ready).unwrap();
758
759        lc.transition(LifecycleEvent::Suspend {
760            reason: "maintenance".into(),
761        })
762        .unwrap();
763        assert_eq!(lc.state, ShellState::Suspended);
764        assert_eq!(lc.suspend_reason.as_deref(), Some("maintenance"));
765
766        lc.transition(LifecycleEvent::Resume).unwrap();
767        assert_eq!(lc.state, ShellState::Running);
768    }
769
770    #[test]
771    fn test_lifecycle_migrate() {
772        let mut lc = ShellLifecycle::new();
773        lc.transition(LifecycleEvent::Spawn {
774            parent: "root".into(),
775            config: config("x"),
776        })
777        .unwrap();
778        lc.transition(LifecycleEvent::Bootstrap).unwrap();
779        lc.transition(LifecycleEvent::Ready).unwrap();
780
781        lc.transition(LifecycleEvent::Migrate {
782            target: "gpu-01".into(),
783        })
784        .unwrap();
785        assert_eq!(lc.state, ShellState::Migrating);
786        assert_eq!(lc.migration_target.as_deref(), Some("gpu-01"));
787
788        lc.transition(LifecycleEvent::Ready).unwrap();
789        assert_eq!(lc.state, ShellState::Running);
790    }
791
792    #[test]
793    fn test_lifecycle_kill() {
794        let mut lc = ShellLifecycle::new();
795        lc.transition(LifecycleEvent::Spawn {
796            parent: "root".into(),
797            config: config("x"),
798        })
799        .unwrap();
800        lc.transition(LifecycleEvent::Bootstrap).unwrap();
801        lc.transition(LifecycleEvent::Ready).unwrap();
802
803        lc.transition(LifecycleEvent::Kill {
804            reason: "evicted".into(),
805        })
806        .unwrap();
807        assert_eq!(lc.state, ShellState::Dying);
808        assert_eq!(lc.kill_reason.as_deref(), Some("evicted"));
809
810        // Can't go anywhere but Dead from Dying
811        let err = lc.transition(LifecycleEvent::Resume);
812        assert!(err.is_err());
813
814        // Dying → Dead via Timeout
815        lc.transition(LifecycleEvent::Timeout).unwrap();
816        assert_eq!(lc.state, ShellState::Dead);
817    }
818
819    #[test]
820    fn test_lifecycle_cannot_transition_from_dead() {
821        let mut lc = ShellLifecycle::new();
822        lc.transition(LifecycleEvent::Spawn {
823            parent: "root".into(),
824            config: config("x"),
825        })
826        .unwrap();
827        lc.transition(LifecycleEvent::Bootstrap).unwrap();
828        lc.transition(LifecycleEvent::Ready).unwrap();
829        // Kill from Running → Dying
830        lc.transition(LifecycleEvent::Kill {
831            reason: "done".into(),
832        })
833        .unwrap();
834        // Dying → Dead
835        lc.transition(LifecycleEvent::Timeout).unwrap();
836        assert_eq!(lc.state, ShellState::Dead);
837
838        assert!(lc
839            .transition(LifecycleEvent::Spawn {
840                parent: "root".into(),
841                config: config("y"),
842            })
843            .is_err());
844    }
845
846    // --- ShellConfig tests ---
847
848    #[test]
849    fn test_config_json_roundtrip() {
850        let c = config("abc");
851        let json = c.to_json();
852        let c2 = ShellConfig::from_json(&json).unwrap();
853        assert_eq!(c, c2);
854    }
855
856    #[test]
857    fn test_config_json_invalid() {
858        assert!(ShellConfig::from_json("not json").is_err());
859    }
860
861    #[test]
862    fn test_shell_type_serialization() {
863        let types = vec![
864            ShellType::Hermes,
865            ShellType::ZeroClaw,
866            ShellType::CUDAClaw,
867            ShellType::GitNative,
868            ShellType::Remote {
869                address: "1.2.3.4".into(),
870            },
871            ShellType::Custom("special".into()),
872        ];
873        for t in types {
874            let json = serde_json::to_string(&t).unwrap();
875            let t2: ShellType = serde_json::from_str(&json).unwrap();
876            assert_eq!(t, t2);
877        }
878    }
879
880    // --- Pathway tests ---
881
882    #[test]
883    fn test_pathway_new() {
884        let p = Pathway::new("test-path", "routing");
885        assert_eq!(p.name, "test-path");
886        assert_eq!(p.use_count, 0);
887        assert_eq!(p.strength, 0.0);
888        assert_eq!(p.category, "routing");
889    }
890
891    #[test]
892    fn test_pathway_use_once() {
893        let mut p = Pathway::new("p", "general");
894        p.use_once();
895        assert_eq!(p.use_count, 1);
896        assert!(p.strength > 0.0);
897        // First use: 0.0 + 0.1 * (1.0 - 0.0) = 0.1
898        assert!((p.strength - 0.1).abs() < 1e-10);
899    }
900
901    #[test]
902    fn test_pathway_asymptotic_growth() {
903        let mut p = Pathway::new("p", "general");
904        for _ in 0..1000 {
905            let prev = p.strength;
906            p.use_once();
907            assert!(p.strength >= prev - 1e-15);
908            assert!(p.strength <= 1.0);
909        }
910        // Should be very close to 1.0
911        assert!(p.strength > 0.9999);
912    }
913
914    #[test]
915    fn test_pathway_decay() {
916        let mut p = Pathway::new("p", "general");
917        p.strength = 0.5;
918        p.decay();
919        assert!((p.strength - 0.49).abs() < 1e-10);
920    }
921
922    #[test]
923    fn test_pathway_decay_floor() {
924        let mut p = Pathway::new("p", "general");
925        p.strength = 0.005;
926        p.decay();
927        assert_eq!(p.strength, 0.0);
928    }
929
930    // --- DNA tests ---
931
932    #[test]
933    fn test_dna_new() {
934        let dna = DNA::new();
935        assert!(dna.pathways.is_empty());
936        assert_eq!(dna.tick_count, 0);
937    }
938
939    #[test]
940    fn test_dna_record_use_creates_pathway() {
941        let mut dna = DNA::new();
942        dna.record_use("routing/main");
943        assert_eq!(dna.pathways.len(), 1);
944        assert_eq!(dna.pathways["routing/main"].use_count, 1);
945    }
946
947    #[test]
948    fn test_dna_record_use_increments() {
949        let mut dna = DNA::new();
950        dna.record_use("routing/main");
951        dna.record_use("routing/main");
952        dna.record_use("routing/main");
953        assert_eq!(dna.pathways["routing/main"].use_count, 3);
954    }
955
956    #[test]
957    fn test_dna_tick_decay_and_prune() {
958        let mut dna = DNA::new();
959        dna.record_use("weak");
960        // After one use: strength ≈ 0.1
961        // One tick: 0.1 - 0.01 = 0.09
962        // Many ticks should prune it
963        for _ in 0..20 {
964            dna.tick();
965        }
966        assert!(!dna.pathways.contains_key("weak"));
967    }
968
969    #[test]
970    fn test_dna_tick_preserves_strong() {
971        let mut dna = DNA::new();
972        // Use a pathway many times
973        for _ in 0..50 {
974            dna.record_use("strong");
975        }
976        // A few ticks shouldn't prune it
977        for _ in 0..5 {
978            dna.tick();
979        }
980        assert!(dna.pathways.contains_key("strong"));
981    }
982
983    #[test]
984    fn test_dna_tick_count_increments() {
985        let mut dna = DNA::new();
986        dna.tick();
987        dna.tick();
988        dna.tick();
989        assert_eq!(dna.tick_count, 3);
990    }
991
992    #[test]
993    fn test_dna_strongest() {
994        let mut dna = DNA::new();
995        dna.record_use("b");
996        dna.record_use("a");
997        dna.record_use("a");
998        let top = dna.strongest(2);
999        assert_eq!(top.len(), 2);
1000        assert_eq!(top[0].0, "a");
1001        assert_eq!(top[1].0, "b");
1002    }
1003
1004    #[test]
1005    fn test_dna_strongest_limited() {
1006        let mut dna = DNA::new();
1007        dna.record_use("x");
1008        dna.record_use("y");
1009        dna.record_use("z");
1010        let top = dna.strongest(1);
1011        assert_eq!(top.len(), 1);
1012    }
1013
1014    #[test]
1015    fn test_dna_total_strength() {
1016        let mut dna = DNA::new();
1017        dna.record_use("a");
1018        dna.record_use("b");
1019        dna.record_use("b");
1020        let total = dna.total_strength();
1021        // a: 0.1, b: 0.1 + 0.1*(1-0.1) = 0.1 + 0.09 = 0.19
1022        assert!((total - 0.29).abs() < 1e-10);
1023    }
1024
1025    #[test]
1026    fn test_dna_diversity_empty() {
1027        let dna = DNA::new();
1028        assert_eq!(dna.diversity(), 0.0);
1029    }
1030
1031    #[test]
1032    fn test_dna_diversity_single() {
1033        let mut dna = DNA::new();
1034        dna.record_use("only");
1035        // Single pathway: probability = 1.0, entropy = -1.0 * log2(1.0) = 0.0
1036        assert_eq!(dna.diversity(), 0.0);
1037    }
1038
1039    #[test]
1040    fn test_dna_diversity_balanced() {
1041        let mut dna = DNA::new();
1042        dna.record_use("a");
1043        dna.record_use("b");
1044        // Both have same strength → maximum entropy for 2 items = 1.0
1045        let d = dna.diversity();
1046        assert!((d - 1.0).abs() < 1e-10);
1047    }
1048
1049    #[test]
1050    fn test_dna_category_inference_routing() {
1051        let mut dna = DNA::new();
1052        dna.record_use("route-dispatch");
1053        assert_eq!(dna.pathways["route-dispatch"].category, "routing");
1054    }
1055
1056    #[test]
1057    fn test_dna_category_inference_provider() {
1058        let mut dna = DNA::new();
1059        dna.record_use("model-provider");
1060        assert_eq!(dna.pathways["model-provider"].category, "provider");
1061    }
1062
1063    #[test]
1064    fn test_dna_category_inference_room() {
1065        let mut dna = DNA::new();
1066        dna.record_use("room-lobby");
1067        assert_eq!(dna.pathways["room-lobby"].category, "room");
1068    }
1069
1070    #[test]
1071    fn test_dna_category_inference_tile() {
1072        let mut dna = DNA::new();
1073        dna.record_use("tile-render");
1074        assert_eq!(dna.pathways["tile-render"].category, "tile");
1075    }
1076
1077    #[test]
1078    fn test_dna_category_inference_circuit() {
1079        let mut dna = DNA::new();
1080        dna.record_use("circuit-board");
1081        assert_eq!(dna.pathways["circuit-board"].category, "circuit");
1082    }
1083
1084    #[test]
1085    fn test_dna_category_inference_general() {
1086        let mut dna = DNA::new();
1087        dna.record_use("something-random");
1088        assert_eq!(dna.pathways["something-random"].category, "general");
1089    }
1090
1091    // --- ShellProfile tests ---
1092
1093    #[test]
1094    fn test_profile_new() {
1095        let c = config("p1");
1096        let p = ShellProfile::new(c, 100);
1097        assert_eq!(p.shell_id, "p1");
1098        assert_eq!(p.born_at, 100);
1099        assert_eq!(p.total_ticks, 0);
1100    }
1101
1102    #[test]
1103    fn test_profile_pathway_count() {
1104        let c = config("p1");
1105        let mut p = ShellProfile::new(c, 0);
1106        assert_eq!(p.pathway_count(), 0);
1107        p.dna.record_use("a");
1108        assert_eq!(p.pathway_count(), 1);
1109        p.dna.record_use("b");
1110        assert_eq!(p.pathway_count(), 2);
1111    }
1112
1113    #[test]
1114    fn test_profile_age_seconds() {
1115        let c = config("p1");
1116        let p = ShellProfile::new(c, 100);
1117        assert_eq!(p.age_seconds(200), 100);
1118        assert_eq!(p.age_seconds(50), 0); // saturating_sub
1119    }
1120
1121    #[test]
1122    fn test_profile_efficiency_zero_energy() {
1123        let c = config("p1");
1124        let p = ShellProfile::new(c, 0);
1125        assert_eq!(p.efficiency(), 0.0);
1126    }
1127
1128    #[test]
1129    fn test_profile_efficiency() {
1130        let c = config("p1");
1131        let mut p = ShellProfile::new(c, 0);
1132        p.messages_sent = 10;
1133        p.messages_received = 20;
1134        p.total_energy_used = 5.0;
1135        assert!((p.efficiency() - 6.0).abs() < 1e-10);
1136    }
1137
1138    #[test]
1139    fn test_profile_adaptation_score() {
1140        let c = config("p1");
1141        let mut p = ShellProfile::new(c, 0);
1142        p.dna.record_use("a");
1143        p.dna.record_use("b");
1144        let score = p.adaptation_score();
1145        assert!(score > 0.0);
1146    }
1147
1148    // --- ShellNursery tests ---
1149
1150    #[test]
1151    fn test_nursery_new() {
1152        let n = ShellNursery::new();
1153        assert!(n.shells.is_empty());
1154    }
1155
1156    #[test]
1157    fn test_nursery_spawn() {
1158        let mut n = ShellNursery::new();
1159        let profile = n.spawn(config("s1")).unwrap();
1160        assert_eq!(profile.shell_id, "s1");
1161        assert_eq!(profile.lifecycle.state, ShellState::Spawning);
1162    }
1163
1164    #[test]
1165    fn test_nursery_spawn_duplicate() {
1166        let mut n = ShellNursery::new();
1167        n.spawn(config("s1")).unwrap();
1168        let err = n.spawn(config("s1")).unwrap_err();
1169        assert_eq!(err, LifecycleError::AlreadyExists("s1".into()));
1170    }
1171
1172    #[test]
1173    fn test_nursery_spawn_with_parent() {
1174        let mut n = ShellNursery::new();
1175        let parent = n.spawn(config("parent")).unwrap();
1176        // Advance parent to Running
1177        parent.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1178        parent.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1179
1180        let child = n.spawn(config_with_parent("child", "parent")).unwrap();
1181        assert_eq!(child.config.parent_id, Some("parent".into()));
1182        assert_eq!(n.get("parent").unwrap().children_spawned, 1);
1183    }
1184
1185    #[test]
1186    fn test_nursery_spawn_parent_not_found() {
1187        let mut n = ShellNursery::new();
1188        let err = n
1189            .spawn(config_with_parent("child", "nonexistent"))
1190            .unwrap_err();
1191        assert_eq!(err, LifecycleError::ParentNotRunning("nonexistent".into()));
1192    }
1193
1194    #[test]
1195    fn test_nursery_spawn_parent_not_running() {
1196        let mut n = ShellNursery::new();
1197        n.spawn(config("parent")).unwrap();
1198        // Parent is still in Spawning state
1199        let err = n
1200            .spawn(config_with_parent("child", "parent"))
1201            .unwrap_err();
1202        assert_eq!(err, LifecycleError::ParentNotRunning("parent".into()));
1203    }
1204
1205    #[test]
1206    fn test_nursery_spawn_max_children() {
1207        let mut n = ShellNursery::new();
1208        let mut small_config = config("parent");
1209        small_config.max_children = 1;
1210        let parent = n.spawn(small_config).unwrap();
1211        parent.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1212        parent.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1213
1214        n.spawn(config_with_parent("child1", "parent")).unwrap();
1215        let err = n
1216            .spawn(config_with_parent("child2", "parent"))
1217            .unwrap_err();
1218        assert_eq!(err, LifecycleError::MaxChildrenReached(1));
1219    }
1220
1221    #[test]
1222    fn test_nursery_kill() {
1223        let mut n = ShellNursery::new();
1224        let s1 = n.spawn(config("s1")).unwrap();
1225        s1.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1226        s1.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1227        let killed = n.kill("s1", "test").unwrap();
1228        assert_eq!(killed.lifecycle.state, ShellState::Dead);
1229        assert!(n.get("s1").is_none());
1230    }
1231
1232    #[test]
1233    fn test_nursery_kill_not_found() {
1234        let mut n = ShellNursery::new();
1235        let err = n.kill("ghost", "test").unwrap_err();
1236        assert_eq!(err, LifecycleError::ShellNotFound("ghost".into()));
1237    }
1238
1239    #[test]
1240    fn test_nursery_get() {
1241        let mut n = ShellNursery::new();
1242        n.spawn(config("s1")).unwrap();
1243        assert!(n.get("s1").is_some());
1244        assert!(n.get("s2").is_none());
1245    }
1246
1247    #[test]
1248    fn test_nursery_tick_all() {
1249        let mut n = ShellNursery::new();
1250        let s1 = n.spawn(config("s1")).unwrap();
1251        s1.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1252        s1.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1253        s1.dna.record_use("test-path");
1254
1255        n.tick_all();
1256        assert_eq!(n.get("s1").unwrap().total_ticks, 1);
1257        assert_eq!(n.get("s1").unwrap().dna.tick_count, 1);
1258    }
1259
1260    #[test]
1261    fn test_nursery_tick_all_skips_non_running() {
1262        let mut n = ShellNursery::new();
1263        n.spawn(config("s1")).unwrap();
1264        // s1 is in Spawning state, not Running
1265        n.tick_all();
1266        assert_eq!(n.get("s1").unwrap().total_ticks, 0);
1267    }
1268
1269    #[test]
1270    fn test_nursery_running() {
1271        let mut n = ShellNursery::new();
1272        let s1 = n.spawn(config("s1")).unwrap();
1273        s1.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1274        s1.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1275        n.spawn(config("s2")).unwrap(); // still Spawning
1276
1277        let running = n.running();
1278        assert_eq!(running.len(), 1);
1279        assert_eq!(running[0].shell_id, "s1");
1280    }
1281
1282    #[test]
1283    fn test_nursery_children_of() {
1284        let mut n = ShellNursery::new();
1285        let parent = n.spawn(config("parent")).unwrap();
1286        parent.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1287        parent.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1288
1289        n.spawn(config_with_parent("c1", "parent")).unwrap();
1290        n.spawn(config_with_parent("c2", "parent")).unwrap();
1291        n.spawn(config("orphan")).unwrap();
1292
1293        let children = n.children_of("parent");
1294        assert_eq!(children.len(), 2);
1295    }
1296
1297    #[test]
1298    fn test_nursery_children_of_none() {
1299        let n = ShellNursery::new();
1300        assert!(n.children_of("nobody").is_empty());
1301    }
1302
1303    #[test]
1304    fn test_nursery_lineage() {
1305        let mut n = ShellNursery::new();
1306        let gp = n.spawn(config("grandparent")).unwrap();
1307        gp.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1308        gp.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1309
1310        let _parent_config = config_with_parent("parent", "grandparent");
1311        // Need to make sure parent can be spawned
1312        let parent = n.spawn(config_with_parent("parent", "grandparent")).unwrap();
1313        parent.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1314        parent.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1315
1316        n.spawn(config_with_parent("child", "parent")).unwrap();
1317
1318        let lineage = n.lineage("child");
1319        assert_eq!(lineage, vec!["child", "parent", "grandparent"]);
1320    }
1321
1322    #[test]
1323    fn test_nursery_lineage_not_found() {
1324        let n = ShellNursery::new();
1325        let lineage = n.lineage("ghost");
1326        assert!(lineage.is_empty());
1327    }
1328
1329    // --- Serialization roundtrip tests ---
1330
1331    #[test]
1332    fn test_lifecycle_serialize_roundtrip() {
1333        let lc = ShellLifecycle::new();
1334        let json = serde_json::to_string(&lc).unwrap();
1335        let lc2: ShellLifecycle = serde_json::from_str(&json).unwrap();
1336        assert_eq!(lc, lc2);
1337    }
1338
1339    #[test]
1340    fn test_dna_serialize_roundtrip() {
1341        let mut dna = DNA::new();
1342        dna.record_use("a");
1343        dna.record_use("b");
1344        let json = serde_json::to_string(&dna).unwrap();
1345        let dna2: DNA = serde_json::from_str(&json).unwrap();
1346        assert_eq!(dna, dna2);
1347    }
1348
1349    #[test]
1350    fn test_profile_serialize_roundtrip() {
1351        let p = ShellProfile::new(config("test"), 42);
1352        let json = serde_json::to_string(&p).unwrap();
1353        let p2: ShellProfile = serde_json::from_str(&json).unwrap();
1354        assert_eq!(p, p2);
1355    }
1356
1357    #[test]
1358    fn test_nursery_serialize_roundtrip() {
1359        let mut n = ShellNursery::new();
1360        n.spawn(config("s1")).unwrap();
1361        let json = serde_json::to_string(&n).unwrap();
1362        let n2: ShellNursery = serde_json::from_str(&json).unwrap();
1363        assert_eq!(n, n2);
1364    }
1365
1366    #[test]
1367    fn test_error_display() {
1368        let e = LifecycleError::InvalidTransition {
1369            from: ShellState::Running,
1370            to: ShellState::Conceived,
1371        };
1372        assert!(e.to_string().contains("Running"));
1373        assert!(e.to_string().contains("Conceived"));
1374
1375        let e = LifecycleError::ShellNotFound("abc".into());
1376        assert!(e.to_string().contains("abc"));
1377
1378        let e = LifecycleError::AlreadyExists("x".into());
1379        assert!(e.to_string().contains("x"));
1380
1381        let e = LifecycleError::MaxChildrenReached(5);
1382        assert!(e.to_string().contains("5"));
1383
1384        let e = LifecycleError::ParentNotRunning("p".into());
1385        assert!(e.to_string().contains("p"));
1386    }
1387
1388    #[test]
1389    fn test_nursery_default() {
1390        let n = ShellNursery::default();
1391        assert!(n.shells.is_empty());
1392    }
1393
1394    #[test]
1395    fn test_lifecycle_default() {
1396        let lc = ShellLifecycle::default();
1397        assert_eq!(lc.state, ShellState::Conceived);
1398    }
1399
1400    #[test]
1401    fn test_dna_default() {
1402        let dna = DNA::default();
1403        assert!(dna.pathways.is_empty());
1404    }
1405
1406    // --- Edge case / stress tests ---
1407
1408    #[test]
1409    fn test_pathway_many_uses() {
1410        let mut p = Pathway::new("stress", "general");
1411        for _ in 0..100_000 {
1412            p.use_once();
1413        }
1414        assert!(p.strength < 1.0);
1415        assert!(p.strength > 0.9999);
1416        assert_eq!(p.use_count, 100_000);
1417    }
1418
1419    #[test]
1420    fn test_dna_many_pathways() {
1421        let mut dna = DNA::new();
1422        for i in 0..100 {
1423            dna.record_use(&format!("path-{}", i));
1424        }
1425        assert_eq!(dna.pathways.len(), 100);
1426        assert!(dna.total_strength() > 0.0);
1427        assert!(dna.diversity() > 0.0);
1428    }
1429
1430    #[test]
1431    fn test_dna_strongest_empty() {
1432        let dna = DNA::new();
1433        assert!(dna.strongest(5).is_empty());
1434    }
1435
1436    #[test]
1437    fn test_dna_total_strength_empty() {
1438        let dna = DNA::new();
1439        assert_eq!(dna.total_strength(), 0.0);
1440    }
1441
1442    #[test]
1443    fn test_nursery_multiple_children_different_parents() {
1444        let mut n = ShellNursery::new();
1445        let p1 = n.spawn(config("p1")).unwrap();
1446        p1.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1447        p1.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1448
1449        let p2 = n.spawn(config("p2")).unwrap();
1450        p2.lifecycle.transition(LifecycleEvent::Bootstrap).unwrap();
1451        p2.lifecycle.transition(LifecycleEvent::Ready).unwrap();
1452
1453        n.spawn(config_with_parent("c1", "p1")).unwrap();
1454        n.spawn(config_with_parent("c2", "p1")).unwrap();
1455        n.spawn(config_with_parent("c3", "p2")).unwrap();
1456
1457        assert_eq!(n.children_of("p1").len(), 2);
1458        assert_eq!(n.children_of("p2").len(), 1);
1459    }
1460}