1#![forbid(unsafe_code)]
2
3use crate::budget::DegradationLevel;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum GuardrailKind {
50 Memory,
52 QueueDepth,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
58pub enum AlertSeverity {
59 Warning,
61 Critical,
63 Emergency,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct GuardrailAlert {
70 pub kind: GuardrailKind,
72 pub severity: AlertSeverity,
74 pub recommended_level: DegradationLevel,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct MemoryBudgetConfig {
85 pub soft_limit_bytes: usize,
88 pub hard_limit_bytes: usize,
91 pub emergency_limit_bytes: usize,
94}
95
96impl Default for MemoryBudgetConfig {
97 fn default() -> Self {
98 Self {
99 soft_limit_bytes: 8 * 1024 * 1024,
100 hard_limit_bytes: 16 * 1024 * 1024,
101 emergency_limit_bytes: 32 * 1024 * 1024,
102 }
103 }
104}
105
106impl MemoryBudgetConfig {
107 #[must_use]
109 pub fn small() -> Self {
110 Self {
111 soft_limit_bytes: 2 * 1024 * 1024,
112 hard_limit_bytes: 4 * 1024 * 1024,
113 emergency_limit_bytes: 8 * 1024 * 1024,
114 }
115 }
116
117 #[must_use]
119 pub fn large() -> Self {
120 Self {
121 soft_limit_bytes: 32 * 1024 * 1024,
122 hard_limit_bytes: 64 * 1024 * 1024,
123 emergency_limit_bytes: 128 * 1024 * 1024,
124 }
125 }
126}
127
128#[derive(Debug, Clone)]
133pub struct MemoryBudget {
134 config: MemoryBudgetConfig,
135 peak_bytes: usize,
137 current_bytes: usize,
139 soft_violations: u32,
141 hard_violations: u32,
143}
144
145impl MemoryBudget {
146 #[must_use]
148 pub fn new(config: MemoryBudgetConfig) -> Self {
149 Self {
150 config,
151 peak_bytes: 0,
152 current_bytes: 0,
153 soft_violations: 0,
154 hard_violations: 0,
155 }
156 }
157
158 pub fn check(&mut self, current_bytes: usize) -> Option<GuardrailAlert> {
160 self.current_bytes = current_bytes;
161 if current_bytes > self.peak_bytes {
162 self.peak_bytes = current_bytes;
163 }
164
165 if current_bytes >= self.config.emergency_limit_bytes {
166 self.hard_violations = self.hard_violations.saturating_add(1);
167 Some(GuardrailAlert {
168 kind: GuardrailKind::Memory,
169 severity: AlertSeverity::Emergency,
170 recommended_level: DegradationLevel::SkipFrame,
171 })
172 } else if current_bytes >= self.config.hard_limit_bytes {
173 self.hard_violations = self.hard_violations.saturating_add(1);
174 Some(GuardrailAlert {
175 kind: GuardrailKind::Memory,
176 severity: AlertSeverity::Critical,
177 recommended_level: DegradationLevel::Skeleton,
178 })
179 } else if current_bytes >= self.config.soft_limit_bytes {
180 self.soft_violations = self.soft_violations.saturating_add(1);
181 Some(GuardrailAlert {
182 kind: GuardrailKind::Memory,
183 severity: AlertSeverity::Warning,
184 recommended_level: DegradationLevel::SimpleBorders,
185 })
186 } else {
187 None
188 }
189 }
190
191 #[inline]
193 #[must_use]
194 pub fn current_bytes(&self) -> usize {
195 self.current_bytes
196 }
197
198 #[inline]
200 #[must_use]
201 pub fn peak_bytes(&self) -> usize {
202 self.peak_bytes
203 }
204
205 #[inline]
207 #[must_use]
208 pub fn usage_fraction(&self) -> f64 {
209 if self.config.soft_limit_bytes == 0 {
210 return 1.0;
211 }
212 self.current_bytes as f64 / self.config.soft_limit_bytes as f64
213 }
214
215 #[inline]
217 #[must_use]
218 pub fn soft_violations(&self) -> u32 {
219 self.soft_violations
220 }
221
222 #[inline]
224 #[must_use]
225 pub fn hard_violations(&self) -> u32 {
226 self.hard_violations
227 }
228
229 #[inline]
231 #[must_use]
232 pub fn config(&self) -> &MemoryBudgetConfig {
233 &self.config
234 }
235
236 pub fn reset(&mut self) {
238 self.peak_bytes = 0;
239 self.current_bytes = 0;
240 self.soft_violations = 0;
241 self.hard_violations = 0;
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
251pub enum QueueDropPolicy {
252 #[default]
254 DropOldest,
255 DropNewest,
257 Backpressure,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub struct QueueConfig {
264 pub warn_depth: u32,
267 pub max_depth: u32,
270 pub emergency_depth: u32,
273 pub drop_policy: QueueDropPolicy,
275}
276
277impl Default for QueueConfig {
278 fn default() -> Self {
279 Self {
280 warn_depth: 3,
281 max_depth: 8,
282 emergency_depth: 16,
283 drop_policy: QueueDropPolicy::DropOldest,
284 }
285 }
286}
287
288impl QueueConfig {
289 #[must_use]
291 pub fn strict() -> Self {
292 Self {
293 warn_depth: 2,
294 max_depth: 4,
295 emergency_depth: 8,
296 drop_policy: QueueDropPolicy::Backpressure,
297 }
298 }
299
300 #[must_use]
302 pub fn relaxed() -> Self {
303 Self {
304 warn_depth: 8,
305 max_depth: 16,
306 emergency_depth: 32,
307 drop_policy: QueueDropPolicy::DropOldest,
308 }
309 }
310}
311
312#[derive(Debug, Clone)]
317pub struct QueueGuardrails {
318 config: QueueConfig,
319 peak_depth: u32,
321 current_depth: u32,
323 total_drops: u64,
325 total_backpressure_events: u64,
327}
328
329impl QueueGuardrails {
330 #[must_use]
332 pub fn new(config: QueueConfig) -> Self {
333 Self {
334 config,
335 peak_depth: 0,
336 current_depth: 0,
337 total_drops: 0,
338 total_backpressure_events: 0,
339 }
340 }
341
342 pub fn check(&mut self, current_depth: u32) -> (Option<GuardrailAlert>, QueueAction) {
347 self.current_depth = current_depth;
348 if current_depth > self.peak_depth {
349 self.peak_depth = current_depth;
350 }
351
352 if current_depth >= self.config.emergency_depth {
353 let action = match self.config.drop_policy {
354 QueueDropPolicy::DropOldest => {
355 let excess = current_depth - 1; self.total_drops = self.total_drops.saturating_add(excess as u64);
357 QueueAction::DropOldest(excess)
358 }
359 QueueDropPolicy::DropNewest => {
360 let excess = current_depth - 1; self.total_drops = self.total_drops.saturating_add(excess as u64);
362 QueueAction::DropNewest(excess)
363 }
364 QueueDropPolicy::Backpressure => {
365 self.total_backpressure_events =
366 self.total_backpressure_events.saturating_add(1);
367 QueueAction::Backpressure
368 }
369 };
370 (
371 Some(GuardrailAlert {
372 kind: GuardrailKind::QueueDepth,
373 severity: AlertSeverity::Emergency,
374 recommended_level: DegradationLevel::SkipFrame,
375 }),
376 action,
377 )
378 } else if current_depth >= self.config.max_depth {
379 let action = match self.config.drop_policy {
380 QueueDropPolicy::DropOldest => {
381 let excess = current_depth.saturating_sub(self.config.warn_depth);
382 self.total_drops = self.total_drops.saturating_add(excess as u64);
383 QueueAction::DropOldest(excess)
384 }
385 QueueDropPolicy::DropNewest => {
386 let excess = current_depth.saturating_sub(self.config.warn_depth);
387 self.total_drops = self.total_drops.saturating_add(excess as u64);
388 QueueAction::DropNewest(excess)
389 }
390 QueueDropPolicy::Backpressure => {
391 self.total_backpressure_events =
392 self.total_backpressure_events.saturating_add(1);
393 QueueAction::Backpressure
394 }
395 };
396 (
397 Some(GuardrailAlert {
398 kind: GuardrailKind::QueueDepth,
399 severity: AlertSeverity::Critical,
400 recommended_level: DegradationLevel::EssentialOnly,
401 }),
402 action,
403 )
404 } else if current_depth >= self.config.warn_depth {
405 (
406 Some(GuardrailAlert {
407 kind: GuardrailKind::QueueDepth,
408 severity: AlertSeverity::Warning,
409 recommended_level: DegradationLevel::SimpleBorders,
410 }),
411 QueueAction::None,
412 )
413 } else {
414 (None, QueueAction::None)
415 }
416 }
417
418 #[inline]
420 #[must_use]
421 pub fn current_depth(&self) -> u32 {
422 self.current_depth
423 }
424
425 #[inline]
427 #[must_use]
428 pub fn peak_depth(&self) -> u32 {
429 self.peak_depth
430 }
431
432 #[inline]
434 #[must_use]
435 pub fn total_drops(&self) -> u64 {
436 self.total_drops
437 }
438
439 #[inline]
441 #[must_use]
442 pub fn total_backpressure_events(&self) -> u64 {
443 self.total_backpressure_events
444 }
445
446 #[inline]
448 #[must_use]
449 pub fn config(&self) -> &QueueConfig {
450 &self.config
451 }
452
453 pub fn reset(&mut self) {
455 self.peak_depth = 0;
456 self.current_depth = 0;
457 self.total_drops = 0;
458 self.total_backpressure_events = 0;
459 }
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub enum QueueAction {
465 None,
467 DropOldest(u32),
469 DropNewest(u32),
471 Backpressure,
473}
474
475impl QueueAction {
476 #[inline]
478 #[must_use]
479 pub fn drops_frames(self) -> bool {
480 matches!(self, Self::DropOldest(_) | Self::DropNewest(_))
481 }
482}
483
484#[derive(Debug, Clone, Default)]
490pub struct GuardrailsConfig {
491 pub memory: MemoryBudgetConfig,
493 pub queue: QueueConfig,
495}
496
497#[derive(Debug, Clone)]
499pub struct GuardrailVerdict {
500 pub alerts: Vec<GuardrailAlert>,
502 pub queue_action: QueueAction,
504 pub recommended_level: DegradationLevel,
506}
507
508impl GuardrailVerdict {
509 #[inline]
511 #[must_use]
512 pub fn should_drop_frame(&self) -> bool {
513 self.recommended_level >= DegradationLevel::SkipFrame
514 }
515
516 #[inline]
518 #[must_use]
519 pub fn should_degrade(&self) -> bool {
520 self.recommended_level > DegradationLevel::Full
521 && self.recommended_level < DegradationLevel::SkipFrame
522 }
523
524 #[inline]
526 #[must_use]
527 pub fn is_clear(&self) -> bool {
528 self.alerts.is_empty()
529 }
530
531 #[must_use]
533 pub fn max_severity(&self) -> Option<AlertSeverity> {
534 self.alerts.iter().map(|a| a.severity).max()
535 }
536}
537
538#[derive(Debug, Clone)]
543pub struct FrameGuardrails {
544 memory: MemoryBudget,
545 queue: QueueGuardrails,
546 frames_checked: u64,
548 frames_with_alerts: u64,
550}
551
552impl FrameGuardrails {
553 #[must_use]
555 pub fn new(config: GuardrailsConfig) -> Self {
556 Self {
557 memory: MemoryBudget::new(config.memory),
558 queue: QueueGuardrails::new(config.queue),
559 frames_checked: 0,
560 frames_with_alerts: 0,
561 }
562 }
563
564 pub fn check_frame(&mut self, memory_bytes: usize, queue_depth: u32) -> GuardrailVerdict {
569 self.frames_checked = self.frames_checked.saturating_add(1);
570
571 let mut alerts = Vec::new();
572 let mut max_level = DegradationLevel::Full;
573
574 if let Some(alert) = self.memory.check(memory_bytes) {
576 if alert.recommended_level > max_level {
577 max_level = alert.recommended_level;
578 }
579 alerts.push(alert);
580 }
581
582 let (queue_alert, queue_action) = self.queue.check(queue_depth);
584 if let Some(alert) = queue_alert {
585 if alert.recommended_level > max_level {
586 max_level = alert.recommended_level;
587 }
588 alerts.push(alert);
589 }
590
591 if !alerts.is_empty() {
592 self.frames_with_alerts = self.frames_with_alerts.saturating_add(1);
593 }
594
595 GuardrailVerdict {
596 alerts,
597 queue_action,
598 recommended_level: max_level,
599 }
600 }
601
602 #[inline]
604 #[must_use]
605 pub fn memory(&self) -> &MemoryBudget {
606 &self.memory
607 }
608
609 #[inline]
611 #[must_use]
612 pub fn queue(&self) -> &QueueGuardrails {
613 &self.queue
614 }
615
616 #[inline]
618 #[must_use]
619 pub fn frames_checked(&self) -> u64 {
620 self.frames_checked
621 }
622
623 #[inline]
625 #[must_use]
626 pub fn frames_with_alerts(&self) -> u64 {
627 self.frames_with_alerts
628 }
629
630 #[inline]
632 #[must_use]
633 pub fn alert_rate(&self) -> f64 {
634 if self.frames_checked == 0 {
635 return 0.0;
636 }
637 self.frames_with_alerts as f64 / self.frames_checked as f64
638 }
639
640 #[must_use]
642 pub fn snapshot(&self) -> GuardrailSnapshot {
643 GuardrailSnapshot {
644 memory_bytes: self.memory.current_bytes(),
645 memory_peak_bytes: self.memory.peak_bytes(),
646 memory_usage_fraction: self.memory.usage_fraction(),
647 memory_soft_violations: self.memory.soft_violations(),
648 memory_hard_violations: self.memory.hard_violations(),
649 queue_depth: self.queue.current_depth(),
650 queue_peak_depth: self.queue.peak_depth(),
651 queue_total_drops: self.queue.total_drops(),
652 queue_total_backpressure: self.queue.total_backpressure_events(),
653 frames_checked: self.frames_checked,
654 frames_with_alerts: self.frames_with_alerts,
655 }
656 }
657
658 pub fn reset(&mut self) {
660 self.memory.reset();
661 self.queue.reset();
662 self.frames_checked = 0;
663 self.frames_with_alerts = 0;
664 }
665}
666
667#[derive(Debug, Clone, Copy, PartialEq)]
672pub struct GuardrailSnapshot {
673 pub memory_bytes: usize,
675 pub memory_peak_bytes: usize,
677 pub memory_usage_fraction: f64,
679 pub memory_soft_violations: u32,
681 pub memory_hard_violations: u32,
683 pub queue_depth: u32,
685 pub queue_peak_depth: u32,
687 pub queue_total_drops: u64,
689 pub queue_total_backpressure: u64,
691 pub frames_checked: u64,
693 pub frames_with_alerts: u64,
695}
696
697impl GuardrailSnapshot {
698 pub fn to_jsonl(&self) -> String {
700 format!(
701 concat!(
702 r#"{{"memory_bytes":{},"memory_peak":{},"memory_frac":{:.4},"#,
703 r#""mem_soft_violations":{},"mem_hard_violations":{},"#,
704 r#""queue_depth":{},"queue_peak":{},"queue_drops":{},"#,
705 r#""queue_backpressure":{},"frames_checked":{},"frames_alerted":{}}}"#,
706 ),
707 self.memory_bytes,
708 self.memory_peak_bytes,
709 self.memory_usage_fraction,
710 self.memory_soft_violations,
711 self.memory_hard_violations,
712 self.queue_depth,
713 self.queue_peak_depth,
714 self.queue_total_drops,
715 self.queue_total_backpressure,
716 self.frames_checked,
717 self.frames_with_alerts,
718 )
719 }
720}
721
722pub const CELL_SIZE_BYTES: usize = 16;
728
729#[inline]
733#[must_use]
734pub fn buffer_memory_bytes(width: u16, height: u16) -> usize {
735 width as usize * height as usize * CELL_SIZE_BYTES
736}
737
738#[cfg(test)]
743mod tests {
744 use super::*;
745
746 #[test]
749 fn memory_below_soft_no_alert() {
750 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
751 assert!(mb.check(1024).is_none());
752 assert_eq!(mb.current_bytes(), 1024);
753 }
754
755 #[test]
756 fn memory_at_soft_limit_warns() {
757 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
758 let alert = mb.check(8 * 1024 * 1024).unwrap();
759 assert_eq!(alert.kind, GuardrailKind::Memory);
760 assert_eq!(alert.severity, AlertSeverity::Warning);
761 assert_eq!(alert.recommended_level, DegradationLevel::SimpleBorders);
762 }
763
764 #[test]
765 fn memory_at_hard_limit_critical() {
766 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
767 let alert = mb.check(16 * 1024 * 1024).unwrap();
768 assert_eq!(alert.severity, AlertSeverity::Critical);
769 assert_eq!(alert.recommended_level, DegradationLevel::Skeleton);
770 }
771
772 #[test]
773 fn memory_at_emergency_limit() {
774 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
775 let alert = mb.check(32 * 1024 * 1024).unwrap();
776 assert_eq!(alert.severity, AlertSeverity::Emergency);
777 assert_eq!(alert.recommended_level, DegradationLevel::SkipFrame);
778 }
779
780 #[test]
781 fn memory_peak_tracking() {
782 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
783 mb.check(1000);
784 mb.check(5000);
785 mb.check(3000);
786 assert_eq!(mb.peak_bytes(), 5000);
787 assert_eq!(mb.current_bytes(), 3000);
788 }
789
790 #[test]
791 fn memory_violation_counts() {
792 let config = MemoryBudgetConfig {
793 soft_limit_bytes: 100,
794 hard_limit_bytes: 200,
795 emergency_limit_bytes: 300,
796 };
797 let mut mb = MemoryBudget::new(config);
798 mb.check(50); mb.check(150); mb.check(150); mb.check(250); assert_eq!(mb.soft_violations(), 2);
803 assert_eq!(mb.hard_violations(), 1);
804 }
805
806 #[test]
807 fn memory_usage_fraction() {
808 let config = MemoryBudgetConfig {
809 soft_limit_bytes: 1000,
810 hard_limit_bytes: 2000,
811 emergency_limit_bytes: 3000,
812 };
813 let mut mb = MemoryBudget::new(config);
814 mb.check(500);
815 assert!((mb.usage_fraction() - 0.5).abs() < f64::EPSILON);
816 }
817
818 #[test]
819 fn memory_usage_fraction_zero_limit() {
820 let config = MemoryBudgetConfig {
821 soft_limit_bytes: 0,
822 hard_limit_bytes: 0,
823 emergency_limit_bytes: 0,
824 };
825 let mut mb = MemoryBudget::new(config);
826 mb.check(100);
827 assert!((mb.usage_fraction() - 1.0).abs() < f64::EPSILON);
828 }
829
830 #[test]
831 fn memory_reset_clears_state() {
832 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
833 mb.check(10 * 1024 * 1024); assert!(mb.soft_violations() > 0);
835 mb.reset();
836 assert_eq!(mb.peak_bytes(), 0);
837 assert_eq!(mb.current_bytes(), 0);
838 assert_eq!(mb.soft_violations(), 0);
839 assert_eq!(mb.hard_violations(), 0);
840 }
841
842 #[test]
843 fn memory_config_accessors() {
844 let config = MemoryBudgetConfig::small();
845 let mb = MemoryBudget::new(config);
846 assert_eq!(mb.config().soft_limit_bytes, 2 * 1024 * 1024);
847 }
848
849 #[test]
852 fn queue_below_warn_no_alert() {
853 let mut qg = QueueGuardrails::new(QueueConfig::default());
854 let (alert, action) = qg.check(1);
855 assert!(alert.is_none());
856 assert_eq!(action, QueueAction::None);
857 }
858
859 #[test]
860 fn queue_at_warn_depth() {
861 let mut qg = QueueGuardrails::new(QueueConfig::default());
862 let (alert, action) = qg.check(3);
863 assert_eq!(alert.unwrap().severity, AlertSeverity::Warning);
864 assert_eq!(action, QueueAction::None); }
866
867 #[test]
868 fn queue_at_max_depth_drop_oldest() {
869 let config = QueueConfig {
870 drop_policy: QueueDropPolicy::DropOldest,
871 ..QueueConfig::default()
872 };
873 let mut qg = QueueGuardrails::new(config);
874 let (alert, action) = qg.check(8);
875 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
876 assert!(action.drops_frames());
877 }
878
879 #[test]
880 fn queue_at_max_depth_drop_newest() {
881 let config = QueueConfig {
882 drop_policy: QueueDropPolicy::DropNewest,
883 ..QueueConfig::default()
884 };
885 let mut qg = QueueGuardrails::new(config);
886 let (alert, action) = qg.check(8);
887 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
888 assert_eq!(action, QueueAction::DropNewest(5));
889 }
890
891 #[test]
892 fn queue_at_max_depth_backpressure() {
893 let config = QueueConfig {
894 drop_policy: QueueDropPolicy::Backpressure,
895 ..QueueConfig::default()
896 };
897 let mut qg = QueueGuardrails::new(config);
898 let (alert, action) = qg.check(8);
899 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
900 assert_eq!(action, QueueAction::Backpressure);
901 }
902
903 #[test]
904 fn queue_emergency_drops_to_latest() {
905 let mut qg = QueueGuardrails::new(QueueConfig::default());
906 let (alert, action) = qg.check(16);
907 assert_eq!(alert.unwrap().severity, AlertSeverity::Emergency);
908 assert_eq!(action, QueueAction::DropOldest(15));
910 }
911
912 #[test]
913 fn queue_peak_tracking() {
914 let mut qg = QueueGuardrails::new(QueueConfig::default());
915 qg.check(2);
916 qg.check(5);
917 qg.check(1);
918 assert_eq!(qg.peak_depth(), 5);
919 assert_eq!(qg.current_depth(), 1);
920 }
921
922 #[test]
923 fn queue_drop_counting() {
924 let mut qg = QueueGuardrails::new(QueueConfig::default());
925 qg.check(8); assert!(qg.total_drops() > 0);
927 }
928
929 #[test]
930 fn queue_backpressure_counting() {
931 let config = QueueConfig::strict();
932 let mut qg = QueueGuardrails::new(config);
933 qg.check(4); assert!(qg.total_backpressure_events() > 0);
935 }
936
937 #[test]
938 fn queue_reset_clears_state() {
939 let mut qg = QueueGuardrails::new(QueueConfig::default());
940 qg.check(10);
941 qg.reset();
942 assert_eq!(qg.peak_depth(), 0);
943 assert_eq!(qg.current_depth(), 0);
944 assert_eq!(qg.total_drops(), 0);
945 }
946
947 #[test]
948 fn queue_config_accessors() {
949 let config = QueueConfig::relaxed();
950 let qg = QueueGuardrails::new(config);
951 assert_eq!(qg.config().max_depth, 16);
952 }
953
954 #[test]
957 fn queue_action_drops_frames() {
958 assert!(!QueueAction::None.drops_frames());
959 assert!(QueueAction::DropOldest(3).drops_frames());
960 assert!(QueueAction::DropNewest(1).drops_frames());
961 assert!(!QueueAction::Backpressure.drops_frames());
962 }
963
964 #[test]
967 fn guardrails_clear_when_healthy() {
968 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
969 let v = g.check_frame(1024, 0);
970 assert!(v.is_clear());
971 assert_eq!(v.recommended_level, DegradationLevel::Full);
972 assert_eq!(v.queue_action, QueueAction::None);
973 }
974
975 #[test]
976 fn guardrails_memory_alert_propagates() {
977 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
978 let v = g.check_frame(8 * 1024 * 1024, 0);
979 assert!(!v.is_clear());
980 assert_eq!(v.alerts.len(), 1);
981 assert_eq!(v.alerts[0].kind, GuardrailKind::Memory);
982 assert!(v.should_degrade());
983 assert!(!v.should_drop_frame());
984 }
985
986 #[test]
987 fn guardrails_queue_alert_propagates() {
988 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
989 let v = g.check_frame(0, 8);
990 assert!(!v.is_clear());
991 assert!(v.alerts.iter().any(|a| a.kind == GuardrailKind::QueueDepth));
992 }
993
994 #[test]
995 fn guardrails_both_alerts_combine() {
996 let config = GuardrailsConfig {
997 memory: MemoryBudgetConfig {
998 soft_limit_bytes: 100,
999 hard_limit_bytes: 200,
1000 emergency_limit_bytes: 300,
1001 },
1002 queue: QueueConfig {
1003 warn_depth: 1,
1004 max_depth: 2,
1005 emergency_depth: 3,
1006 drop_policy: QueueDropPolicy::DropOldest,
1007 },
1008 };
1009 let mut g = FrameGuardrails::new(config);
1010 let v = g.check_frame(150, 2);
1011 assert_eq!(v.alerts.len(), 2);
1012 assert!(v.recommended_level >= DegradationLevel::SimpleBorders);
1014 }
1015
1016 #[test]
1017 fn guardrails_emergency_recommends_skip() {
1018 let config = GuardrailsConfig {
1019 memory: MemoryBudgetConfig {
1020 soft_limit_bytes: 100,
1021 hard_limit_bytes: 200,
1022 emergency_limit_bytes: 300,
1023 },
1024 queue: QueueConfig::default(),
1025 };
1026 let mut g = FrameGuardrails::new(config);
1027 let v = g.check_frame(300, 0);
1028 assert!(v.should_drop_frame());
1029 }
1030
1031 #[test]
1032 fn guardrails_frame_counting() {
1033 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1034 g.check_frame(0, 0);
1035 g.check_frame(0, 0);
1036 g.check_frame(8 * 1024 * 1024, 0); assert_eq!(g.frames_checked(), 3);
1038 assert_eq!(g.frames_with_alerts(), 1);
1039 }
1040
1041 #[test]
1042 fn guardrails_alert_rate() {
1043 let config = GuardrailsConfig {
1044 memory: MemoryBudgetConfig {
1045 soft_limit_bytes: 100,
1046 hard_limit_bytes: 200,
1047 emergency_limit_bytes: 300,
1048 },
1049 queue: QueueConfig::default(),
1050 };
1051 let mut g = FrameGuardrails::new(config);
1052 g.check_frame(50, 0); g.check_frame(150, 0); g.check_frame(50, 0); g.check_frame(150, 0); assert!((g.alert_rate() - 0.5).abs() < f64::EPSILON);
1057 }
1058
1059 #[test]
1060 fn guardrails_alert_rate_zero_frames() {
1061 let g = FrameGuardrails::new(GuardrailsConfig::default());
1062 assert!((g.alert_rate() - 0.0).abs() < f64::EPSILON);
1063 }
1064
1065 #[test]
1066 fn guardrails_snapshot_jsonl() {
1067 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1068 g.check_frame(1024, 1);
1069 let snap = g.snapshot();
1070 let line = snap.to_jsonl();
1071 assert!(line.starts_with('{'));
1072 assert!(line.ends_with('}'));
1073 assert!(line.contains("\"memory_bytes\":1024"));
1074 assert!(line.contains("\"queue_depth\":1"));
1075 }
1076
1077 #[test]
1078 fn guardrails_reset_clears_all() {
1079 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1080 g.check_frame(8 * 1024 * 1024, 5);
1081 g.reset();
1082 assert_eq!(g.frames_checked(), 0);
1083 assert_eq!(g.frames_with_alerts(), 0);
1084 assert_eq!(g.memory().peak_bytes(), 0);
1085 assert_eq!(g.queue().peak_depth(), 0);
1086 }
1087
1088 #[test]
1089 fn guardrails_subsystem_access() {
1090 let g = FrameGuardrails::new(GuardrailsConfig::default());
1091 let _ = g.memory().config();
1092 let _ = g.queue().config();
1093 }
1094
1095 #[test]
1098 fn verdict_max_severity_none_when_clear() {
1099 let v = GuardrailVerdict {
1100 alerts: vec![],
1101 queue_action: QueueAction::None,
1102 recommended_level: DegradationLevel::Full,
1103 };
1104 assert!(v.max_severity().is_none());
1105 assert!(v.is_clear());
1106 }
1107
1108 #[test]
1109 fn verdict_max_severity_picks_highest() {
1110 let v = GuardrailVerdict {
1111 alerts: vec![
1112 GuardrailAlert {
1113 kind: GuardrailKind::Memory,
1114 severity: AlertSeverity::Warning,
1115 recommended_level: DegradationLevel::SimpleBorders,
1116 },
1117 GuardrailAlert {
1118 kind: GuardrailKind::QueueDepth,
1119 severity: AlertSeverity::Critical,
1120 recommended_level: DegradationLevel::EssentialOnly,
1121 },
1122 ],
1123 queue_action: QueueAction::None,
1124 recommended_level: DegradationLevel::EssentialOnly,
1125 };
1126 assert_eq!(v.max_severity(), Some(AlertSeverity::Critical));
1127 }
1128
1129 #[test]
1132 fn severity_ordering() {
1133 assert!(AlertSeverity::Warning < AlertSeverity::Critical);
1134 assert!(AlertSeverity::Critical < AlertSeverity::Emergency);
1135 }
1136
1137 #[test]
1140 fn memory_config_small_preset() {
1141 let c = MemoryBudgetConfig::small();
1142 assert!(c.soft_limit_bytes < MemoryBudgetConfig::default().soft_limit_bytes);
1143 }
1144
1145 #[test]
1146 fn memory_config_large_preset() {
1147 let c = MemoryBudgetConfig::large();
1148 assert!(c.soft_limit_bytes > MemoryBudgetConfig::default().soft_limit_bytes);
1149 }
1150
1151 #[test]
1152 fn queue_config_strict_preset() {
1153 let c = QueueConfig::strict();
1154 assert_eq!(c.drop_policy, QueueDropPolicy::Backpressure);
1155 assert!(c.max_depth < QueueConfig::default().max_depth);
1156 }
1157
1158 #[test]
1159 fn queue_config_relaxed_preset() {
1160 let c = QueueConfig::relaxed();
1161 assert!(c.max_depth > QueueConfig::default().max_depth);
1162 }
1163
1164 #[test]
1167 fn buffer_memory_typical_terminal() {
1168 assert_eq!(buffer_memory_bytes(80, 24), 80 * 24 * 16);
1170 }
1171
1172 #[test]
1173 fn buffer_memory_zero_dimension() {
1174 assert_eq!(buffer_memory_bytes(0, 24), 0);
1175 assert_eq!(buffer_memory_bytes(80, 0), 0);
1176 assert_eq!(buffer_memory_bytes(0, 0), 0);
1177 }
1178
1179 #[test]
1180 fn buffer_memory_large_terminal() {
1181 let bytes = buffer_memory_bytes(300, 100);
1183 assert_eq!(bytes, 300 * 100 * 16);
1184 assert_eq!(bytes, 480_000);
1185 }
1186
1187 #[test]
1190 fn queue_drop_policy_default_is_drop_oldest() {
1191 assert_eq!(QueueDropPolicy::default(), QueueDropPolicy::DropOldest);
1192 }
1193
1194 #[test]
1197 fn guardrails_deterministic_for_same_inputs() {
1198 let config = GuardrailsConfig::default();
1199 let mut g1 = FrameGuardrails::new(config.clone());
1200 let mut g2 = FrameGuardrails::new(config);
1201
1202 let inputs = [(1024, 0), (8 * 1024 * 1024, 3), (20 * 1024 * 1024, 10)];
1203 for (mem, queue) in inputs {
1204 let v1 = g1.check_frame(mem, queue);
1205 let v2 = g2.check_frame(mem, queue);
1206 assert_eq!(v1.recommended_level, v2.recommended_level);
1207 assert_eq!(v1.alerts.len(), v2.alerts.len());
1208 assert_eq!(v1.queue_action, v2.queue_action);
1209 }
1210 }
1211}