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 self.total_drops = self.total_drops.saturating_add(1);
361 QueueAction::DropNewest(1)
362 }
363 QueueDropPolicy::Backpressure => {
364 self.total_backpressure_events =
365 self.total_backpressure_events.saturating_add(1);
366 QueueAction::Backpressure
367 }
368 };
369 (
370 Some(GuardrailAlert {
371 kind: GuardrailKind::QueueDepth,
372 severity: AlertSeverity::Emergency,
373 recommended_level: DegradationLevel::SkipFrame,
374 }),
375 action,
376 )
377 } else if current_depth >= self.config.max_depth {
378 let action = match self.config.drop_policy {
379 QueueDropPolicy::DropOldest => {
380 let excess = current_depth.saturating_sub(self.config.warn_depth);
381 self.total_drops = self.total_drops.saturating_add(excess as u64);
382 QueueAction::DropOldest(excess)
383 }
384 QueueDropPolicy::DropNewest => {
385 self.total_drops = self.total_drops.saturating_add(1);
386 QueueAction::DropNewest(1)
387 }
388 QueueDropPolicy::Backpressure => {
389 self.total_backpressure_events =
390 self.total_backpressure_events.saturating_add(1);
391 QueueAction::Backpressure
392 }
393 };
394 (
395 Some(GuardrailAlert {
396 kind: GuardrailKind::QueueDepth,
397 severity: AlertSeverity::Critical,
398 recommended_level: DegradationLevel::EssentialOnly,
399 }),
400 action,
401 )
402 } else if current_depth >= self.config.warn_depth {
403 (
404 Some(GuardrailAlert {
405 kind: GuardrailKind::QueueDepth,
406 severity: AlertSeverity::Warning,
407 recommended_level: DegradationLevel::SimpleBorders,
408 }),
409 QueueAction::None,
410 )
411 } else {
412 (None, QueueAction::None)
413 }
414 }
415
416 #[inline]
418 #[must_use]
419 pub fn current_depth(&self) -> u32 {
420 self.current_depth
421 }
422
423 #[inline]
425 #[must_use]
426 pub fn peak_depth(&self) -> u32 {
427 self.peak_depth
428 }
429
430 #[inline]
432 #[must_use]
433 pub fn total_drops(&self) -> u64 {
434 self.total_drops
435 }
436
437 #[inline]
439 #[must_use]
440 pub fn total_backpressure_events(&self) -> u64 {
441 self.total_backpressure_events
442 }
443
444 #[inline]
446 #[must_use]
447 pub fn config(&self) -> &QueueConfig {
448 &self.config
449 }
450
451 pub fn reset(&mut self) {
453 self.peak_depth = 0;
454 self.current_depth = 0;
455 self.total_drops = 0;
456 self.total_backpressure_events = 0;
457 }
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
462pub enum QueueAction {
463 None,
465 DropOldest(u32),
467 DropNewest(u32),
469 Backpressure,
471}
472
473impl QueueAction {
474 #[inline]
476 #[must_use]
477 pub fn drops_frames(self) -> bool {
478 matches!(self, Self::DropOldest(_) | Self::DropNewest(_))
479 }
480}
481
482#[derive(Debug, Clone, Default)]
488pub struct GuardrailsConfig {
489 pub memory: MemoryBudgetConfig,
491 pub queue: QueueConfig,
493}
494
495#[derive(Debug, Clone)]
497pub struct GuardrailVerdict {
498 pub alerts: Vec<GuardrailAlert>,
500 pub queue_action: QueueAction,
502 pub recommended_level: DegradationLevel,
504}
505
506impl GuardrailVerdict {
507 #[inline]
509 #[must_use]
510 pub fn should_drop_frame(&self) -> bool {
511 self.recommended_level >= DegradationLevel::SkipFrame
512 }
513
514 #[inline]
516 #[must_use]
517 pub fn should_degrade(&self) -> bool {
518 self.recommended_level > DegradationLevel::Full
519 && self.recommended_level < DegradationLevel::SkipFrame
520 }
521
522 #[inline]
524 #[must_use]
525 pub fn is_clear(&self) -> bool {
526 self.alerts.is_empty()
527 }
528
529 #[must_use]
531 pub fn max_severity(&self) -> Option<AlertSeverity> {
532 self.alerts.iter().map(|a| a.severity).max()
533 }
534}
535
536#[derive(Debug, Clone)]
541pub struct FrameGuardrails {
542 memory: MemoryBudget,
543 queue: QueueGuardrails,
544 frames_checked: u64,
546 frames_with_alerts: u64,
548}
549
550impl FrameGuardrails {
551 #[must_use]
553 pub fn new(config: GuardrailsConfig) -> Self {
554 Self {
555 memory: MemoryBudget::new(config.memory),
556 queue: QueueGuardrails::new(config.queue),
557 frames_checked: 0,
558 frames_with_alerts: 0,
559 }
560 }
561
562 pub fn check_frame(&mut self, memory_bytes: usize, queue_depth: u32) -> GuardrailVerdict {
567 self.frames_checked = self.frames_checked.saturating_add(1);
568
569 let mut alerts = Vec::new();
570 let mut max_level = DegradationLevel::Full;
571
572 if let Some(alert) = self.memory.check(memory_bytes) {
574 if alert.recommended_level > max_level {
575 max_level = alert.recommended_level;
576 }
577 alerts.push(alert);
578 }
579
580 let (queue_alert, queue_action) = self.queue.check(queue_depth);
582 if let Some(alert) = queue_alert {
583 if alert.recommended_level > max_level {
584 max_level = alert.recommended_level;
585 }
586 alerts.push(alert);
587 }
588
589 if !alerts.is_empty() {
590 self.frames_with_alerts = self.frames_with_alerts.saturating_add(1);
591 }
592
593 GuardrailVerdict {
594 alerts,
595 queue_action,
596 recommended_level: max_level,
597 }
598 }
599
600 #[inline]
602 #[must_use]
603 pub fn memory(&self) -> &MemoryBudget {
604 &self.memory
605 }
606
607 #[inline]
609 #[must_use]
610 pub fn queue(&self) -> &QueueGuardrails {
611 &self.queue
612 }
613
614 #[inline]
616 #[must_use]
617 pub fn frames_checked(&self) -> u64 {
618 self.frames_checked
619 }
620
621 #[inline]
623 #[must_use]
624 pub fn frames_with_alerts(&self) -> u64 {
625 self.frames_with_alerts
626 }
627
628 #[inline]
630 #[must_use]
631 pub fn alert_rate(&self) -> f64 {
632 if self.frames_checked == 0 {
633 return 0.0;
634 }
635 self.frames_with_alerts as f64 / self.frames_checked as f64
636 }
637
638 #[must_use]
640 pub fn snapshot(&self) -> GuardrailSnapshot {
641 GuardrailSnapshot {
642 memory_bytes: self.memory.current_bytes(),
643 memory_peak_bytes: self.memory.peak_bytes(),
644 memory_usage_fraction: self.memory.usage_fraction(),
645 memory_soft_violations: self.memory.soft_violations(),
646 memory_hard_violations: self.memory.hard_violations(),
647 queue_depth: self.queue.current_depth(),
648 queue_peak_depth: self.queue.peak_depth(),
649 queue_total_drops: self.queue.total_drops(),
650 queue_total_backpressure: self.queue.total_backpressure_events(),
651 frames_checked: self.frames_checked,
652 frames_with_alerts: self.frames_with_alerts,
653 }
654 }
655
656 pub fn reset(&mut self) {
658 self.memory.reset();
659 self.queue.reset();
660 self.frames_checked = 0;
661 self.frames_with_alerts = 0;
662 }
663}
664
665#[derive(Debug, Clone, Copy, PartialEq)]
670pub struct GuardrailSnapshot {
671 pub memory_bytes: usize,
673 pub memory_peak_bytes: usize,
675 pub memory_usage_fraction: f64,
677 pub memory_soft_violations: u32,
679 pub memory_hard_violations: u32,
681 pub queue_depth: u32,
683 pub queue_peak_depth: u32,
685 pub queue_total_drops: u64,
687 pub queue_total_backpressure: u64,
689 pub frames_checked: u64,
691 pub frames_with_alerts: u64,
693}
694
695impl GuardrailSnapshot {
696 pub fn to_jsonl(&self) -> String {
698 format!(
699 concat!(
700 r#"{{"memory_bytes":{},"memory_peak":{},"memory_frac":{:.4},"#,
701 r#""mem_soft_violations":{},"mem_hard_violations":{},"#,
702 r#""queue_depth":{},"queue_peak":{},"queue_drops":{},"#,
703 r#""queue_backpressure":{},"frames_checked":{},"frames_alerted":{}}}"#,
704 ),
705 self.memory_bytes,
706 self.memory_peak_bytes,
707 self.memory_usage_fraction,
708 self.memory_soft_violations,
709 self.memory_hard_violations,
710 self.queue_depth,
711 self.queue_peak_depth,
712 self.queue_total_drops,
713 self.queue_total_backpressure,
714 self.frames_checked,
715 self.frames_with_alerts,
716 )
717 }
718}
719
720pub const CELL_SIZE_BYTES: usize = 16;
726
727#[inline]
731#[must_use]
732pub fn buffer_memory_bytes(width: u16, height: u16) -> usize {
733 width as usize * height as usize * CELL_SIZE_BYTES
734}
735
736#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
747 fn memory_below_soft_no_alert() {
748 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
749 assert!(mb.check(1024).is_none());
750 assert_eq!(mb.current_bytes(), 1024);
751 }
752
753 #[test]
754 fn memory_at_soft_limit_warns() {
755 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
756 let alert = mb.check(8 * 1024 * 1024).unwrap();
757 assert_eq!(alert.kind, GuardrailKind::Memory);
758 assert_eq!(alert.severity, AlertSeverity::Warning);
759 assert_eq!(alert.recommended_level, DegradationLevel::SimpleBorders);
760 }
761
762 #[test]
763 fn memory_at_hard_limit_critical() {
764 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
765 let alert = mb.check(16 * 1024 * 1024).unwrap();
766 assert_eq!(alert.severity, AlertSeverity::Critical);
767 assert_eq!(alert.recommended_level, DegradationLevel::Skeleton);
768 }
769
770 #[test]
771 fn memory_at_emergency_limit() {
772 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
773 let alert = mb.check(32 * 1024 * 1024).unwrap();
774 assert_eq!(alert.severity, AlertSeverity::Emergency);
775 assert_eq!(alert.recommended_level, DegradationLevel::SkipFrame);
776 }
777
778 #[test]
779 fn memory_peak_tracking() {
780 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
781 mb.check(1000);
782 mb.check(5000);
783 mb.check(3000);
784 assert_eq!(mb.peak_bytes(), 5000);
785 assert_eq!(mb.current_bytes(), 3000);
786 }
787
788 #[test]
789 fn memory_violation_counts() {
790 let config = MemoryBudgetConfig {
791 soft_limit_bytes: 100,
792 hard_limit_bytes: 200,
793 emergency_limit_bytes: 300,
794 };
795 let mut mb = MemoryBudget::new(config);
796 mb.check(50); mb.check(150); mb.check(150); mb.check(250); assert_eq!(mb.soft_violations(), 2);
801 assert_eq!(mb.hard_violations(), 1);
802 }
803
804 #[test]
805 fn memory_usage_fraction() {
806 let config = MemoryBudgetConfig {
807 soft_limit_bytes: 1000,
808 hard_limit_bytes: 2000,
809 emergency_limit_bytes: 3000,
810 };
811 let mut mb = MemoryBudget::new(config);
812 mb.check(500);
813 assert!((mb.usage_fraction() - 0.5).abs() < f64::EPSILON);
814 }
815
816 #[test]
817 fn memory_usage_fraction_zero_limit() {
818 let config = MemoryBudgetConfig {
819 soft_limit_bytes: 0,
820 hard_limit_bytes: 0,
821 emergency_limit_bytes: 0,
822 };
823 let mut mb = MemoryBudget::new(config);
824 mb.check(100);
825 assert!((mb.usage_fraction() - 1.0).abs() < f64::EPSILON);
826 }
827
828 #[test]
829 fn memory_reset_clears_state() {
830 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
831 mb.check(10 * 1024 * 1024); assert!(mb.soft_violations() > 0);
833 mb.reset();
834 assert_eq!(mb.peak_bytes(), 0);
835 assert_eq!(mb.current_bytes(), 0);
836 assert_eq!(mb.soft_violations(), 0);
837 assert_eq!(mb.hard_violations(), 0);
838 }
839
840 #[test]
841 fn memory_config_accessors() {
842 let config = MemoryBudgetConfig::small();
843 let mb = MemoryBudget::new(config);
844 assert_eq!(mb.config().soft_limit_bytes, 2 * 1024 * 1024);
845 }
846
847 #[test]
850 fn queue_below_warn_no_alert() {
851 let mut qg = QueueGuardrails::new(QueueConfig::default());
852 let (alert, action) = qg.check(1);
853 assert!(alert.is_none());
854 assert_eq!(action, QueueAction::None);
855 }
856
857 #[test]
858 fn queue_at_warn_depth() {
859 let mut qg = QueueGuardrails::new(QueueConfig::default());
860 let (alert, action) = qg.check(3);
861 assert_eq!(alert.unwrap().severity, AlertSeverity::Warning);
862 assert_eq!(action, QueueAction::None); }
864
865 #[test]
866 fn queue_at_max_depth_drop_oldest() {
867 let config = QueueConfig {
868 drop_policy: QueueDropPolicy::DropOldest,
869 ..QueueConfig::default()
870 };
871 let mut qg = QueueGuardrails::new(config);
872 let (alert, action) = qg.check(8);
873 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
874 assert!(action.drops_frames());
875 }
876
877 #[test]
878 fn queue_at_max_depth_drop_newest() {
879 let config = QueueConfig {
880 drop_policy: QueueDropPolicy::DropNewest,
881 ..QueueConfig::default()
882 };
883 let mut qg = QueueGuardrails::new(config);
884 let (alert, action) = qg.check(8);
885 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
886 assert_eq!(action, QueueAction::DropNewest(1));
887 }
888
889 #[test]
890 fn queue_at_max_depth_backpressure() {
891 let config = QueueConfig {
892 drop_policy: QueueDropPolicy::Backpressure,
893 ..QueueConfig::default()
894 };
895 let mut qg = QueueGuardrails::new(config);
896 let (alert, action) = qg.check(8);
897 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
898 assert_eq!(action, QueueAction::Backpressure);
899 }
900
901 #[test]
902 fn queue_emergency_drops_to_latest() {
903 let mut qg = QueueGuardrails::new(QueueConfig::default());
904 let (alert, action) = qg.check(16);
905 assert_eq!(alert.unwrap().severity, AlertSeverity::Emergency);
906 assert_eq!(action, QueueAction::DropOldest(15));
908 }
909
910 #[test]
911 fn queue_peak_tracking() {
912 let mut qg = QueueGuardrails::new(QueueConfig::default());
913 qg.check(2);
914 qg.check(5);
915 qg.check(1);
916 assert_eq!(qg.peak_depth(), 5);
917 assert_eq!(qg.current_depth(), 1);
918 }
919
920 #[test]
921 fn queue_drop_counting() {
922 let mut qg = QueueGuardrails::new(QueueConfig::default());
923 qg.check(8); assert!(qg.total_drops() > 0);
925 }
926
927 #[test]
928 fn queue_backpressure_counting() {
929 let config = QueueConfig::strict();
930 let mut qg = QueueGuardrails::new(config);
931 qg.check(4); assert!(qg.total_backpressure_events() > 0);
933 }
934
935 #[test]
936 fn queue_reset_clears_state() {
937 let mut qg = QueueGuardrails::new(QueueConfig::default());
938 qg.check(10);
939 qg.reset();
940 assert_eq!(qg.peak_depth(), 0);
941 assert_eq!(qg.current_depth(), 0);
942 assert_eq!(qg.total_drops(), 0);
943 }
944
945 #[test]
946 fn queue_config_accessors() {
947 let config = QueueConfig::relaxed();
948 let qg = QueueGuardrails::new(config);
949 assert_eq!(qg.config().max_depth, 16);
950 }
951
952 #[test]
955 fn queue_action_drops_frames() {
956 assert!(!QueueAction::None.drops_frames());
957 assert!(QueueAction::DropOldest(3).drops_frames());
958 assert!(QueueAction::DropNewest(1).drops_frames());
959 assert!(!QueueAction::Backpressure.drops_frames());
960 }
961
962 #[test]
965 fn guardrails_clear_when_healthy() {
966 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
967 let v = g.check_frame(1024, 0);
968 assert!(v.is_clear());
969 assert_eq!(v.recommended_level, DegradationLevel::Full);
970 assert_eq!(v.queue_action, QueueAction::None);
971 }
972
973 #[test]
974 fn guardrails_memory_alert_propagates() {
975 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
976 let v = g.check_frame(8 * 1024 * 1024, 0);
977 assert!(!v.is_clear());
978 assert_eq!(v.alerts.len(), 1);
979 assert_eq!(v.alerts[0].kind, GuardrailKind::Memory);
980 assert!(v.should_degrade());
981 assert!(!v.should_drop_frame());
982 }
983
984 #[test]
985 fn guardrails_queue_alert_propagates() {
986 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
987 let v = g.check_frame(0, 8);
988 assert!(!v.is_clear());
989 assert!(v.alerts.iter().any(|a| a.kind == GuardrailKind::QueueDepth));
990 }
991
992 #[test]
993 fn guardrails_both_alerts_combine() {
994 let config = GuardrailsConfig {
995 memory: MemoryBudgetConfig {
996 soft_limit_bytes: 100,
997 hard_limit_bytes: 200,
998 emergency_limit_bytes: 300,
999 },
1000 queue: QueueConfig {
1001 warn_depth: 1,
1002 max_depth: 2,
1003 emergency_depth: 3,
1004 drop_policy: QueueDropPolicy::DropOldest,
1005 },
1006 };
1007 let mut g = FrameGuardrails::new(config);
1008 let v = g.check_frame(150, 2);
1009 assert_eq!(v.alerts.len(), 2);
1010 assert!(v.recommended_level >= DegradationLevel::SimpleBorders);
1012 }
1013
1014 #[test]
1015 fn guardrails_emergency_recommends_skip() {
1016 let config = GuardrailsConfig {
1017 memory: MemoryBudgetConfig {
1018 soft_limit_bytes: 100,
1019 hard_limit_bytes: 200,
1020 emergency_limit_bytes: 300,
1021 },
1022 queue: QueueConfig::default(),
1023 };
1024 let mut g = FrameGuardrails::new(config);
1025 let v = g.check_frame(300, 0);
1026 assert!(v.should_drop_frame());
1027 }
1028
1029 #[test]
1030 fn guardrails_frame_counting() {
1031 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1032 g.check_frame(0, 0);
1033 g.check_frame(0, 0);
1034 g.check_frame(8 * 1024 * 1024, 0); assert_eq!(g.frames_checked(), 3);
1036 assert_eq!(g.frames_with_alerts(), 1);
1037 }
1038
1039 #[test]
1040 fn guardrails_alert_rate() {
1041 let config = GuardrailsConfig {
1042 memory: MemoryBudgetConfig {
1043 soft_limit_bytes: 100,
1044 hard_limit_bytes: 200,
1045 emergency_limit_bytes: 300,
1046 },
1047 queue: QueueConfig::default(),
1048 };
1049 let mut g = FrameGuardrails::new(config);
1050 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);
1055 }
1056
1057 #[test]
1058 fn guardrails_alert_rate_zero_frames() {
1059 let g = FrameGuardrails::new(GuardrailsConfig::default());
1060 assert!((g.alert_rate() - 0.0).abs() < f64::EPSILON);
1061 }
1062
1063 #[test]
1064 fn guardrails_snapshot_jsonl() {
1065 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1066 g.check_frame(1024, 1);
1067 let snap = g.snapshot();
1068 let line = snap.to_jsonl();
1069 assert!(line.starts_with('{'));
1070 assert!(line.ends_with('}'));
1071 assert!(line.contains("\"memory_bytes\":1024"));
1072 assert!(line.contains("\"queue_depth\":1"));
1073 }
1074
1075 #[test]
1076 fn guardrails_reset_clears_all() {
1077 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1078 g.check_frame(8 * 1024 * 1024, 5);
1079 g.reset();
1080 assert_eq!(g.frames_checked(), 0);
1081 assert_eq!(g.frames_with_alerts(), 0);
1082 assert_eq!(g.memory().peak_bytes(), 0);
1083 assert_eq!(g.queue().peak_depth(), 0);
1084 }
1085
1086 #[test]
1087 fn guardrails_subsystem_access() {
1088 let g = FrameGuardrails::new(GuardrailsConfig::default());
1089 let _ = g.memory().config();
1090 let _ = g.queue().config();
1091 }
1092
1093 #[test]
1096 fn verdict_max_severity_none_when_clear() {
1097 let v = GuardrailVerdict {
1098 alerts: vec![],
1099 queue_action: QueueAction::None,
1100 recommended_level: DegradationLevel::Full,
1101 };
1102 assert!(v.max_severity().is_none());
1103 assert!(v.is_clear());
1104 }
1105
1106 #[test]
1107 fn verdict_max_severity_picks_highest() {
1108 let v = GuardrailVerdict {
1109 alerts: vec![
1110 GuardrailAlert {
1111 kind: GuardrailKind::Memory,
1112 severity: AlertSeverity::Warning,
1113 recommended_level: DegradationLevel::SimpleBorders,
1114 },
1115 GuardrailAlert {
1116 kind: GuardrailKind::QueueDepth,
1117 severity: AlertSeverity::Critical,
1118 recommended_level: DegradationLevel::EssentialOnly,
1119 },
1120 ],
1121 queue_action: QueueAction::None,
1122 recommended_level: DegradationLevel::EssentialOnly,
1123 };
1124 assert_eq!(v.max_severity(), Some(AlertSeverity::Critical));
1125 }
1126
1127 #[test]
1130 fn severity_ordering() {
1131 assert!(AlertSeverity::Warning < AlertSeverity::Critical);
1132 assert!(AlertSeverity::Critical < AlertSeverity::Emergency);
1133 }
1134
1135 #[test]
1138 fn memory_config_small_preset() {
1139 let c = MemoryBudgetConfig::small();
1140 assert!(c.soft_limit_bytes < MemoryBudgetConfig::default().soft_limit_bytes);
1141 }
1142
1143 #[test]
1144 fn memory_config_large_preset() {
1145 let c = MemoryBudgetConfig::large();
1146 assert!(c.soft_limit_bytes > MemoryBudgetConfig::default().soft_limit_bytes);
1147 }
1148
1149 #[test]
1150 fn queue_config_strict_preset() {
1151 let c = QueueConfig::strict();
1152 assert_eq!(c.drop_policy, QueueDropPolicy::Backpressure);
1153 assert!(c.max_depth < QueueConfig::default().max_depth);
1154 }
1155
1156 #[test]
1157 fn queue_config_relaxed_preset() {
1158 let c = QueueConfig::relaxed();
1159 assert!(c.max_depth > QueueConfig::default().max_depth);
1160 }
1161
1162 #[test]
1165 fn buffer_memory_typical_terminal() {
1166 assert_eq!(buffer_memory_bytes(80, 24), 80 * 24 * 16);
1168 }
1169
1170 #[test]
1171 fn buffer_memory_zero_dimension() {
1172 assert_eq!(buffer_memory_bytes(0, 24), 0);
1173 assert_eq!(buffer_memory_bytes(80, 0), 0);
1174 assert_eq!(buffer_memory_bytes(0, 0), 0);
1175 }
1176
1177 #[test]
1178 fn buffer_memory_large_terminal() {
1179 let bytes = buffer_memory_bytes(300, 100);
1181 assert_eq!(bytes, 300 * 100 * 16);
1182 assert_eq!(bytes, 480_000);
1183 }
1184
1185 #[test]
1188 fn queue_drop_policy_default_is_drop_oldest() {
1189 assert_eq!(QueueDropPolicy::default(), QueueDropPolicy::DropOldest);
1190 }
1191
1192 #[test]
1195 fn guardrails_deterministic_for_same_inputs() {
1196 let config = GuardrailsConfig::default();
1197 let mut g1 = FrameGuardrails::new(config.clone());
1198 let mut g2 = FrameGuardrails::new(config);
1199
1200 let inputs = [(1024, 0), (8 * 1024 * 1024, 3), (20 * 1024 * 1024, 10)];
1201 for (mem, queue) in inputs {
1202 let v1 = g1.check_frame(mem, queue);
1203 let v2 = g2.check_frame(mem, queue);
1204 assert_eq!(v1.recommended_level, v2.recommended_level);
1205 assert_eq!(v1.alerts.len(), v2.alerts.len());
1206 assert_eq!(v1.queue_action, v2.queue_action);
1207 }
1208 }
1209}