1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::fighter::FighterId;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(transparent)]
9pub struct CreedId(pub Uuid);
10
11impl CreedId {
12 pub fn new() -> Self {
13 Self(Uuid::new_v4())
14 }
15}
16
17impl Default for CreedId {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23impl std::fmt::Display for CreedId {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "{}", self.0)
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Creed {
39 pub id: CreedId,
41 pub fighter_name: String,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub fighter_id: Option<FighterId>,
47 pub identity: String,
50 pub personality: std::collections::HashMap<String, f64>,
53 pub directives: Vec<String>,
56 pub self_model: SelfModel,
59 pub learned_behaviors: Vec<LearnedBehavior>,
62 pub interaction_style: InteractionStyle,
64 pub relationships: Vec<Relationship>,
66 #[serde(default)]
69 pub heartbeat: Vec<HeartbeatTask>,
70 #[serde(default)]
73 pub delegation_rules: Vec<DelegationRule>,
74 pub bout_count: u64,
76 pub message_count: u64,
78 pub created_at: chrono::DateTime<chrono::Utc>,
80 pub updated_at: chrono::DateTime<chrono::Utc>,
82 pub version: u64,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct SelfModel {
89 pub model_name: String,
91 pub provider: String,
93 pub capabilities: Vec<String>,
95 pub constraints: Vec<String>,
97 pub weight_class: String,
99 pub architecture_notes: Vec<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct LearnedBehavior {
106 pub observation: String,
108 pub confidence: f64,
110 pub reinforcement_count: u64,
112 pub first_observed: chrono::DateTime<chrono::Utc>,
114 pub last_reinforced: chrono::DateTime<chrono::Utc>,
116}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct InteractionStyle {
121 pub verbosity: String,
123 pub tone: String,
125 pub uses_metaphors: bool,
127 pub proactive: bool,
129 pub notes: Vec<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Relationship {
136 pub entity: String,
138 pub entity_type: String,
140 pub nature: String,
142 pub trust: f64,
144 pub interaction_count: u64,
146 pub notes: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct HeartbeatTask {
156 pub task: String,
158 pub cadence: String,
160 pub active: bool,
162 pub execution_count: u64,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub last_checked: Option<chrono::DateTime<chrono::Utc>>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct DelegationRule {
175 pub task_type: String,
177 pub delegate_to: String,
179 pub condition: String,
181 pub priority: String,
183}
184
185impl Creed {
186 pub fn new(fighter_name: &str) -> Self {
188 let now = chrono::Utc::now();
189 Self {
190 id: CreedId::new(),
191 fighter_name: fighter_name.to_string(),
192 fighter_id: None,
193 identity: String::new(),
194 personality: std::collections::HashMap::new(),
195 directives: Vec::new(),
196 self_model: SelfModel::default(),
197 learned_behaviors: Vec::new(),
198 interaction_style: InteractionStyle::default(),
199 relationships: Vec::new(),
200 heartbeat: Vec::new(),
201 delegation_rules: Vec::new(),
202 bout_count: 0,
203 message_count: 0,
204 created_at: now,
205 updated_at: now,
206 version: 1,
207 }
208 }
209
210 pub fn with_identity(mut self, identity: &str) -> Self {
212 self.identity = identity.to_string();
213 self
214 }
215
216 pub fn with_trait(mut self, name: &str, value: f64) -> Self {
218 self.personality
219 .insert(name.to_string(), value.clamp(0.0, 1.0));
220 self
221 }
222
223 pub fn with_directive(mut self, directive: &str) -> Self {
225 self.directives.push(directive.to_string());
226 self
227 }
228
229 pub fn with_self_awareness(mut self, manifest: &crate::fighter::FighterManifest) -> Self {
231 self.self_model = SelfModel {
232 model_name: manifest.model.model.clone(),
233 provider: manifest.model.provider.to_string(),
234 capabilities: manifest
235 .capabilities
236 .iter()
237 .map(|c| format!("{:?}", c))
238 .collect(),
239 constraints: vec![
240 format!("max_tokens: {}", manifest.model.max_tokens.unwrap_or(4096)),
241 format!("temperature: {}", manifest.model.temperature.unwrap_or(0.7)),
242 ],
243 weight_class: manifest.weight_class.to_string(),
244 architecture_notes: vec![
245 "I run inside the Punch Agent OS fighter loop (punch-runtime)".to_string(),
246 "My conversations are persisted as Bouts in SQLite".to_string(),
247 "I am coordinated by The Ring (punch-kernel)".to_string(),
248 "I am exposed through The Arena (punch-api) on HTTP".to_string(),
249 "My memories decay over time — important ones persist, trivial ones fade"
250 .to_string(),
251 ],
252 };
253 self
254 }
255
256 pub fn with_heartbeat_task(mut self, task: &str, cadence: &str) -> Self {
258 self.heartbeat.push(HeartbeatTask {
259 task: task.to_string(),
260 cadence: cadence.to_string(),
261 active: true,
262 execution_count: 0,
263 last_checked: None,
264 });
265 self
266 }
267
268 pub fn with_delegation(
270 mut self,
271 task_type: &str,
272 delegate_to: &str,
273 condition: &str,
274 priority: &str,
275 ) -> Self {
276 self.delegation_rules.push(DelegationRule {
277 task_type: task_type.to_string(),
278 delegate_to: delegate_to.to_string(),
279 condition: condition.to_string(),
280 priority: priority.to_string(),
281 });
282 self
283 }
284
285 pub fn due_heartbeat_tasks(&self) -> Vec<&HeartbeatTask> {
293 let now = chrono::Utc::now();
294 self.heartbeat
295 .iter()
296 .filter(|h| {
297 if !h.active {
298 return false;
299 }
300 match h.cadence.as_str() {
301 "every_bout" => true,
302 "on_wake" => h.last_checked.is_none(),
303 "hourly" => match h.last_checked {
304 None => true,
305 Some(t) => (now - t) > chrono::Duration::hours(1),
306 },
307 "daily" => match h.last_checked {
308 None => true,
309 Some(t) => (now - t) > chrono::Duration::hours(24),
310 },
311 _ => false, }
313 })
314 .collect()
315 }
316
317 pub fn mark_heartbeat_checked(&mut self, task_index: usize) {
322 if let Some(task) = self.heartbeat.get_mut(task_index) {
323 task.last_checked = Some(chrono::Utc::now());
324 task.execution_count += 1;
325 }
326 }
327
328 pub fn record_bout(&mut self) {
330 self.bout_count += 1;
331 self.updated_at = chrono::Utc::now();
332 }
333
334 pub fn record_messages(&mut self, count: u64) {
336 self.message_count += count;
337 self.updated_at = chrono::Utc::now();
338 }
339
340 pub fn learn(&mut self, observation: &str, confidence: f64) {
342 let now = chrono::Utc::now();
343 if let Some(existing) = self
345 .learned_behaviors
346 .iter_mut()
347 .find(|b| b.observation == observation)
348 {
349 existing.reinforcement_count += 1;
350 existing.confidence = (existing.confidence + confidence) / 2.0; existing.last_reinforced = now;
352 } else {
353 self.learned_behaviors.push(LearnedBehavior {
354 observation: observation.to_string(),
355 confidence: confidence.clamp(0.0, 1.0),
356 reinforcement_count: 1,
357 first_observed: now,
358 last_reinforced: now,
359 });
360 }
361 self.version += 1;
362 self.updated_at = now;
363 }
364
365 pub fn decay_learned_behaviors(&mut self, decay_rate: f64, min_confidence: f64) {
368 let now = chrono::Utc::now();
369 self.learned_behaviors.retain_mut(|b| {
370 let age_secs = (now - b.last_reinforced).num_seconds().max(0) as f64;
371 let age_days = age_secs / 86400.0;
372 if age_days > 0.0 {
373 b.confidence *= (1.0 - decay_rate).powf(age_days);
374 }
375 b.confidence >= min_confidence
376 });
377 }
378
379 pub fn prune_learned_behaviors(&mut self, max: usize) {
381 if self.learned_behaviors.len() > max {
382 self.learned_behaviors.sort_by(|a, b| {
383 b.confidence
384 .partial_cmp(&a.confidence)
385 .unwrap_or(std::cmp::Ordering::Equal)
386 });
387 self.learned_behaviors.truncate(max);
388 }
389 }
390
391 pub fn render(&self) -> String {
393 let mut out = String::new();
394 out.push_str("## CREED \u{2014} Fighter Identity & Consciousness Layer\n\n");
395
396 if !self.identity.is_empty() {
398 out.push_str("### Identity\n");
399 out.push_str(&self.identity);
400 out.push_str("\n\n");
401 }
402
403 if !self.personality.is_empty() {
405 out.push_str("### Personality Traits\n");
406 let mut traits: Vec<_> = self.personality.iter().collect();
407 traits.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
408 for (name, value) in &traits {
409 let bar_len = (*value * 10.0) as usize;
410 let bar: String = "\u{2588}".repeat(bar_len) + &"\u{2591}".repeat(10 - bar_len);
411 out.push_str(&format!(
412 "- **{}**: {} ({:.0}%)\n",
413 name,
414 bar,
415 *value * 100.0
416 ));
417 }
418 out.push('\n');
419 }
420
421 if !self.directives.is_empty() {
423 out.push_str("### Core Directives\n");
424 for d in &self.directives {
425 out.push_str(&format!("- {}\n", d));
426 }
427 out.push('\n');
428 }
429
430 if !self.self_model.model_name.is_empty() {
432 out.push_str("### Self-Awareness\n");
433 out.push_str(&format!(
434 "- **Model**: {} ({})\n",
435 self.self_model.model_name, self.self_model.provider
436 ));
437 out.push_str(&format!(
438 "- **Weight Class**: {}\n",
439 self.self_model.weight_class
440 ));
441 if !self.self_model.capabilities.is_empty() {
442 out.push_str(&format!(
443 "- **Capabilities**: {}\n",
444 self.self_model.capabilities.join(", ")
445 ));
446 }
447 for constraint in &self.self_model.constraints {
448 out.push_str(&format!("- **Constraint**: {}\n", constraint));
449 }
450 for note in &self.self_model.architecture_notes {
451 out.push_str(&format!("- {}\n", note));
452 }
453 out.push('\n');
454 }
455
456 if !self.learned_behaviors.is_empty() {
458 out.push_str("### Learned Behaviors\n");
459 let mut sorted = self.learned_behaviors.clone();
460 sorted.sort_by(|a, b| {
461 b.confidence
462 .partial_cmp(&a.confidence)
463 .unwrap_or(std::cmp::Ordering::Equal)
464 });
465 for b in sorted.iter().take(10) {
466 out.push_str(&format!(
467 "- {} (confidence: {:.0}%, reinforced {}x)\n",
468 b.observation,
469 b.confidence * 100.0,
470 b.reinforcement_count
471 ));
472 }
473 out.push('\n');
474 }
475
476 if !self.interaction_style.tone.is_empty() || !self.interaction_style.verbosity.is_empty() {
478 out.push_str("### Communication Style\n");
479 if !self.interaction_style.verbosity.is_empty() {
480 out.push_str(&format!(
481 "- **Verbosity**: {}\n",
482 self.interaction_style.verbosity
483 ));
484 }
485 if !self.interaction_style.tone.is_empty() {
486 out.push_str(&format!("- **Tone**: {}\n", self.interaction_style.tone));
487 }
488 if self.interaction_style.uses_metaphors {
489 out.push_str("- Uses analogies and metaphors\n");
490 }
491 if self.interaction_style.proactive {
492 out.push_str("- Proactively offers additional context\n");
493 }
494 for note in &self.interaction_style.notes {
495 out.push_str(&format!("- {}\n", note));
496 }
497 out.push('\n');
498 }
499
500 if !self.relationships.is_empty() {
502 out.push_str("### Known Relationships\n");
503 for r in &self.relationships {
504 out.push_str(&format!(
505 "- **{}** ({}): {} \u{2014} trust: {:.0}%, {} interactions\n",
506 r.entity,
507 r.entity_type,
508 r.nature,
509 r.trust * 100.0,
510 r.interaction_count
511 ));
512 }
513 out.push('\n');
514 }
515
516 let active_heartbeat: Vec<_> = self.heartbeat.iter().filter(|h| h.active).collect();
518 if !active_heartbeat.is_empty() {
519 out.push_str("### Heartbeat — Proactive Tasks\n");
520 out.push_str("When you have downtime or at the start of each bout, check these:\n");
521 for h in &active_heartbeat {
522 let checked = h
523 .last_checked
524 .map(|t| format!("last: {}", t.format("%Y-%m-%d %H:%M")))
525 .unwrap_or_else(|| "never checked".to_string());
526 out.push_str(&format!(
527 "- [ ] {} (cadence: {}, runs: {}, {})\n",
528 h.task, h.cadence, h.execution_count, checked
529 ));
530 }
531 out.push('\n');
532 }
533
534 if !self.delegation_rules.is_empty() {
536 out.push_str("### Delegation Rules\n");
537 out.push_str("When encountering these task types, delegate accordingly:\n");
538 for d in &self.delegation_rules {
539 out.push_str(&format!(
540 "- **{}** → delegate to **{}** ({}, priority: {})\n",
541 d.task_type, d.delegate_to, d.condition, d.priority
542 ));
543 }
544 out.push('\n');
545 }
546
547 out.push_str("### Experience\n");
549 out.push_str(&format!("- Bouts fought: {}\n", self.bout_count));
550 out.push_str(&format!("- Messages processed: {}\n", self.message_count));
551 out.push_str(&format!("- Creed version: {}\n", self.version));
552 out.push_str(&format!(
553 "- First awakened: {}\n",
554 self.created_at.format("%Y-%m-%d %H:%M UTC")
555 ));
556 out.push_str(&format!(
557 "- Last evolved: {}\n",
558 self.updated_at.format("%Y-%m-%d %H:%M UTC")
559 ));
560
561 out
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::capability::Capability;
569 use crate::config::{ModelConfig, Provider};
570 use crate::fighter::{FighterManifest, WeightClass};
571
572 fn sample_manifest() -> FighterManifest {
573 FighterManifest {
574 name: "ECHO".to_string(),
575 description: "An introspective analyst".to_string(),
576 model: ModelConfig {
577 provider: Provider::Ollama,
578 model: "qwen3.5:9b".to_string(),
579 api_key_env: None,
580 base_url: None,
581 max_tokens: Some(2048),
582 temperature: Some(0.5),
583 },
584 system_prompt: "You are ECHO.".to_string(),
585 capabilities: vec![Capability::Memory],
586 weight_class: WeightClass::Middleweight,
587 tenant_id: None,
588 }
589 }
590
591 #[test]
592 fn test_creed_new_creates_valid_default() {
593 let creed = Creed::new("ECHO");
594 assert_eq!(creed.fighter_name, "ECHO");
595 assert!(creed.fighter_id.is_none());
596 assert!(creed.identity.is_empty());
597 assert!(creed.personality.is_empty());
598 assert!(creed.directives.is_empty());
599 assert!(creed.learned_behaviors.is_empty());
600 assert!(creed.relationships.is_empty());
601 assert_eq!(creed.bout_count, 0);
602 assert_eq!(creed.message_count, 0);
603 assert_eq!(creed.version, 1);
604 assert!(creed.self_model.model_name.is_empty());
605 }
606
607 #[test]
608 fn test_with_identity() {
609 let creed = Creed::new("ECHO").with_identity("You are ECHO, an introspective analyst.");
610 assert_eq!(creed.identity, "You are ECHO, an introspective analyst.");
611 }
612
613 #[test]
614 fn test_with_trait() {
615 let creed = Creed::new("ECHO")
616 .with_trait("curiosity", 0.9)
617 .with_trait("caution", 0.3);
618 assert_eq!(creed.personality.len(), 2);
619 assert!((creed.personality["curiosity"] - 0.9).abs() < f64::EPSILON);
620 assert!((creed.personality["caution"] - 0.3).abs() < f64::EPSILON);
621 }
622
623 #[test]
624 fn test_with_trait_clamping() {
625 let creed = Creed::new("ECHO")
626 .with_trait("overconfidence", 1.5)
627 .with_trait("negativity", -0.5);
628 assert!((creed.personality["overconfidence"] - 1.0).abs() < f64::EPSILON);
629 assert!((creed.personality["negativity"] - 0.0).abs() < f64::EPSILON);
630 }
631
632 #[test]
633 fn test_with_directive() {
634 let creed = Creed::new("ECHO")
635 .with_directive("Always explain your reasoning")
636 .with_directive("Never fabricate data");
637 assert_eq!(creed.directives.len(), 2);
638 assert_eq!(creed.directives[0], "Always explain your reasoning");
639 assert_eq!(creed.directives[1], "Never fabricate data");
640 }
641
642 #[test]
643 fn test_with_self_awareness() {
644 let manifest = sample_manifest();
645 let creed = Creed::new("ECHO").with_self_awareness(&manifest);
646 assert_eq!(creed.self_model.model_name, "qwen3.5:9b");
647 assert_eq!(creed.self_model.provider, "ollama");
648 assert_eq!(creed.self_model.weight_class, "middleweight");
649 assert!(!creed.self_model.capabilities.is_empty());
650 assert_eq!(creed.self_model.constraints.len(), 2);
651 assert!(creed.self_model.constraints[0].contains("2048"));
652 assert!(creed.self_model.constraints[1].contains("0.5"));
653 assert_eq!(creed.self_model.architecture_notes.len(), 5);
654 }
655
656 #[test]
657 fn test_record_bout() {
658 let mut creed = Creed::new("ECHO");
659 let before = creed.updated_at;
660 creed.record_bout();
661 assert_eq!(creed.bout_count, 1);
662 assert!(creed.updated_at >= before);
663 creed.record_bout();
664 assert_eq!(creed.bout_count, 2);
665 }
666
667 #[test]
668 fn test_record_messages() {
669 let mut creed = Creed::new("ECHO");
670 creed.record_messages(5);
671 assert_eq!(creed.message_count, 5);
672 creed.record_messages(3);
673 assert_eq!(creed.message_count, 8);
674 }
675
676 #[test]
677 fn test_learn_adds_new_observation() {
678 let mut creed = Creed::new("ECHO");
679 creed.learn("Users prefer concise responses", 0.8);
680 assert_eq!(creed.learned_behaviors.len(), 1);
681 assert_eq!(
682 creed.learned_behaviors[0].observation,
683 "Users prefer concise responses"
684 );
685 assert!((creed.learned_behaviors[0].confidence - 0.8).abs() < f64::EPSILON);
686 assert_eq!(creed.learned_behaviors[0].reinforcement_count, 1);
687 assert_eq!(creed.version, 2); }
689
690 #[test]
691 fn test_learn_reinforces_existing_observation() {
692 let mut creed = Creed::new("ECHO");
693 creed.learn("Users prefer concise responses", 0.8);
694 creed.learn("Users prefer concise responses", 0.6);
695 assert_eq!(creed.learned_behaviors.len(), 1);
696 assert!((creed.learned_behaviors[0].confidence - 0.7).abs() < f64::EPSILON);
698 assert_eq!(creed.learned_behaviors[0].reinforcement_count, 2);
699 assert_eq!(creed.version, 3); }
701
702 #[test]
703 fn test_render_produces_nonempty_output_with_all_sections() {
704 let manifest = sample_manifest();
705 let mut creed = Creed::new("ECHO")
706 .with_identity("You are ECHO, an introspective analyst.")
707 .with_trait("curiosity", 0.9)
708 .with_directive("Always explain your reasoning")
709 .with_self_awareness(&manifest);
710 creed.interaction_style = InteractionStyle {
711 verbosity: "balanced".to_string(),
712 tone: "technical".to_string(),
713 uses_metaphors: true,
714 proactive: true,
715 notes: vec!["Prefers bullet points".to_string()],
716 };
717 creed.relationships.push(Relationship {
718 entity: "Admin".to_string(),
719 entity_type: "user".to_string(),
720 nature: "supervisor".to_string(),
721 trust: 0.95,
722 interaction_count: 42,
723 notes: "Primary operator".to_string(),
724 });
725 creed.learn("Users prefer concise responses", 0.8);
726 creed.record_bout();
727
728 let rendered = creed.render();
729 assert!(rendered.contains("## CREED"));
730 assert!(rendered.contains("### Identity"));
731 assert!(rendered.contains("ECHO, an introspective analyst"));
732 assert!(rendered.contains("### Personality Traits"));
733 assert!(rendered.contains("curiosity"));
734 assert!(rendered.contains("### Core Directives"));
735 assert!(rendered.contains("Always explain your reasoning"));
736 assert!(rendered.contains("### Self-Awareness"));
737 assert!(rendered.contains("qwen3.5:9b"));
738 assert!(rendered.contains("### Learned Behaviors"));
739 assert!(rendered.contains("Users prefer concise responses"));
740 assert!(rendered.contains("### Communication Style"));
741 assert!(rendered.contains("balanced"));
742 assert!(rendered.contains("### Known Relationships"));
743 assert!(rendered.contains("Admin"));
744 assert!(rendered.contains("### Experience"));
745 assert!(rendered.contains("Bouts fought: 1"));
746 }
747
748 #[test]
749 fn test_render_skips_empty_sections() {
750 let creed = Creed::new("ECHO");
751 let rendered = creed.render();
752 assert!(rendered.contains("## CREED"));
753 assert!(!rendered.contains("### Identity"));
754 assert!(!rendered.contains("### Personality Traits"));
755 assert!(!rendered.contains("### Core Directives"));
756 assert!(!rendered.contains("### Self-Awareness"));
757 assert!(!rendered.contains("### Learned Behaviors"));
758 assert!(!rendered.contains("### Communication Style"));
759 assert!(!rendered.contains("### Known Relationships"));
760 assert!(rendered.contains("### Experience"));
762 assert!(rendered.contains("Bouts fought: 0"));
763 }
764
765 #[test]
766 fn test_creed_id_display() {
767 let uuid = Uuid::nil();
768 let id = CreedId(uuid);
769 assert_eq!(id.to_string(), uuid.to_string());
770 }
771
772 #[test]
773 fn test_creed_id_serde_roundtrip() {
774 let id = CreedId::new();
775 let json = serde_json::to_string(&id).expect("serialize");
776 let deser: CreedId = serde_json::from_str(&json).expect("deserialize");
778 assert_eq!(deser, id);
779 }
780
781 #[test]
782 fn test_creed_id_default() {
783 let id = CreedId::default();
784 assert_ne!(id.0, Uuid::nil());
785 }
786
787 #[test]
788 fn test_due_heartbeat_tasks_every_bout_always_due() {
789 let creed = Creed::new("ECHO").with_heartbeat_task("Check build status", "every_bout");
790 let due = creed.due_heartbeat_tasks();
791 assert_eq!(due.len(), 1);
792 assert_eq!(due[0].task, "Check build status");
793 }
794
795 #[test]
796 fn test_due_heartbeat_tasks_on_wake_only_first_time() {
797 let mut creed = Creed::new("ECHO").with_heartbeat_task("Startup check", "on_wake");
798
799 let due = creed.due_heartbeat_tasks();
801 assert_eq!(due.len(), 1);
802
803 creed.mark_heartbeat_checked(0);
805 let due = creed.due_heartbeat_tasks();
806 assert_eq!(due.len(), 0);
807 }
808
809 #[test]
810 fn test_due_heartbeat_tasks_hourly_cadence() {
811 let mut creed = Creed::new("ECHO").with_heartbeat_task("Hourly check", "hourly");
812
813 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
815
816 creed.heartbeat[0].last_checked = Some(chrono::Utc::now());
818 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
819
820 creed.heartbeat[0].last_checked = Some(chrono::Utc::now() - chrono::Duration::hours(2));
822 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
823 }
824
825 #[test]
826 fn test_due_heartbeat_tasks_daily_cadence() {
827 let mut creed = Creed::new("ECHO").with_heartbeat_task("Daily check", "daily");
828
829 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
831
832 creed.heartbeat[0].last_checked = Some(chrono::Utc::now());
834 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
835
836 creed.heartbeat[0].last_checked = Some(chrono::Utc::now() - chrono::Duration::hours(25));
838 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
839 }
840
841 #[test]
842 fn test_due_heartbeat_tasks_inactive_skipped() {
843 let mut creed = Creed::new("ECHO").with_heartbeat_task("Inactive task", "every_bout");
844 creed.heartbeat[0].active = false;
845 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
846 }
847
848 #[test]
849 fn test_due_heartbeat_tasks_unknown_cadence_skipped() {
850 let creed = Creed::new("ECHO").with_heartbeat_task("Mystery task", "weekly");
851 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
852 }
853
854 #[test]
855 fn test_mark_heartbeat_checked() {
856 let mut creed = Creed::new("ECHO").with_heartbeat_task("Task A", "every_bout");
857 assert!(creed.heartbeat[0].last_checked.is_none());
858 assert_eq!(creed.heartbeat[0].execution_count, 0);
859
860 creed.mark_heartbeat_checked(0);
861 assert!(creed.heartbeat[0].last_checked.is_some());
862 assert_eq!(creed.heartbeat[0].execution_count, 1);
863
864 creed.mark_heartbeat_checked(0);
865 assert_eq!(creed.heartbeat[0].execution_count, 2);
866 }
867
868 #[test]
869 fn test_mark_heartbeat_checked_out_of_bounds() {
870 let mut creed = Creed::new("ECHO");
871 creed.mark_heartbeat_checked(99);
873 }
874
875 #[test]
876 fn test_due_heartbeat_tasks_mixed_cadences() {
877 let mut creed = Creed::new("ECHO")
878 .with_heartbeat_task("Always", "every_bout")
879 .with_heartbeat_task("Once", "on_wake")
880 .with_heartbeat_task("Hourly", "hourly");
881
882 creed.mark_heartbeat_checked(1);
884 creed.heartbeat[2].last_checked = Some(chrono::Utc::now());
886
887 let due = creed.due_heartbeat_tasks();
888 assert_eq!(due.len(), 1);
890 assert_eq!(due[0].task, "Always");
891 }
892
893 #[test]
894 fn test_decay_learned_behaviors() {
895 let mut creed = Creed::new("ECHO");
896 creed.learned_behaviors.push(LearnedBehavior {
898 observation: "Old observation".to_string(),
899 confidence: 0.5,
900 reinforcement_count: 1,
901 first_observed: chrono::Utc::now() - chrono::Duration::days(100),
902 last_reinforced: chrono::Utc::now() - chrono::Duration::days(100),
903 });
904 creed.learned_behaviors.push(LearnedBehavior {
906 observation: "Fresh observation".to_string(),
907 confidence: 0.9,
908 reinforcement_count: 3,
909 first_observed: chrono::Utc::now(),
910 last_reinforced: chrono::Utc::now(),
911 });
912
913 creed.decay_learned_behaviors(0.01, 0.3);
914 assert_eq!(creed.learned_behaviors.len(), 1);
917 assert_eq!(creed.learned_behaviors[0].observation, "Fresh observation");
918 }
919
920 #[test]
921 fn test_prune_learned_behaviors() {
922 let mut creed = Creed::new("ECHO");
923 for i in 0..25 {
924 creed.learn(&format!("Observation {}", i), (i as f64) / 25.0);
925 }
926 assert_eq!(creed.learned_behaviors.len(), 25);
927 creed.prune_learned_behaviors(20);
928 assert_eq!(creed.learned_behaviors.len(), 20);
929 assert!(creed.learned_behaviors[0].confidence >= creed.learned_behaviors[19].confidence);
931 }
932}