1#![forbid(unsafe_code)]
18
19use std::collections::VecDeque;
20
21use serde::{Deserialize, Serialize};
22
23use crate::HarmonyVector;
24use crate::anomaly::{AnomalyDetector, AnomalySeverity, HarmonyDimension};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31#[repr(u8)]
32pub enum ActionLevel {
33 Observe = 0,
35 Advise = 1,
37 Correct = 2,
39 Intervene = 3,
41}
42
43impl ActionLevel {
44 #[must_use]
46 pub const fn as_str(self) -> &'static str {
47 match self {
48 Self::Observe => "observe",
49 Self::Advise => "advise",
50 Self::Correct => "correct",
51 Self::Intervene => "intervene",
52 }
53 }
54
55 #[must_use]
57 pub const fn is_active(self) -> bool {
58 matches!(self, Self::Correct | Self::Intervene)
59 }
60}
61
62impl std::fmt::Display for ActionLevel {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "{}", self.as_str())
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum ActionType {
74 None,
76 Log,
78 Recommend,
80 ShedLoad,
82 ToolCooldown,
84 TightenDharma,
86 MemorySweep,
88 CircuitBreaker,
90 ForceTheta,
92 RefuseWrites,
94 IncreaseMonitoring,
96}
97
98impl ActionType {
99 #[must_use]
101 pub const fn as_str(self) -> &'static str {
102 match self {
103 Self::None => "none",
104 Self::Log => "log",
105 Self::Recommend => "recommend",
106 Self::ShedLoad => "shed_load",
107 Self::ToolCooldown => "tool_cooldown",
108 Self::TightenDharma => "tighten_dharma",
109 Self::MemorySweep => "memory_sweep",
110 Self::CircuitBreaker => "circuit_breaker",
111 Self::ForceTheta => "force_theta",
112 Self::RefuseWrites => "refuse_writes",
113 Self::IncreaseMonitoring => "increase_monitoring",
114 }
115 }
116}
117
118impl std::fmt::Display for ActionType {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(f, "{}", self.as_str())
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct HomeostaticAction {
129 pub dimension: HarmonyDimension,
131 pub level: ActionLevel,
133 pub action: ActionType,
135 pub current_value: f32,
137 pub threshold: f32,
139 pub description: String,
141 pub executed: bool,
143}
144
145impl HomeostaticAction {
146 #[must_use]
148 pub fn to_json(&self) -> serde_json::Value {
149 serde_json::json!({
150 "dimension": self.dimension.as_str(),
151 "level": self.level.as_str(),
152 "action": self.action.as_str(),
153 "current_value": self.current_value,
154 "threshold": self.threshold,
155 "description": self.description,
156 "executed": self.executed,
157 })
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct DimensionThreshold {
166 pub dimension: HarmonyDimension,
168 pub advise_threshold: f32,
170 pub correct_threshold: f32,
172 pub intervene_threshold: f32,
174 pub high_is_bad: bool,
176}
177
178impl DimensionThreshold {
179 #[must_use]
181 pub const fn high_is_bad(
182 dimension: HarmonyDimension,
183 advise: f32,
184 correct: f32,
185 intervene: f32,
186 ) -> Self {
187 Self {
188 dimension,
189 advise_threshold: advise,
190 correct_threshold: correct,
191 intervene_threshold: intervene,
192 high_is_bad: true,
193 }
194 }
195
196 #[must_use]
198 pub const fn low_is_bad(
199 dimension: HarmonyDimension,
200 advise: f32,
201 correct: f32,
202 intervene: f32,
203 ) -> Self {
204 Self {
205 dimension,
206 advise_threshold: advise,
207 correct_threshold: correct,
208 intervene_threshold: intervene,
209 high_is_bad: false,
210 }
211 }
212
213 #[must_use]
215 pub fn evaluate(&self, value: f32) -> ActionLevel {
216 if self.high_is_bad {
217 if value >= self.intervene_threshold {
218 ActionLevel::Intervene
219 } else if value >= self.correct_threshold {
220 ActionLevel::Correct
221 } else if value >= self.advise_threshold {
222 ActionLevel::Advise
223 } else {
224 ActionLevel::Observe
225 }
226 } else {
227 if value <= self.intervene_threshold {
229 ActionLevel::Intervene
230 } else if value <= self.correct_threshold {
231 ActionLevel::Correct
232 } else if value <= self.advise_threshold {
233 ActionLevel::Advise
234 } else {
235 ActionLevel::Observe
236 }
237 }
238 }
239}
240
241#[must_use]
243pub fn default_thresholds() -> Vec<DimensionThreshold> {
244 vec![
245 DimensionThreshold::high_is_bad(HarmonyDimension::CpuLoad, 0.7, 0.85, 0.95),
246 DimensionThreshold::high_is_bad(HarmonyDimension::MemoryPressure, 0.7, 0.85, 0.95),
247 DimensionThreshold::high_is_bad(HarmonyDimension::SwapUsage, 0.3, 0.5, 0.8),
248 DimensionThreshold::high_is_bad(HarmonyDimension::DiskIoRate, 0.7, 0.85, 0.95),
249 DimensionThreshold::low_is_bad(HarmonyDimension::HealthScore, 0.6, 0.4, 0.2),
250 DimensionThreshold::low_is_bad(HarmonyDimension::BatteryPercent, 0.3, 0.15, 0.05),
251 DimensionThreshold::high_is_bad(HarmonyDimension::Temperature, 70.0, 80.0, 90.0),
252 ]
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct HomeostaticConfig {
260 pub thresholds: Vec<DimensionThreshold>,
262 pub execute_actions: bool,
264 pub max_history: usize,
266 pub use_anomaly_detector: bool,
268}
269
270impl Default for HomeostaticConfig {
271 fn default() -> Self {
272 Self {
273 thresholds: default_thresholds(),
274 execute_actions: true,
275 max_history: 100,
276 use_anomaly_detector: true,
277 }
278 }
279}
280
281#[derive(Debug, Clone, Default, Serialize, Deserialize)]
285pub struct LoopStats {
286 pub cycles: u64,
288 pub total_actions: u64,
290 pub actions_per_level: [u64; 4],
292 pub actions_per_type: std::collections::HashMap<String, u64>,
294 pub last_cycle: i64,
296}
297
298pub struct HomeostaticLoop {
323 config: HomeostaticConfig,
324 history: VecDeque<HomeostaticAction>,
325 stats: LoopStats,
326}
327
328impl Default for HomeostaticLoop {
329 fn default() -> Self {
330 Self::new(HomeostaticConfig::default())
331 }
332}
333
334impl std::fmt::Debug for HomeostaticLoop {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 f.debug_struct("HomeostaticLoop")
337 .field("cycles", &self.stats.cycles)
338 .field("total_actions", &self.stats.total_actions)
339 .field("history_len", &self.history.len())
340 .finish_non_exhaustive()
341 }
342}
343
344impl HomeostaticLoop {
345 #[must_use]
347 pub fn new(config: HomeostaticConfig) -> Self {
348 Self {
349 config,
350 history: VecDeque::new(),
351 stats: LoopStats::default(),
352 }
353 }
354
355 pub fn sample_cycle(
358 &mut self,
359 hv: &HarmonyVector,
360 detector: &AnomalyDetector,
361 ) -> Vec<HomeostaticAction> {
362 self.stats.cycles += 1;
363 self.stats.last_cycle = chrono::Utc::now().timestamp();
364
365 let mut new_actions = Vec::new();
366
367 for threshold in &self.config.thresholds {
369 if let Some(value) = threshold.dimension.extract(hv) {
370 let level = threshold.evaluate(value);
371
372 if level != ActionLevel::Observe {
373 let action_type = self.select_action(threshold.dimension, level);
374 let description =
375 self.describe_action(threshold.dimension, level, value, threshold);
376 let crossed_threshold = match level {
377 ActionLevel::Intervene => threshold.intervene_threshold,
378 ActionLevel::Correct => threshold.correct_threshold,
379 ActionLevel::Advise => threshold.advise_threshold,
380 ActionLevel::Observe => threshold.advise_threshold,
381 };
382
383 new_actions.push(HomeostaticAction {
384 dimension: threshold.dimension,
385 level,
386 action: action_type,
387 current_value: value,
388 threshold: crossed_threshold,
389 description,
390 executed: self.config.execute_actions && level.is_active(),
391 });
392 }
393 }
394 }
395
396 if self.config.use_anomaly_detector {
398 for dim in HarmonyDimension::ALL {
399 let (mean, std, n) = detector.stats(dim);
400 if n < 5 {
401 continue;
402 }
403
404 if let Some(current) = dim.extract(hv) {
405 if std > 0.0 {
406 let z = (current - mean) / std;
407 if let Some(severity) = AnomalySeverity::from_z_score(z) {
408 let level = match severity {
409 AnomalySeverity::Critical => ActionLevel::Intervene,
410 AnomalySeverity::Warning => ActionLevel::Advise,
411 };
412
413 if !new_actions.iter().any(|a| a.dimension == dim) {
415 let action_type = if level == ActionLevel::Intervene {
416 ActionType::IncreaseMonitoring
417 } else {
418 ActionType::Log
419 };
420
421 new_actions.push(HomeostaticAction {
422 dimension: dim,
423 level,
424 action: action_type,
425 current_value: current,
426 threshold: mean,
427 description: format!(
428 "Anomaly detected: {} z-score {:.2} (mean={:.2}, std={:.2})",
429 dim.as_str(), z, mean, std
430 ),
431 executed: false,
432 });
433 }
434 }
435 }
436 }
437 }
438 }
439
440 for action in &new_actions {
442 self.record_action(action);
443 }
444
445 new_actions
446 }
447
448 const fn select_action(&self, dim: HarmonyDimension, level: ActionLevel) -> ActionType {
450 match (dim, level) {
451 (HarmonyDimension::CpuLoad, ActionLevel::Advise) => ActionType::Log,
453 (HarmonyDimension::CpuLoad, ActionLevel::Correct) => ActionType::ShedLoad,
454 (HarmonyDimension::CpuLoad, ActionLevel::Intervene) => ActionType::ForceTheta,
455
456 (HarmonyDimension::MemoryPressure, ActionLevel::Advise) => ActionType::Log,
458 (HarmonyDimension::MemoryPressure, ActionLevel::Correct) => ActionType::MemorySweep,
459 (HarmonyDimension::MemoryPressure, ActionLevel::Intervene) => ActionType::RefuseWrites,
460
461 (HarmonyDimension::SwapUsage, ActionLevel::Advise) => ActionType::Log,
463 (HarmonyDimension::SwapUsage, ActionLevel::Correct) => ActionType::MemorySweep,
464 (HarmonyDimension::SwapUsage, ActionLevel::Intervene) => ActionType::RefuseWrites,
465
466 (HarmonyDimension::DiskIoRate, ActionLevel::Advise) => ActionType::Log,
468 (HarmonyDimension::DiskIoRate, ActionLevel::Correct) => ActionType::ShedLoad,
469 (HarmonyDimension::DiskIoRate, ActionLevel::Intervene) => ActionType::CircuitBreaker,
470
471 (HarmonyDimension::HealthScore, ActionLevel::Advise) => ActionType::Recommend,
473 (HarmonyDimension::HealthScore, ActionLevel::Correct) => ActionType::TightenDharma,
474 (HarmonyDimension::HealthScore, ActionLevel::Intervene) => ActionType::ForceTheta,
475
476 (HarmonyDimension::BatteryPercent, ActionLevel::Advise) => ActionType::Recommend,
478 (HarmonyDimension::BatteryPercent, ActionLevel::Correct) => ActionType::ShedLoad,
479 (HarmonyDimension::BatteryPercent, ActionLevel::Intervene) => ActionType::ForceTheta,
480
481 (HarmonyDimension::Temperature, ActionLevel::Advise) => ActionType::Log,
483 (HarmonyDimension::Temperature, ActionLevel::Correct) => ActionType::ShedLoad,
484 (HarmonyDimension::Temperature, ActionLevel::Intervene) => ActionType::ForceTheta,
485
486 (_, ActionLevel::Observe) => ActionType::None,
488 }
489 }
490
491 fn describe_action(
493 &self,
494 dim: HarmonyDimension,
495 level: ActionLevel,
496 value: f32,
497 threshold: &DimensionThreshold,
498 ) -> String {
499 let direction = if threshold.high_is_bad { "high" } else { "low" };
500 format!(
501 "{} {} ({:.2} {} threshold {:.2}) → {}",
502 dim.as_str(),
503 direction,
504 value,
505 if threshold.high_is_bad { ">=" } else { "<=" },
506 match level {
507 ActionLevel::Intervene => threshold.intervene_threshold,
508 ActionLevel::Correct => threshold.correct_threshold,
509 ActionLevel::Advise => threshold.advise_threshold,
510 ActionLevel::Observe => threshold.advise_threshold,
511 },
512 level,
513 )
514 }
515
516 fn record_action(&mut self, action: &HomeostaticAction) {
518 self.stats.total_actions += 1;
519 self.stats.actions_per_level[action.level as usize] += 1;
520 *self
521 .stats
522 .actions_per_type
523 .entry(action.action.as_str().to_string())
524 .or_insert(0) += 1;
525
526 if self.history.len() >= self.config.max_history {
527 self.history.pop_front();
528 }
529 self.history.push_back(action.clone());
530 }
531
532 #[must_use]
534 pub fn history(&self, limit: usize) -> Vec<&HomeostaticAction> {
535 self.history.iter().rev().take(limit).collect()
536 }
537
538 #[must_use]
540 pub const fn cycles(&self) -> u64 {
541 self.stats.cycles
542 }
543
544 #[must_use]
546 pub const fn total_actions(&self) -> u64 {
547 self.stats.total_actions
548 }
549
550 #[must_use]
552 pub const fn actions_at_level(&self, level: ActionLevel) -> u64 {
553 self.stats.actions_per_level[level as usize]
554 }
555
556 #[must_use]
558 pub const fn stats(&self) -> &LoopStats {
559 &self.stats
560 }
561
562 #[must_use]
564 pub fn summary(&self) -> serde_json::Value {
565 serde_json::json!({
566 "cycles": self.stats.cycles,
567 "total_actions": self.stats.total_actions,
568 "actions_per_level": {
569 "observe": self.stats.actions_per_level[0],
570 "advise": self.stats.actions_per_level[1],
571 "correct": self.stats.actions_per_level[2],
572 "intervene": self.stats.actions_per_level[3],
573 },
574 "actions_per_type": self.stats.actions_per_type,
575 "last_cycle": self.stats.last_cycle,
576 "history_len": self.history.len(),
577 "execute_actions": self.config.execute_actions,
578 })
579 }
580
581 pub fn clear(&mut self) {
583 self.history.clear();
584 self.stats = LoopStats::default();
585 }
586}
587
588#[cfg(test)]
591mod tests {
592 use super::*;
593 use crate::anomaly::AnomalyConfig;
594
595 fn make_hv(
596 cpu: f32,
597 mem: f32,
598 swap: f32,
599 disk: f32,
600 health: f32,
601 battery: f32,
602 temp: Option<f32>,
603 ) -> HarmonyVector {
604 let _ = health; HarmonyVector {
606 cpu_load: cpu,
607 memory_pressure: mem,
608 swap_usage: swap,
609 thermal_state: crate::ThermalState::Normal,
610 temperature_c: temp,
611 battery_state: crate::BatteryState::Discharging,
612 battery_percent: battery,
613 disk_io_rate: disk,
614 active: true,
615 guna: crate::GunaTag::Sattvic,
616 timestamp: chrono::Utc::now(),
617 }
618 }
619
620 #[test]
621 fn action_level_as_str() {
622 assert_eq!(ActionLevel::Observe.as_str(), "observe");
623 assert_eq!(ActionLevel::Advise.as_str(), "advise");
624 assert_eq!(ActionLevel::Correct.as_str(), "correct");
625 assert_eq!(ActionLevel::Intervene.as_str(), "intervene");
626 }
627
628 #[test]
629 fn action_level_is_active() {
630 assert!(!ActionLevel::Observe.is_active());
631 assert!(!ActionLevel::Advise.is_active());
632 assert!(ActionLevel::Correct.is_active());
633 assert!(ActionLevel::Intervene.is_active());
634 }
635
636 #[test]
637 fn action_type_as_str() {
638 assert_eq!(ActionType::None.as_str(), "none");
639 assert_eq!(ActionType::ShedLoad.as_str(), "shed_load");
640 assert_eq!(ActionType::ForceTheta.as_str(), "force_theta");
641 }
642
643 #[test]
644 fn threshold_high_is_bad_evaluate() {
645 let t = DimensionThreshold::high_is_bad(HarmonyDimension::CpuLoad, 0.7, 0.85, 0.95);
646 assert_eq!(t.evaluate(0.5), ActionLevel::Observe);
647 assert_eq!(t.evaluate(0.7), ActionLevel::Advise);
648 assert_eq!(t.evaluate(0.85), ActionLevel::Correct);
649 assert_eq!(t.evaluate(0.95), ActionLevel::Intervene);
650 }
651
652 #[test]
653 fn threshold_low_is_bad_evaluate() {
654 let t = DimensionThreshold::low_is_bad(HarmonyDimension::BatteryPercent, 0.3, 0.15, 0.05);
655 assert_eq!(t.evaluate(0.8), ActionLevel::Observe);
656 assert_eq!(t.evaluate(0.3), ActionLevel::Advise);
657 assert_eq!(t.evaluate(0.15), ActionLevel::Correct);
658 assert_eq!(t.evaluate(0.05), ActionLevel::Intervene);
659 }
660
661 #[test]
662 fn default_thresholds_cover_all_dimensions() {
663 let thresholds = default_thresholds();
664 assert_eq!(thresholds.len(), 7);
665 for dim in HarmonyDimension::ALL {
666 assert!(
667 thresholds.iter().any(|t| t.dimension == dim),
668 "Missing threshold for {dim:?}"
669 );
670 }
671 }
672
673 #[test]
674 fn loop_no_action_on_healthy_state() {
675 let mut loop_ = HomeostaticLoop::default();
676 let hv = make_hv(0.3, 0.3, 0.05, 0.2, 0.9, 0.8, Some(45.0));
677 let detector = AnomalyDetector::new(AnomalyConfig::default());
678
679 let actions = loop_.sample_cycle(&hv, &detector);
680 assert!(actions.is_empty());
681 assert_eq!(loop_.cycles(), 1);
682 }
683
684 #[test]
685 fn loop_advise_on_high_cpu() {
686 let mut loop_ = HomeostaticLoop::default();
687 let hv = make_hv(0.75, 0.3, 0.05, 0.2, 0.9, 0.8, Some(45.0));
688 let detector = AnomalyDetector::new(AnomalyConfig::default());
689
690 let actions = loop_.sample_cycle(&hv, &detector);
691 assert!(actions.iter().any(|a| a.dimension == HarmonyDimension::CpuLoad && a.level == ActionLevel::Advise));
692 }
693
694 #[test]
695 fn loop_correct_on_high_memory() {
696 let mut loop_ = HomeostaticLoop::default();
697 let hv = make_hv(0.3, 0.88, 0.05, 0.2, 0.9, 0.8, Some(45.0));
698 let detector = AnomalyDetector::new(AnomalyConfig::default());
699
700 let actions = loop_.sample_cycle(&hv, &detector);
701 assert!(
702 actions
703 .iter()
704 .any(|a| a.dimension == HarmonyDimension::MemoryPressure
705 && a.level == ActionLevel::Correct)
706 );
707 }
708
709 #[test]
710 fn loop_intervene_on_critical_battery() {
711 let mut loop_ = HomeostaticLoop::default();
712 let hv = make_hv(0.3, 0.3, 0.05, 0.2, 0.9, 0.03, Some(45.0));
713 let detector = AnomalyDetector::new(AnomalyConfig::default());
714
715 let actions = loop_.sample_cycle(&hv, &detector);
716 assert!(
717 actions
718 .iter()
719 .any(|a| a.dimension == HarmonyDimension::BatteryPercent
720 && a.level == ActionLevel::Intervene)
721 );
722 }
723
724 #[test]
725 fn loop_intervene_on_high_temp() {
726 let mut loop_ = HomeostaticLoop::default();
727 let hv = make_hv(0.3, 0.3, 0.05, 0.2, 0.9, 0.8, Some(92.0));
728 let detector = AnomalyDetector::new(AnomalyConfig::default());
729
730 let actions = loop_.sample_cycle(&hv, &detector);
731 assert!(
732 actions
733 .iter()
734 .any(|a| a.dimension == HarmonyDimension::Temperature
735 && a.level == ActionLevel::Intervene)
736 );
737 }
738
739 #[test]
740 fn loop_actions_recorded_in_history() {
741 let mut loop_ = HomeostaticLoop::default();
742 let hv = make_hv(0.9, 0.9, 0.6, 0.2, 0.3, 0.1, Some(85.0));
743 let detector = AnomalyDetector::new(AnomalyConfig::default());
744
745 let actions = loop_.sample_cycle(&hv, &detector);
746 assert!(!actions.is_empty());
747
748 let history = loop_.history(10);
749 assert!(!history.is_empty());
750 }
751
752 #[test]
753 fn loop_stats_tracked() {
754 let mut loop_ = HomeostaticLoop::default();
755 let hv = make_hv(0.75, 0.3, 0.05, 0.2, 0.9, 0.8, Some(45.0));
756 let detector = AnomalyDetector::new(AnomalyConfig::default());
757
758 loop_.sample_cycle(&hv, &detector);
759 loop_.sample_cycle(&hv, &detector);
760
761 assert_eq!(loop_.cycles(), 2);
762 assert!(loop_.total_actions() >= 2);
763 assert!(loop_.actions_at_level(ActionLevel::Advise) >= 2);
764 }
765
766 #[test]
767 fn loop_dry_run_does_not_execute() {
768 let config = HomeostaticConfig {
769 execute_actions: false,
770 ..Default::default()
771 };
772 let mut loop_ = HomeostaticLoop::new(config);
773 let hv = make_hv(0.9, 0.3, 0.05, 0.2, 0.9, 0.8, Some(45.0));
774 let detector = AnomalyDetector::new(AnomalyConfig::default());
775
776 let actions = loop_.sample_cycle(&hv, &detector);
777 assert!(actions.iter().all(|a| !a.executed));
778 }
779
780 #[test]
781 fn loop_execute_actions_flag() {
782 let mut loop_ = HomeostaticLoop::default();
783 let hv = make_hv(0.9, 0.3, 0.05, 0.2, 0.9, 0.8, Some(45.0));
784 let detector = AnomalyDetector::new(AnomalyConfig::default());
785
786 let actions = loop_.sample_cycle(&hv, &detector);
787 assert!(actions.iter().any(|a| a.level.is_active() && a.executed));
789 }
790
791 #[test]
792 fn loop_clear_resets() {
793 let mut loop_ = HomeostaticLoop::default();
794 let hv = make_hv(0.9, 0.3, 0.05, 0.2, 0.9, 0.8, Some(45.0));
795 let detector = AnomalyDetector::new(AnomalyConfig::default());
796
797 loop_.sample_cycle(&hv, &detector);
798 assert!(loop_.total_actions() > 0);
799
800 loop_.clear();
801 assert_eq!(loop_.total_actions(), 0);
802 assert_eq!(loop_.cycles(), 0);
803 }
804
805 #[test]
806 fn loop_summary_json() {
807 let mut loop_ = HomeostaticLoop::default();
808 let hv = make_hv(0.75, 0.3, 0.05, 0.2, 0.9, 0.8, Some(45.0));
809 let detector = AnomalyDetector::new(AnomalyConfig::default());
810
811 loop_.sample_cycle(&hv, &detector);
812 let summary = loop_.summary();
813 assert_eq!(summary["cycles"], 1);
814 assert!(summary["total_actions"].as_u64().unwrap() > 0);
815 }
816
817 #[test]
818 fn action_to_json() {
819 let action = HomeostaticAction {
820 dimension: HarmonyDimension::CpuLoad,
821 level: ActionLevel::Correct,
822 action: ActionType::ShedLoad,
823 current_value: 0.88,
824 threshold: 0.85,
825 description: "test".to_string(),
826 executed: true,
827 };
828 let json = action.to_json();
829 assert_eq!(json["dimension"], "cpu_load");
830 assert_eq!(json["level"], "correct");
831 assert_eq!(json["action"], "shed_load");
832 assert_eq!(json["executed"], true);
833 }
834
835 #[test]
836 fn multiple_dimensions_flagged() {
837 let mut loop_ = HomeostaticLoop::default();
838 let hv = make_hv(0.9, 0.9, 0.6, 0.9, 0.3, 0.1, Some(85.0));
839 let detector = AnomalyDetector::new(AnomalyConfig::default());
840
841 let actions = loop_.sample_cycle(&hv, &detector);
842 assert!(actions.len() >= 3);
844 }
845
846 #[test]
847 fn select_action_mapping() {
848 let loop_ = HomeostaticLoop::default();
849 assert_eq!(
850 loop_.select_action(HarmonyDimension::CpuLoad, ActionLevel::Correct),
851 ActionType::ShedLoad
852 );
853 assert_eq!(
854 loop_.select_action(HarmonyDimension::CpuLoad, ActionLevel::Intervene),
855 ActionType::ForceTheta
856 );
857 assert_eq!(
858 loop_.select_action(HarmonyDimension::MemoryPressure, ActionLevel::Correct),
859 ActionType::MemorySweep
860 );
861 assert_eq!(
862 loop_.select_action(HarmonyDimension::MemoryPressure, ActionLevel::Intervene),
863 ActionType::RefuseWrites
864 );
865 assert_eq!(
866 loop_.select_action(HarmonyDimension::BatteryPercent, ActionLevel::Advise),
867 ActionType::Recommend
868 );
869 assert_eq!(
870 loop_.select_action(HarmonyDimension::Temperature, ActionLevel::Intervene),
871 ActionType::ForceTheta
872 );
873 }
874}