1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[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#[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#[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 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#[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#[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#[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 pub fn transition(&mut self, event: LifecycleEvent) -> Result<(), LifecycleError> {
195 let target = Self::target_state_for_event(&event);
196
197 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 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 } else if !self.can_transition_to(&target) {
221 return Err(LifecycleError::InvalidTransition {
222 from: self.state,
223 to: target,
224 });
225 }
226
227 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#[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 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 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#[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 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 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 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 pub fn total_strength(&self) -> f64 {
363 self.pathways.values().map(|p| p.strength).sum()
364 }
365
366 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#[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 pub fn adaptation_score(&self) -> f64 {
465 self.dna.diversity() * self.dna.total_strength()
466 }
467}
468
469#[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 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 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; 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 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 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 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 pub fn get(&self, id: &str) -> Option<&ShellProfile> {
566 self.shells.get(id)
567 }
568
569 pub fn get_mut(&mut self, id: &str) -> Option<&mut ShellProfile> {
571 self.shells.get_mut(id)
572 }
573
574 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 pub fn running(&self) -> Vec<&ShellProfile> {
586 self.shells
587 .values()
588 .filter(|p| p.lifecycle.state == ShellState::Running)
589 .collect()
590 }
591
592 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 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(¤t_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#[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 #[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 #[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 let err = lc.transition(LifecycleEvent::Resume);
812 assert!(err.is_err());
813
814 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 lc.transition(LifecycleEvent::Kill {
831 reason: "done".into(),
832 })
833 .unwrap();
834 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 #[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 #[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 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 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 #[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 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 for _ in 0..50 {
974 dna.record_use("strong");
975 }
976 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 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 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 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 #[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); }
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 #[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 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 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 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(); 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 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 #[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 #[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}