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 #[must_use]
136 pub fn normalized(self) -> Self {
137 let defaults = Self::default();
138 let soft = if self.soft_limit_bytes == 0 {
139 defaults.soft_limit_bytes
140 } else {
141 self.soft_limit_bytes
142 };
143 let hard = if self.hard_limit_bytes == 0 {
144 defaults.hard_limit_bytes
145 } else {
146 self.hard_limit_bytes
147 }
148 .max(soft);
149 let emergency = if self.emergency_limit_bytes == 0 {
150 defaults.emergency_limit_bytes
151 } else {
152 self.emergency_limit_bytes
153 }
154 .max(hard);
155 Self {
156 soft_limit_bytes: soft,
157 hard_limit_bytes: hard,
158 emergency_limit_bytes: emergency,
159 }
160 }
161}
162
163#[derive(Debug, Clone)]
168pub struct MemoryBudget {
169 config: MemoryBudgetConfig,
170 peak_bytes: usize,
172 current_bytes: usize,
174 soft_violations: u32,
176 hard_violations: u32,
178 emergency_violations: u32,
185}
186
187impl MemoryBudget {
188 #[must_use]
190 pub fn new(config: MemoryBudgetConfig) -> Self {
191 Self {
192 config: config.normalized(),
196 peak_bytes: 0,
197 current_bytes: 0,
198 soft_violations: 0,
199 hard_violations: 0,
200 emergency_violations: 0,
201 }
202 }
203
204 pub fn check(&mut self, current_bytes: usize) -> Option<GuardrailAlert> {
206 self.current_bytes = current_bytes;
207 if current_bytes > self.peak_bytes {
208 self.peak_bytes = current_bytes;
209 }
210
211 if current_bytes >= self.config.emergency_limit_bytes {
212 self.emergency_violations = self.emergency_violations.saturating_add(1);
216 Some(GuardrailAlert {
217 kind: GuardrailKind::Memory,
218 severity: AlertSeverity::Emergency,
219 recommended_level: DegradationLevel::SkipFrame,
220 })
221 } else if current_bytes >= self.config.hard_limit_bytes {
222 self.hard_violations = self.hard_violations.saturating_add(1);
223 Some(GuardrailAlert {
224 kind: GuardrailKind::Memory,
225 severity: AlertSeverity::Critical,
226 recommended_level: DegradationLevel::Skeleton,
227 })
228 } else if current_bytes >= self.config.soft_limit_bytes {
229 self.soft_violations = self.soft_violations.saturating_add(1);
230 Some(GuardrailAlert {
231 kind: GuardrailKind::Memory,
232 severity: AlertSeverity::Warning,
233 recommended_level: DegradationLevel::SimpleBorders,
234 })
235 } else {
236 None
237 }
238 }
239
240 #[inline]
242 #[must_use]
243 pub fn current_bytes(&self) -> usize {
244 self.current_bytes
245 }
246
247 #[inline]
249 #[must_use]
250 pub fn peak_bytes(&self) -> usize {
251 self.peak_bytes
252 }
253
254 #[inline]
256 #[must_use]
257 pub fn usage_fraction(&self) -> f64 {
258 if self.config.soft_limit_bytes == 0 {
259 return 1.0;
260 }
261 self.current_bytes as f64 / self.config.soft_limit_bytes as f64
262 }
263
264 #[inline]
266 #[must_use]
267 pub fn soft_violations(&self) -> u32 {
268 self.soft_violations
269 }
270
271 #[inline]
273 #[must_use]
274 pub fn hard_violations(&self) -> u32 {
275 self.hard_violations
276 }
277
278 #[inline]
280 #[must_use]
281 pub fn emergency_violations(&self) -> u32 {
282 self.emergency_violations
283 }
284
285 #[inline]
287 #[must_use]
288 pub fn config(&self) -> &MemoryBudgetConfig {
289 &self.config
290 }
291
292 pub fn reset(&mut self) {
294 self.peak_bytes = 0;
295 self.current_bytes = 0;
296 self.soft_violations = 0;
297 self.hard_violations = 0;
298 self.emergency_violations = 0;
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
308pub enum QueueDropPolicy {
309 #[default]
311 DropOldest,
312 DropNewest,
314 Backpressure,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub struct QueueConfig {
321 pub warn_depth: u32,
324 pub max_depth: u32,
327 pub emergency_depth: u32,
333 pub drop_policy: QueueDropPolicy,
334}
335
336impl Default for QueueConfig {
337 fn default() -> Self {
338 Self {
339 warn_depth: 3,
340 max_depth: 8,
341 emergency_depth: 16,
342 drop_policy: QueueDropPolicy::DropOldest,
343 }
344 }
345}
346
347impl QueueConfig {
348 #[must_use]
350 pub fn strict() -> Self {
351 Self {
352 warn_depth: 2,
353 max_depth: 4,
354 emergency_depth: 8,
355 drop_policy: QueueDropPolicy::Backpressure,
356 }
357 }
358
359 #[must_use]
361 pub fn relaxed() -> Self {
362 Self {
363 warn_depth: 8,
364 max_depth: 16,
365 emergency_depth: 32,
366 drop_policy: QueueDropPolicy::DropOldest,
367 }
368 }
369}
370
371#[derive(Debug, Clone)]
376pub struct QueueGuardrails {
377 config: QueueConfig,
378 peak_depth: u32,
380 current_depth: u32,
382 total_drops: u64,
384 total_backpressure_events: u64,
386}
387
388impl QueueGuardrails {
389 #[must_use]
398 pub fn new(config: QueueConfig) -> Self {
399 let warn_depth = config.warn_depth.max(1);
402 let max_depth = config.max_depth.max(warn_depth);
403 let emergency_depth = config.emergency_depth.max(max_depth);
404 Self {
405 config: QueueConfig {
406 warn_depth,
407 max_depth,
408 emergency_depth,
409 drop_policy: config.drop_policy,
410 },
411 peak_depth: 0,
412 current_depth: 0,
413 total_drops: 0,
414 total_backpressure_events: 0,
415 }
416 }
417
418 pub fn check(&mut self, current_depth: u32) -> (Option<GuardrailAlert>, QueueAction) {
423 self.current_depth = current_depth;
424 if current_depth > self.peak_depth {
425 self.peak_depth = current_depth;
426 }
427
428 if current_depth >= self.config.emergency_depth {
429 let action = match self.config.drop_policy {
430 QueueDropPolicy::DropOldest => {
431 let excess = current_depth - 1; self.total_drops = self.total_drops.saturating_add(excess as u64);
433 QueueAction::DropOldest(excess)
434 }
435 QueueDropPolicy::DropNewest => {
436 let excess = current_depth - 1; self.total_drops = self.total_drops.saturating_add(excess as u64);
438 QueueAction::DropNewest(excess)
439 }
440 QueueDropPolicy::Backpressure => {
441 self.total_backpressure_events =
442 self.total_backpressure_events.saturating_add(1);
443 QueueAction::Backpressure
444 }
445 };
446 (
447 Some(GuardrailAlert {
448 kind: GuardrailKind::QueueDepth,
449 severity: AlertSeverity::Emergency,
450 recommended_level: DegradationLevel::SkipFrame,
451 }),
452 action,
453 )
454 } else if current_depth >= self.config.max_depth {
455 let action = match self.config.drop_policy {
456 QueueDropPolicy::DropOldest => {
457 let excess = current_depth.saturating_sub(self.config.warn_depth);
458 if excess == 0 {
459 QueueAction::None
463 } else {
464 self.total_drops = self.total_drops.saturating_add(u64::from(excess));
465 QueueAction::DropOldest(excess)
466 }
467 }
468 QueueDropPolicy::DropNewest => {
469 let excess = current_depth.saturating_sub(self.config.warn_depth);
470 if excess == 0 {
471 QueueAction::None
472 } else {
473 self.total_drops = self.total_drops.saturating_add(u64::from(excess));
474 QueueAction::DropNewest(excess)
475 }
476 }
477 QueueDropPolicy::Backpressure => {
478 self.total_backpressure_events =
479 self.total_backpressure_events.saturating_add(1);
480 QueueAction::Backpressure
481 }
482 };
483 (
484 Some(GuardrailAlert {
485 kind: GuardrailKind::QueueDepth,
486 severity: AlertSeverity::Critical,
487 recommended_level: DegradationLevel::EssentialOnly,
488 }),
489 action,
490 )
491 } else if current_depth >= self.config.warn_depth {
492 (
493 Some(GuardrailAlert {
494 kind: GuardrailKind::QueueDepth,
495 severity: AlertSeverity::Warning,
496 recommended_level: DegradationLevel::SimpleBorders,
497 }),
498 QueueAction::None,
499 )
500 } else {
501 (None, QueueAction::None)
502 }
503 }
504
505 #[inline]
507 #[must_use]
508 pub fn current_depth(&self) -> u32 {
509 self.current_depth
510 }
511
512 #[inline]
514 #[must_use]
515 pub fn peak_depth(&self) -> u32 {
516 self.peak_depth
517 }
518
519 #[inline]
521 #[must_use]
522 pub fn total_drops(&self) -> u64 {
523 self.total_drops
524 }
525
526 #[inline]
528 #[must_use]
529 pub fn total_backpressure_events(&self) -> u64 {
530 self.total_backpressure_events
531 }
532
533 #[inline]
535 #[must_use]
536 pub fn config(&self) -> &QueueConfig {
537 &self.config
538 }
539
540 pub fn reset(&mut self) {
542 self.peak_depth = 0;
543 self.current_depth = 0;
544 self.total_drops = 0;
545 self.total_backpressure_events = 0;
546 }
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub enum QueueAction {
552 None,
554 DropOldest(u32),
556 DropNewest(u32),
558 Backpressure,
560}
561
562impl QueueAction {
563 #[inline]
565 #[must_use]
566 pub fn drops_frames(self) -> bool {
567 matches!(self, Self::DropOldest(_) | Self::DropNewest(_))
568 }
569}
570
571#[derive(Debug, Clone, Default)]
577pub struct GuardrailsConfig {
578 pub memory: MemoryBudgetConfig,
580 pub queue: QueueConfig,
582}
583
584#[derive(Debug, Clone)]
586pub struct GuardrailVerdict {
587 pub alerts: Vec<GuardrailAlert>,
589 pub queue_action: QueueAction,
591 pub recommended_level: DegradationLevel,
593}
594
595impl GuardrailVerdict {
596 #[inline]
598 #[must_use]
599 pub fn should_drop_frame(&self) -> bool {
600 self.recommended_level >= DegradationLevel::SkipFrame
601 }
602
603 #[inline]
605 #[must_use]
606 pub fn should_degrade(&self) -> bool {
607 self.recommended_level > DegradationLevel::Full
608 && self.recommended_level < DegradationLevel::SkipFrame
609 }
610
611 #[inline]
613 #[must_use]
614 pub fn is_clear(&self) -> bool {
615 self.alerts.is_empty()
616 }
617
618 #[must_use]
620 pub fn max_severity(&self) -> Option<AlertSeverity> {
621 self.alerts.iter().map(|a| a.severity).max()
622 }
623}
624
625#[derive(Debug, Clone)]
630pub struct FrameGuardrails {
631 memory: MemoryBudget,
632 queue: QueueGuardrails,
633 frames_checked: u64,
635 frames_with_alerts: u64,
637}
638
639impl FrameGuardrails {
640 #[must_use]
642 pub fn new(config: GuardrailsConfig) -> Self {
643 Self {
644 memory: MemoryBudget::new(config.memory),
645 queue: QueueGuardrails::new(config.queue),
646 frames_checked: 0,
647 frames_with_alerts: 0,
648 }
649 }
650
651 pub fn check_frame(&mut self, memory_bytes: usize, queue_depth: u32) -> GuardrailVerdict {
656 self.frames_checked = self.frames_checked.saturating_add(1);
657
658 let mut alerts = Vec::new();
659 let mut max_level = DegradationLevel::Full;
660
661 if let Some(alert) = self.memory.check(memory_bytes) {
663 if alert.recommended_level > max_level {
664 max_level = alert.recommended_level;
665 }
666 alerts.push(alert);
667 }
668
669 let (queue_alert, queue_action) = self.queue.check(queue_depth);
671 if let Some(alert) = queue_alert {
672 if alert.recommended_level > max_level {
673 max_level = alert.recommended_level;
674 }
675 alerts.push(alert);
676 }
677
678 if !alerts.is_empty() {
679 self.frames_with_alerts = self.frames_with_alerts.saturating_add(1);
680 }
681
682 GuardrailVerdict {
683 alerts,
684 queue_action,
685 recommended_level: max_level,
686 }
687 }
688
689 #[inline]
691 #[must_use]
692 pub fn memory(&self) -> &MemoryBudget {
693 &self.memory
694 }
695
696 #[inline]
698 #[must_use]
699 pub fn queue(&self) -> &QueueGuardrails {
700 &self.queue
701 }
702
703 #[inline]
705 #[must_use]
706 pub fn frames_checked(&self) -> u64 {
707 self.frames_checked
708 }
709
710 #[inline]
712 #[must_use]
713 pub fn frames_with_alerts(&self) -> u64 {
714 self.frames_with_alerts
715 }
716
717 #[inline]
719 #[must_use]
720 pub fn alert_rate(&self) -> f64 {
721 if self.frames_checked == 0 {
722 return 0.0;
723 }
724 self.frames_with_alerts as f64 / self.frames_checked as f64
725 }
726
727 #[must_use]
729 pub fn snapshot(&self) -> GuardrailSnapshot {
730 GuardrailSnapshot {
731 memory_bytes: self.memory.current_bytes(),
732 memory_peak_bytes: self.memory.peak_bytes(),
733 memory_usage_fraction: self.memory.usage_fraction(),
734 memory_soft_violations: self.memory.soft_violations(),
735 memory_hard_violations: self.memory.hard_violations(),
736 memory_emergency_violations: self.memory.emergency_violations(),
737 queue_depth: self.queue.current_depth(),
738 queue_peak_depth: self.queue.peak_depth(),
739 queue_total_drops: self.queue.total_drops(),
740 queue_total_backpressure: self.queue.total_backpressure_events(),
741 frames_checked: self.frames_checked,
742 frames_with_alerts: self.frames_with_alerts,
743 }
744 }
745
746 pub fn reset(&mut self) {
748 self.memory.reset();
749 self.queue.reset();
750 self.frames_checked = 0;
751 self.frames_with_alerts = 0;
752 }
753}
754
755#[derive(Debug, Clone, Copy, PartialEq)]
760pub struct GuardrailSnapshot {
761 pub memory_bytes: usize,
763 pub memory_peak_bytes: usize,
765 pub memory_usage_fraction: f64,
767 pub memory_soft_violations: u32,
769 pub memory_hard_violations: u32,
771 pub memory_emergency_violations: u32,
775 pub queue_depth: u32,
777 pub queue_peak_depth: u32,
779 pub queue_total_drops: u64,
781 pub queue_total_backpressure: u64,
783 pub frames_checked: u64,
785 pub frames_with_alerts: u64,
787}
788
789impl GuardrailSnapshot {
790 pub fn to_jsonl(&self) -> String {
792 format!(
793 concat!(
794 r#"{{"memory_bytes":{},"memory_peak":{},"memory_frac":{:.4},"#,
795 r#""mem_soft_violations":{},"mem_hard_violations":{},"mem_emergency_violations":{},"#,
796 r#""queue_depth":{},"queue_peak":{},"queue_drops":{},"#,
797 r#""queue_backpressure":{},"frames_checked":{},"frames_alerted":{}}}"#,
798 ),
799 self.memory_bytes,
800 self.memory_peak_bytes,
801 self.memory_usage_fraction,
802 self.memory_soft_violations,
803 self.memory_hard_violations,
804 self.memory_emergency_violations,
805 self.queue_depth,
806 self.queue_peak_depth,
807 self.queue_total_drops,
808 self.queue_total_backpressure,
809 self.frames_checked,
810 self.frames_with_alerts,
811 )
812 }
813}
814
815pub const CELL_SIZE_BYTES: usize = 16;
821
822#[inline]
826#[must_use]
827pub fn buffer_memory_bytes(width: u16, height: u16) -> usize {
828 width as usize * height as usize * CELL_SIZE_BYTES
829}
830
831#[cfg(test)]
836mod tests {
837 use super::*;
838
839 #[test]
840 fn zero_and_inverted_configs_are_normalized() {
841 let zero = MemoryBudget::new(MemoryBudgetConfig {
845 soft_limit_bytes: 0,
846 hard_limit_bytes: 0,
847 emergency_limit_bytes: 0,
848 });
849 assert!(zero.config.soft_limit_bytes > 0);
850 assert!(zero.config.soft_limit_bytes <= zero.config.hard_limit_bytes);
851 assert!(zero.config.hard_limit_bytes <= zero.config.emergency_limit_bytes);
852
853 let inverted = MemoryBudget::new(MemoryBudgetConfig {
854 soft_limit_bytes: 32 * 1024 * 1024,
855 hard_limit_bytes: 8 * 1024 * 1024,
856 emergency_limit_bytes: 16 * 1024 * 1024,
857 });
858 assert!(inverted.config.soft_limit_bytes <= inverted.config.hard_limit_bytes);
859 assert!(inverted.config.hard_limit_bytes <= inverted.config.emergency_limit_bytes);
860 }
861
862 #[test]
865 fn memory_below_soft_no_alert() {
866 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
867 assert!(mb.check(1024).is_none());
868 assert_eq!(mb.current_bytes(), 1024);
869 }
870
871 #[test]
872 fn memory_at_soft_limit_warns() {
873 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
874 let alert = mb.check(8 * 1024 * 1024).unwrap();
875 assert_eq!(alert.kind, GuardrailKind::Memory);
876 assert_eq!(alert.severity, AlertSeverity::Warning);
877 assert_eq!(alert.recommended_level, DegradationLevel::SimpleBorders);
878 }
879
880 #[test]
881 fn memory_at_hard_limit_critical() {
882 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
883 let alert = mb.check(16 * 1024 * 1024).unwrap();
884 assert_eq!(alert.severity, AlertSeverity::Critical);
885 assert_eq!(alert.recommended_level, DegradationLevel::Skeleton);
886 }
887
888 #[test]
889 fn memory_at_emergency_limit() {
890 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
891 let alert = mb.check(32 * 1024 * 1024).unwrap();
892 assert_eq!(alert.severity, AlertSeverity::Emergency);
893 assert_eq!(alert.recommended_level, DegradationLevel::SkipFrame);
894 }
895
896 #[test]
897 fn memory_peak_tracking() {
898 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
899 mb.check(1000);
900 mb.check(5000);
901 mb.check(3000);
902 assert_eq!(mb.peak_bytes(), 5000);
903 assert_eq!(mb.current_bytes(), 3000);
904 }
905
906 #[test]
907 fn memory_violation_counts() {
908 let config = MemoryBudgetConfig {
909 soft_limit_bytes: 100,
910 hard_limit_bytes: 200,
911 emergency_limit_bytes: 300,
912 };
913 let mut mb = MemoryBudget::new(config);
914 mb.check(50); mb.check(150); mb.check(150); mb.check(250); mb.check(350); assert_eq!(mb.soft_violations(), 2);
920 assert_eq!(mb.hard_violations(), 1);
921 assert_eq!(
922 mb.emergency_violations(),
923 1,
924 "emergency shedding must be counted separately from hard hits (bd-1za0z F5)"
925 );
926 }
927
928 #[test]
929 fn memory_usage_fraction() {
930 let config = MemoryBudgetConfig {
931 soft_limit_bytes: 1000,
932 hard_limit_bytes: 2000,
933 emergency_limit_bytes: 3000,
934 };
935 let mut mb = MemoryBudget::new(config);
936 mb.check(500);
937 assert!((mb.usage_fraction() - 0.5).abs() < f64::EPSILON);
938 }
939
940 #[test]
941 fn memory_usage_fraction_zero_limit() {
942 let config = MemoryBudgetConfig {
947 soft_limit_bytes: 0,
948 hard_limit_bytes: 0,
949 emergency_limit_bytes: 0,
950 };
951 let mut mb = MemoryBudget::new(config);
952 mb.check(100);
953 assert!(mb.usage_fraction() > 0.0);
954 assert!(mb.usage_fraction() < 0.001);
955 }
956
957 #[test]
958 fn memory_reset_clears_state() {
959 let mut mb = MemoryBudget::new(MemoryBudgetConfig::default());
960 mb.check(10 * 1024 * 1024); assert!(mb.soft_violations() > 0);
962 mb.reset();
963 assert_eq!(mb.peak_bytes(), 0);
964 assert_eq!(mb.current_bytes(), 0);
965 assert_eq!(mb.soft_violations(), 0);
966 assert_eq!(mb.hard_violations(), 0);
967 }
968
969 #[test]
970 fn memory_config_accessors() {
971 let config = MemoryBudgetConfig::small();
972 let mb = MemoryBudget::new(config);
973 assert_eq!(mb.config().soft_limit_bytes, 2 * 1024 * 1024);
974 }
975
976 #[test]
979 fn queue_below_warn_no_alert() {
980 let mut qg = QueueGuardrails::new(QueueConfig::default());
981 let (alert, action) = qg.check(1);
982 assert!(alert.is_none());
983 assert_eq!(action, QueueAction::None);
984 }
985
986 #[test]
987 fn queue_at_warn_depth() {
988 let mut qg = QueueGuardrails::new(QueueConfig::default());
989 let (alert, action) = qg.check(3);
990 assert_eq!(alert.unwrap().severity, AlertSeverity::Warning);
991 assert_eq!(action, QueueAction::None); }
993
994 #[test]
995 fn queue_at_max_depth_drop_oldest() {
996 let config = QueueConfig {
997 drop_policy: QueueDropPolicy::DropOldest,
998 ..QueueConfig::default()
999 };
1000 let mut qg = QueueGuardrails::new(config);
1001 let (alert, action) = qg.check(8);
1002 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
1003 assert!(action.drops_frames());
1004 }
1005
1006 #[test]
1007 fn queue_at_max_depth_drop_newest() {
1008 let config = QueueConfig {
1009 drop_policy: QueueDropPolicy::DropNewest,
1010 ..QueueConfig::default()
1011 };
1012 let mut qg = QueueGuardrails::new(config);
1013 let (alert, action) = qg.check(8);
1014 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
1015 assert_eq!(action, QueueAction::DropNewest(5));
1016 }
1017
1018 #[test]
1019 fn queue_at_max_depth_backpressure() {
1020 let config = QueueConfig {
1021 drop_policy: QueueDropPolicy::Backpressure,
1022 ..QueueConfig::default()
1023 };
1024 let mut qg = QueueGuardrails::new(config);
1025 let (alert, action) = qg.check(8);
1026 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
1027 assert_eq!(action, QueueAction::Backpressure);
1028 }
1029
1030 #[test]
1031 fn queue_emergency_drops_to_latest() {
1032 let mut qg = QueueGuardrails::new(QueueConfig::default());
1033 let (alert, action) = qg.check(16);
1034 assert_eq!(alert.unwrap().severity, AlertSeverity::Emergency);
1035 assert_eq!(action, QueueAction::DropOldest(15));
1037 }
1038
1039 #[test]
1040 fn queue_peak_tracking() {
1041 let mut qg = QueueGuardrails::new(QueueConfig::default());
1042 qg.check(2);
1043 qg.check(5);
1044 qg.check(1);
1045 assert_eq!(qg.peak_depth(), 5);
1046 assert_eq!(qg.current_depth(), 1);
1047 }
1048
1049 #[test]
1050 fn queue_drop_counting() {
1051 let mut qg = QueueGuardrails::new(QueueConfig::default());
1052 qg.check(8); assert!(qg.total_drops() > 0);
1054 }
1055
1056 #[test]
1057 fn queue_backpressure_counting() {
1058 let config = QueueConfig::strict();
1059 let mut qg = QueueGuardrails::new(config);
1060 qg.check(4); assert!(qg.total_backpressure_events() > 0);
1062 }
1063
1064 #[test]
1065 fn queue_reset_clears_state() {
1066 let mut qg = QueueGuardrails::new(QueueConfig::default());
1067 qg.check(10);
1068 qg.reset();
1069 assert_eq!(qg.peak_depth(), 0);
1070 assert_eq!(qg.current_depth(), 0);
1071 assert_eq!(qg.total_drops(), 0);
1072 }
1073
1074 #[test]
1075 fn queue_config_accessors() {
1076 let config = QueueConfig::relaxed();
1077 let qg = QueueGuardrails::new(config);
1078 assert_eq!(qg.config().max_depth, 16);
1079 }
1080
1081 #[test]
1084 fn queue_config_normalized_at_construction() {
1085 let config = QueueConfig {
1086 warn_depth: 10,
1087 max_depth: 8,
1088 emergency_depth: 6,
1089 drop_policy: QueueDropPolicy::DropOldest,
1090 };
1091 let qg = QueueGuardrails::new(config);
1092 let cfg = qg.config();
1093 assert_eq!(cfg.warn_depth, 10);
1094 assert_eq!(cfg.max_depth, 10, "max must be raised to warn");
1095 assert_eq!(cfg.emergency_depth, 10, "emergency must be raised to max");
1096
1097 let zeroed = QueueGuardrails::new(QueueConfig {
1098 warn_depth: 0,
1099 ..QueueConfig::default()
1100 });
1101 assert_eq!(
1102 zeroed.config().warn_depth,
1103 1,
1104 "warn_depth 0 would flag an idle queue"
1105 );
1106 }
1107
1108 #[test]
1111 fn queue_collapsed_thresholds_yield_noop_action_not_zero_drop() {
1112 let config = QueueConfig {
1113 warn_depth: 8,
1114 max_depth: 8,
1115 emergency_depth: 16,
1116 drop_policy: QueueDropPolicy::DropOldest,
1117 };
1118 let mut qg = QueueGuardrails::new(config);
1119 let (alert, action) = qg.check(8);
1120 assert_eq!(alert.unwrap().severity, AlertSeverity::Critical);
1121 assert_eq!(
1122 action,
1123 QueueAction::None,
1124 "excess 0 must not be reported as a real drop action"
1125 );
1126 assert_eq!(qg.total_drops(), 0, "no-op action must not count drops");
1127 }
1128
1129 #[test]
1133 fn queue_emergency_drop_newest_keeps_oldest() {
1134 let config = QueueConfig {
1135 drop_policy: QueueDropPolicy::DropNewest,
1136 ..QueueConfig::default()
1137 };
1138 let mut qg = QueueGuardrails::new(config);
1139 let (alert, action) = qg.check(16);
1140 assert_eq!(alert.unwrap().severity, AlertSeverity::Emergency);
1141 assert_eq!(action, QueueAction::DropNewest(15));
1142 }
1143
1144 #[test]
1147 fn queue_action_drops_frames() {
1148 assert!(!QueueAction::None.drops_frames());
1149 assert!(QueueAction::DropOldest(3).drops_frames());
1150 assert!(QueueAction::DropNewest(1).drops_frames());
1151 assert!(!QueueAction::Backpressure.drops_frames());
1152 }
1153
1154 #[test]
1157 fn guardrails_clear_when_healthy() {
1158 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1159 let v = g.check_frame(1024, 0);
1160 assert!(v.is_clear());
1161 assert_eq!(v.recommended_level, DegradationLevel::Full);
1162 assert_eq!(v.queue_action, QueueAction::None);
1163 }
1164
1165 #[test]
1166 fn guardrails_memory_alert_propagates() {
1167 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1168 let v = g.check_frame(8 * 1024 * 1024, 0);
1169 assert!(!v.is_clear());
1170 assert_eq!(v.alerts.len(), 1);
1171 assert_eq!(v.alerts[0].kind, GuardrailKind::Memory);
1172 assert!(v.should_degrade());
1173 assert!(!v.should_drop_frame());
1174 }
1175
1176 #[test]
1177 fn guardrails_queue_alert_propagates() {
1178 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1179 let v = g.check_frame(0, 8);
1180 assert!(!v.is_clear());
1181 assert!(v.alerts.iter().any(|a| a.kind == GuardrailKind::QueueDepth));
1182 }
1183
1184 #[test]
1185 fn guardrails_both_alerts_combine() {
1186 let config = GuardrailsConfig {
1187 memory: MemoryBudgetConfig {
1188 soft_limit_bytes: 100,
1189 hard_limit_bytes: 200,
1190 emergency_limit_bytes: 300,
1191 },
1192 queue: QueueConfig {
1193 warn_depth: 1,
1194 max_depth: 2,
1195 emergency_depth: 3,
1196 drop_policy: QueueDropPolicy::DropOldest,
1197 },
1198 };
1199 let mut g = FrameGuardrails::new(config);
1200 let v = g.check_frame(150, 2);
1201 assert_eq!(v.alerts.len(), 2);
1202 assert!(v.recommended_level >= DegradationLevel::SimpleBorders);
1204 }
1205
1206 #[test]
1207 fn guardrails_emergency_recommends_skip() {
1208 let config = GuardrailsConfig {
1209 memory: MemoryBudgetConfig {
1210 soft_limit_bytes: 100,
1211 hard_limit_bytes: 200,
1212 emergency_limit_bytes: 300,
1213 },
1214 queue: QueueConfig::default(),
1215 };
1216 let mut g = FrameGuardrails::new(config);
1217 let v = g.check_frame(300, 0);
1218 assert!(v.should_drop_frame());
1219 }
1220
1221 #[test]
1222 fn guardrails_frame_counting() {
1223 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1224 g.check_frame(0, 0);
1225 g.check_frame(0, 0);
1226 g.check_frame(8 * 1024 * 1024, 0); assert_eq!(g.frames_checked(), 3);
1228 assert_eq!(g.frames_with_alerts(), 1);
1229 }
1230
1231 #[test]
1232 fn guardrails_alert_rate() {
1233 let config = GuardrailsConfig {
1234 memory: MemoryBudgetConfig {
1235 soft_limit_bytes: 100,
1236 hard_limit_bytes: 200,
1237 emergency_limit_bytes: 300,
1238 },
1239 queue: QueueConfig::default(),
1240 };
1241 let mut g = FrameGuardrails::new(config);
1242 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);
1247 }
1248
1249 #[test]
1250 fn guardrails_alert_rate_zero_frames() {
1251 let g = FrameGuardrails::new(GuardrailsConfig::default());
1252 assert!((g.alert_rate() - 0.0).abs() < f64::EPSILON);
1253 }
1254
1255 #[test]
1256 fn guardrails_snapshot_jsonl() {
1257 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1258 g.check_frame(1024, 1);
1259 let snap = g.snapshot();
1260 let line = snap.to_jsonl();
1261 assert!(line.starts_with('{'));
1262 assert!(line.ends_with('}'));
1263 assert!(line.contains("\"memory_bytes\":1024"));
1264 assert!(line.contains("\"queue_depth\":1"));
1265 }
1266
1267 #[test]
1268 fn guardrails_reset_clears_all() {
1269 let mut g = FrameGuardrails::new(GuardrailsConfig::default());
1270 g.check_frame(8 * 1024 * 1024, 5);
1271 g.reset();
1272 assert_eq!(g.frames_checked(), 0);
1273 assert_eq!(g.frames_with_alerts(), 0);
1274 assert_eq!(g.memory().peak_bytes(), 0);
1275 assert_eq!(g.queue().peak_depth(), 0);
1276 }
1277
1278 #[test]
1279 fn guardrails_subsystem_access() {
1280 let g = FrameGuardrails::new(GuardrailsConfig::default());
1281 let _ = g.memory().config();
1282 let _ = g.queue().config();
1283 }
1284
1285 #[test]
1288 fn verdict_max_severity_none_when_clear() {
1289 let v = GuardrailVerdict {
1290 alerts: vec![],
1291 queue_action: QueueAction::None,
1292 recommended_level: DegradationLevel::Full,
1293 };
1294 assert!(v.max_severity().is_none());
1295 assert!(v.is_clear());
1296 }
1297
1298 #[test]
1299 fn verdict_max_severity_picks_highest() {
1300 let v = GuardrailVerdict {
1301 alerts: vec![
1302 GuardrailAlert {
1303 kind: GuardrailKind::Memory,
1304 severity: AlertSeverity::Warning,
1305 recommended_level: DegradationLevel::SimpleBorders,
1306 },
1307 GuardrailAlert {
1308 kind: GuardrailKind::QueueDepth,
1309 severity: AlertSeverity::Critical,
1310 recommended_level: DegradationLevel::EssentialOnly,
1311 },
1312 ],
1313 queue_action: QueueAction::None,
1314 recommended_level: DegradationLevel::EssentialOnly,
1315 };
1316 assert_eq!(v.max_severity(), Some(AlertSeverity::Critical));
1317 }
1318
1319 #[test]
1322 fn severity_ordering() {
1323 assert!(AlertSeverity::Warning < AlertSeverity::Critical);
1324 assert!(AlertSeverity::Critical < AlertSeverity::Emergency);
1325 }
1326
1327 #[test]
1330 fn memory_config_small_preset() {
1331 let c = MemoryBudgetConfig::small();
1332 assert!(c.soft_limit_bytes < MemoryBudgetConfig::default().soft_limit_bytes);
1333 }
1334
1335 #[test]
1336 fn memory_config_large_preset() {
1337 let c = MemoryBudgetConfig::large();
1338 assert!(c.soft_limit_bytes > MemoryBudgetConfig::default().soft_limit_bytes);
1339 }
1340
1341 #[test]
1342 fn queue_config_strict_preset() {
1343 let c = QueueConfig::strict();
1344 assert_eq!(c.drop_policy, QueueDropPolicy::Backpressure);
1345 assert!(c.max_depth < QueueConfig::default().max_depth);
1346 }
1347
1348 #[test]
1349 fn queue_config_relaxed_preset() {
1350 let c = QueueConfig::relaxed();
1351 assert!(c.max_depth > QueueConfig::default().max_depth);
1352 }
1353
1354 #[test]
1357 fn buffer_memory_typical_terminal() {
1358 assert_eq!(buffer_memory_bytes(80, 24), 80 * 24 * 16);
1360 }
1361
1362 #[test]
1363 fn buffer_memory_zero_dimension() {
1364 assert_eq!(buffer_memory_bytes(0, 24), 0);
1365 assert_eq!(buffer_memory_bytes(80, 0), 0);
1366 assert_eq!(buffer_memory_bytes(0, 0), 0);
1367 }
1368
1369 #[test]
1370 fn buffer_memory_large_terminal() {
1371 let bytes = buffer_memory_bytes(300, 100);
1373 assert_eq!(bytes, 300 * 100 * 16);
1374 assert_eq!(bytes, 480_000);
1375 }
1376
1377 #[test]
1380 fn queue_drop_policy_default_is_drop_oldest() {
1381 assert_eq!(QueueDropPolicy::default(), QueueDropPolicy::DropOldest);
1382 }
1383
1384 #[test]
1387 fn guardrails_deterministic_for_same_inputs() {
1388 let config = GuardrailsConfig::default();
1389 let mut g1 = FrameGuardrails::new(config.clone());
1390 let mut g2 = FrameGuardrails::new(config);
1391
1392 let inputs = [(1024, 0), (8 * 1024 * 1024, 3), (20 * 1024 * 1024, 10)];
1393 for (mem, queue) in inputs {
1394 let v1 = g1.check_frame(mem, queue);
1395 let v2 = g2.check_frame(mem, queue);
1396 assert_eq!(v1.recommended_level, v2.recommended_level);
1397 assert_eq!(v1.alerts.len(), v2.alerts.len());
1398 assert_eq!(v1.queue_action, v2.queue_action);
1399 }
1400 }
1401}