1use crate::store::{BlockKey, ReconstructPolicy, Tier};
25
26#[derive(Clone, Debug)]
35pub struct WitnessRecord {
36 pub timestamp: u64,
38 pub event: WitnessEvent,
40}
41
42#[derive(Clone, Debug)]
47pub enum WitnessEvent {
48 Access {
50 key: BlockKey,
51 score: f32,
52 tier: Tier,
53 },
54 TierChange {
56 key: BlockKey,
57 from_tier: Tier,
58 to_tier: Tier,
59 score: f32,
60 reason: TierChangeReason,
61 },
62 Eviction {
64 key: BlockKey,
65 score: f32,
66 bytes_freed: usize,
67 },
68 Maintenance {
70 upgrades: u32,
71 downgrades: u32,
72 evictions: u32,
73 bytes_freed: usize,
74 budget_remaining_bytes: u32,
75 budget_remaining_ops: u32,
76 },
77 Compaction {
79 key: BlockKey,
80 chain_len_before: u8,
81 },
82 ChecksumFailure {
84 key: BlockKey,
85 expected: u32,
86 actual: u32,
87 },
88 Reconstruction {
90 key: BlockKey,
91 policy: ReconstructPolicy,
92 success: bool,
93 },
94}
95
96#[derive(Clone, Debug, PartialEq, Eq)]
98pub enum TierChangeReason {
99 ScoreUpgrade,
101 ScoreDowngrade,
103 ByteCapPressure,
105 ManualOverride,
107}
108
109#[derive(Clone, Debug, Default)]
118pub struct StoreMetrics {
119 pub total_blocks: u64,
121 pub tier0_blocks: u64,
123 pub tier1_blocks: u64,
125 pub tier2_blocks: u64,
127 pub tier3_blocks: u64,
129 pub tier1_bytes: u64,
131 pub tier2_bytes: u64,
133 pub tier3_bytes: u64,
135 pub total_reads: u64,
137 pub total_writes: u64,
139 pub total_evictions: u64,
141 pub total_upgrades: u64,
143 pub total_downgrades: u64,
145 pub total_reconstructions: u64,
147 pub total_checksum_failures: u64,
149 pub total_compactions: u64,
151 pub tier_flips_last_minute: f32,
153 pub avg_score_tier1: f32,
155 pub avg_score_tier2: f32,
157 pub avg_score_tier3: f32,
159}
160
161impl StoreMetrics {
162 pub fn new() -> Self {
164 Self::default()
165 }
166
167 pub fn compression_ratio(&self) -> f32 {
181 let stored = self.total_stored_bytes();
182 if stored == 0 {
183 return 0.0;
184 }
185 let raw_estimate = (self.tier1_bytes as f64 * 4.0)
186 + (self.tier2_bytes as f64 * 5.5)
187 + (self.tier3_bytes as f64 * 10.67);
188 raw_estimate as f32 / stored as f32
189 }
190
191 pub fn total_stored_bytes(&self) -> u64 {
196 self.tier1_bytes + self.tier2_bytes + self.tier3_bytes
197 }
198
199 pub fn format_report(&self) -> String {
201 let mut s = String::with_capacity(512);
202 s.push_str("=== Temporal Tensor Store Report ===\n");
203 s.push_str(&format_line("Total blocks", self.total_blocks));
204 s.push_str(&format_line(" Tier0 (raw)", self.tier0_blocks));
205 s.push_str(&format_line(" Tier1 (hot)", self.tier1_blocks));
206 s.push_str(&format_line(" Tier2 (warm)", self.tier2_blocks));
207 s.push_str(&format_line(" Tier3 (cold)", self.tier3_blocks));
208 s.push_str("--- Storage ---\n");
209 s.push_str(&format_line("Tier1 bytes", self.tier1_bytes));
210 s.push_str(&format_line("Tier2 bytes", self.tier2_bytes));
211 s.push_str(&format_line("Tier3 bytes", self.tier3_bytes));
212 s.push_str(&format_line("Total stored", self.total_stored_bytes()));
213 s.push_str(&format!("Compression ratio: {:.2}x\n", self.compression_ratio()));
214 s.push_str("--- Operations ---\n");
215 s.push_str(&format_line("Reads", self.total_reads));
216 s.push_str(&format_line("Writes", self.total_writes));
217 s.push_str(&format_line("Evictions", self.total_evictions));
218 s.push_str(&format_line("Upgrades", self.total_upgrades));
219 s.push_str(&format_line("Downgrades", self.total_downgrades));
220 s.push_str(&format_line("Reconstructions", self.total_reconstructions));
221 s.push_str(&format_line("Compactions", self.total_compactions));
222 s.push_str(&format_line("Checksum failures", self.total_checksum_failures));
223 s.push_str(&format!("Tier flip rate: {:.4}/block/min\n", self.tier_flips_last_minute));
224 s
225 }
226
227 pub fn format_json(&self) -> String {
229 format!(
230 concat!(
231 "{{",
232 "\"total_blocks\":{},",
233 "\"tier0_blocks\":{},",
234 "\"tier1_blocks\":{},",
235 "\"tier2_blocks\":{},",
236 "\"tier3_blocks\":{},",
237 "\"tier1_bytes\":{},",
238 "\"tier2_bytes\":{},",
239 "\"tier3_bytes\":{},",
240 "\"total_reads\":{},",
241 "\"total_writes\":{},",
242 "\"total_evictions\":{},",
243 "\"total_upgrades\":{},",
244 "\"total_downgrades\":{},",
245 "\"total_reconstructions\":{},",
246 "\"total_checksum_failures\":{},",
247 "\"total_compactions\":{},",
248 "\"compression_ratio\":{:.4},",
249 "\"tier_flips_last_minute\":{:.4},",
250 "\"avg_score_tier1\":{:.4},",
251 "\"avg_score_tier2\":{:.4},",
252 "\"avg_score_tier3\":{:.4}",
253 "}}"
254 ),
255 self.total_blocks,
256 self.tier0_blocks,
257 self.tier1_blocks,
258 self.tier2_blocks,
259 self.tier3_blocks,
260 self.tier1_bytes,
261 self.tier2_bytes,
262 self.tier3_bytes,
263 self.total_reads,
264 self.total_writes,
265 self.total_evictions,
266 self.total_upgrades,
267 self.total_downgrades,
268 self.total_reconstructions,
269 self.total_checksum_failures,
270 self.total_compactions,
271 self.compression_ratio(),
272 self.tier_flips_last_minute,
273 self.avg_score_tier1,
274 self.avg_score_tier2,
275 self.avg_score_tier3,
276 )
277 }
278
279 pub fn health_check(&self) -> StoreHealthStatus {
281 if self.total_checksum_failures > 0 {
283 return StoreHealthStatus::Critical(
284 format!("{} checksum failures detected", self.total_checksum_failures)
285 );
286 }
287 if self.tier_flips_last_minute > 0.5 {
289 return StoreHealthStatus::Warning(
290 format!("High tier flip rate: {:.3}/block/min", self.tier_flips_last_minute)
291 );
292 }
293 if self.total_evictions > 0 && self.total_blocks > 0 {
295 let eviction_ratio = self.total_evictions as f32 / (self.total_reads + self.total_writes).max(1) as f32;
296 if eviction_ratio > 0.3 {
297 return StoreHealthStatus::Warning(
298 format!("High eviction ratio: {:.1}%", eviction_ratio * 100.0)
299 );
300 }
301 }
302 StoreHealthStatus::Healthy
303 }
304}
305
306#[derive(Clone, Debug, PartialEq)]
308pub enum StoreHealthStatus {
309 Healthy,
311 Warning(String),
313 Critical(String),
315}
316
317pub struct WitnessLog {
328 records: Vec<WitnessRecord>,
329 capacity: usize,
330}
331
332impl WitnessLog {
333 pub fn new(capacity: usize) -> Self {
337 let capacity = capacity.max(1);
338 Self {
339 records: Vec::with_capacity(capacity.min(1024)),
340 capacity,
341 }
342 }
343
344 pub fn record(&mut self, timestamp: u64, event: WitnessEvent) {
348 if self.records.len() >= self.capacity {
349 self.records.remove(0);
350 }
351 self.records.push(WitnessRecord { timestamp, event });
352 }
353
354 pub fn len(&self) -> usize {
356 self.records.len()
357 }
358
359 pub fn is_empty(&self) -> bool {
361 self.records.is_empty()
362 }
363
364 pub fn recent(&self, n: usize) -> &[WitnessRecord] {
368 let start = self.records.len().saturating_sub(n);
369 &self.records[start..]
370 }
371
372 pub fn all(&self) -> &[WitnessRecord] {
374 &self.records
375 }
376
377 pub fn clear(&mut self) {
379 self.records.clear();
380 }
381
382 pub fn count_tier_changes(&self) -> usize {
384 self.records
385 .iter()
386 .filter(|r| matches!(r.event, WitnessEvent::TierChange { .. }))
387 .count()
388 }
389
390 pub fn count_evictions(&self) -> usize {
392 self.records
393 .iter()
394 .filter(|r| matches!(r.event, WitnessEvent::Eviction { .. }))
395 .count()
396 }
397
398 pub fn count_checksum_failures(&self) -> usize {
400 self.records
401 .iter()
402 .filter(|r| matches!(r.event, WitnessEvent::ChecksumFailure { .. }))
403 .count()
404 }
405
406 pub fn tier_flip_rate(&self, window_ticks: u64, num_blocks: u64) -> f32 {
415 if num_blocks == 0 || self.records.is_empty() {
416 return 0.0;
417 }
418
419 let max_ts = self
420 .records
421 .iter()
422 .map(|r| r.timestamp)
423 .max()
424 .unwrap_or(0);
425 let min_ts = max_ts.saturating_sub(window_ticks);
426
427 let flips = self
428 .records
429 .iter()
430 .filter(|r| r.timestamp >= min_ts)
431 .filter(|r| matches!(r.event, WitnessEvent::TierChange { .. }))
432 .count() as f32;
433
434 flips / num_blocks as f32
435 }
436}
437
438#[derive(Clone, Debug)]
447pub struct StoreSnapshot {
448 pub timestamp: u64,
450 pub metrics: StoreMetrics,
452 pub tier_distribution: [u64; 4],
454 pub byte_distribution: [u64; 4],
456}
457
458impl StoreSnapshot {
459 pub fn to_bytes(&self) -> Vec<u8> {
466 let mut buf = Vec::with_capacity(512);
467
468 push_kv(&mut buf, "timestamp", self.timestamp);
469 push_kv(&mut buf, "total_blocks", self.metrics.total_blocks);
470 push_kv(&mut buf, "tier0_blocks", self.metrics.tier0_blocks);
471 push_kv(&mut buf, "tier1_blocks", self.metrics.tier1_blocks);
472 push_kv(&mut buf, "tier2_blocks", self.metrics.tier2_blocks);
473 push_kv(&mut buf, "tier3_blocks", self.metrics.tier3_blocks);
474 push_kv(&mut buf, "tier1_bytes", self.metrics.tier1_bytes);
475 push_kv(&mut buf, "tier2_bytes", self.metrics.tier2_bytes);
476 push_kv(&mut buf, "tier3_bytes", self.metrics.tier3_bytes);
477 push_kv(&mut buf, "total_reads", self.metrics.total_reads);
478 push_kv(&mut buf, "total_writes", self.metrics.total_writes);
479 push_kv(&mut buf, "total_evictions", self.metrics.total_evictions);
480 push_kv(&mut buf, "total_upgrades", self.metrics.total_upgrades);
481 push_kv(&mut buf, "total_downgrades", self.metrics.total_downgrades);
482 push_kv(
483 &mut buf,
484 "total_reconstructions",
485 self.metrics.total_reconstructions,
486 );
487 push_kv(
488 &mut buf,
489 "total_checksum_failures",
490 self.metrics.total_checksum_failures,
491 );
492 push_kv(&mut buf, "total_compactions", self.metrics.total_compactions);
493 push_kv_f32(
494 &mut buf,
495 "tier_flips_last_minute",
496 self.metrics.tier_flips_last_minute,
497 );
498 push_kv_f32(&mut buf, "avg_score_tier1", self.metrics.avg_score_tier1);
499 push_kv_f32(&mut buf, "avg_score_tier2", self.metrics.avg_score_tier2);
500 push_kv_f32(&mut buf, "avg_score_tier3", self.metrics.avg_score_tier3);
501 push_kv_f32(
502 &mut buf,
503 "compression_ratio",
504 self.metrics.compression_ratio(),
505 );
506 push_kv(
507 &mut buf,
508 "total_stored_bytes",
509 self.metrics.total_stored_bytes(),
510 );
511
512 for (i, &count) in self.tier_distribution.iter().enumerate() {
514 push_kv_indexed(&mut buf, "tier_dist", i, count);
515 }
516 for (i, &bytes) in self.byte_distribution.iter().enumerate() {
517 push_kv_indexed(&mut buf, "byte_dist", i, bytes);
518 }
519
520 buf
521 }
522}
523
524pub struct MetricsSeries {
530 snapshots: Vec<(u64, StoreMetrics)>,
531 capacity: usize,
532}
533
534#[derive(Clone, Debug)]
536pub struct MetricsTrend {
537 pub eviction_rate: f32,
539 pub compression_improving: bool,
541 pub tier_distribution_stable: bool,
543}
544
545impl MetricsSeries {
546 pub fn new(capacity: usize) -> Self {
548 Self {
549 snapshots: Vec::with_capacity(capacity.min(256)),
550 capacity: capacity.max(1),
551 }
552 }
553
554 pub fn record(&mut self, timestamp: u64, metrics: StoreMetrics) {
556 if self.snapshots.len() >= self.capacity {
557 self.snapshots.remove(0);
558 }
559 self.snapshots.push((timestamp, metrics));
560 }
561
562 pub fn len(&self) -> usize {
564 self.snapshots.len()
565 }
566
567 pub fn is_empty(&self) -> bool {
569 self.snapshots.is_empty()
570 }
571
572 pub fn latest(&self) -> Option<&(u64, StoreMetrics)> {
574 self.snapshots.last()
575 }
576
577 pub fn trend(&self) -> MetricsTrend {
579 if self.snapshots.len() < 2 {
580 return MetricsTrend {
581 eviction_rate: 0.0,
582 compression_improving: false,
583 tier_distribution_stable: true,
584 };
585 }
586
587 let n = self.snapshots.len();
588 let first = &self.snapshots[0].1;
589 let last = &self.snapshots[n - 1].1;
590
591 let eviction_delta = last.total_evictions.saturating_sub(first.total_evictions);
593 let eviction_rate = eviction_delta as f32 / n as f32;
594
595 let mid = n / 2;
597 let first_half_ratio: f32 = self.snapshots[..mid]
598 .iter()
599 .map(|(_, m)| m.compression_ratio())
600 .sum::<f32>()
601 / mid as f32;
602 let second_half_ratio: f32 = self.snapshots[mid..]
603 .iter()
604 .map(|(_, m)| m.compression_ratio())
605 .sum::<f32>()
606 / (n - mid) as f32;
607 let compression_improving = second_half_ratio > first_half_ratio;
608
609 let avg_tier1: f64 = self
611 .snapshots
612 .iter()
613 .map(|(_, m)| m.tier1_blocks as f64)
614 .sum::<f64>()
615 / n as f64;
616 let var_tier1: f64 = self
617 .snapshots
618 .iter()
619 .map(|(_, m)| {
620 let d = m.tier1_blocks as f64 - avg_tier1;
621 d * d
622 })
623 .sum::<f64>()
624 / n as f64;
625 let tier_distribution_stable = var_tier1.sqrt() < avg_tier1.max(1.0) * 0.3;
626
627 MetricsTrend {
628 eviction_rate,
629 compression_improving,
630 tier_distribution_stable,
631 }
632 }
633}
634
635fn format_line(key: &str, value: u64) -> String {
642 format!("{}: {}\n", key, value)
643}
644
645fn push_kv(buf: &mut Vec<u8>, key: &str, value: u64) {
647 buf.extend_from_slice(key.as_bytes());
648 buf.push(b'=');
649 push_u64(buf, value);
650 buf.push(b'\n');
651}
652
653fn push_kv_f32(buf: &mut Vec<u8>, key: &str, value: f32) {
655 buf.extend_from_slice(key.as_bytes());
656 buf.push(b'=');
657 push_f32(buf, value);
658 buf.push(b'\n');
659}
660
661fn push_kv_indexed(buf: &mut Vec<u8>, key: &str, index: usize, value: u64) {
663 buf.extend_from_slice(key.as_bytes());
664 buf.push(b'[');
665 push_u64(buf, index as u64);
666 buf.push(b']');
667 buf.push(b'=');
668 push_u64(buf, value);
669 buf.push(b'\n');
670}
671
672fn push_u64(buf: &mut Vec<u8>, mut v: u64) {
674 if v == 0 {
675 buf.push(b'0');
676 return;
677 }
678 let start = buf.len();
679 while v > 0 {
680 buf.push(b'0' + (v % 10) as u8);
681 v /= 10;
682 }
683 buf[start..].reverse();
684}
685
686fn push_f32(buf: &mut Vec<u8>, v: f32) {
688 if v < 0.0 {
689 buf.push(b'-');
690 push_f32(buf, -v);
691 return;
692 }
693 let int_part = v as u64;
694 push_u64(buf, int_part);
695 buf.push(b'.');
696 let frac = ((v - int_part as f32) * 1_000_000.0).round() as u64;
697 let s = frac;
699 let digits = if s == 0 {
700 1
701 } else {
702 ((s as f64).log10().floor() as usize) + 1
703 };
704 for _ in 0..(6usize.saturating_sub(digits)) {
705 buf.push(b'0');
706 }
707 push_u64(buf, s);
708}
709
710#[cfg(test)]
715mod tests {
716 use super::*;
717 use crate::store::{BlockKey, Tier};
718
719 fn bk(id: u64) -> BlockKey {
724 BlockKey { tensor_id: id as u128, block_index: 0 }
725 }
726
727 fn make_access(key: u64, score: f32, tier: Tier) -> WitnessEvent {
728 WitnessEvent::Access {
729 key: bk(key),
730 score,
731 tier,
732 }
733 }
734
735 fn make_tier_change(key: u64, from: Tier, to: Tier) -> WitnessEvent {
736 WitnessEvent::TierChange {
737 key: bk(key),
738 from_tier: from,
739 to_tier: to,
740 score: 100.0,
741 reason: TierChangeReason::ScoreUpgrade,
742 }
743 }
744
745 fn make_eviction(key: u64) -> WitnessEvent {
746 WitnessEvent::Eviction {
747 key: bk(key),
748 score: 0.5,
749 bytes_freed: 1024,
750 }
751 }
752
753 fn make_checksum_failure(key: u64) -> WitnessEvent {
754 WitnessEvent::ChecksumFailure {
755 key: bk(key),
756 expected: 0xDEAD,
757 actual: 0xBEEF,
758 }
759 }
760
761 #[test]
766 fn test_capacity_enforcement() {
767 let mut log = WitnessLog::new(3);
768 log.record(1, make_access(1, 1.0, Tier::Tier1));
769 log.record(2, make_access(2, 2.0, Tier::Tier2));
770 log.record(3, make_access(3, 3.0, Tier::Tier3));
771 assert_eq!(log.len(), 3);
772
773 log.record(4, make_access(4, 4.0, Tier::Tier1));
775 assert_eq!(log.len(), 3);
776 assert_eq!(log.all()[0].timestamp, 2);
777 assert_eq!(log.all()[2].timestamp, 4);
778 }
779
780 #[test]
781 fn test_capacity_zero_treated_as_one() {
782 let mut log = WitnessLog::new(0);
783 log.record(1, make_access(1, 1.0, Tier::Tier1));
784 assert_eq!(log.len(), 1);
785 log.record(2, make_access(2, 2.0, Tier::Tier2));
786 assert_eq!(log.len(), 1);
787 assert_eq!(log.all()[0].timestamp, 2);
788 }
789
790 #[test]
795 fn test_record_and_retrieve_all() {
796 let mut log = WitnessLog::new(100);
797 log.record(10, make_access(1, 1.0, Tier::Tier1));
798 log.record(20, make_eviction(2));
799 log.record(30, make_tier_change(3, Tier::Tier3, Tier::Tier2));
800
801 let all = log.all();
802 assert_eq!(all.len(), 3);
803 assert_eq!(all[0].timestamp, 10);
804 assert_eq!(all[1].timestamp, 20);
805 assert_eq!(all[2].timestamp, 30);
806 }
807
808 #[test]
809 fn test_recent_returns_tail() {
810 let mut log = WitnessLog::new(100);
811 for i in 0..10 {
812 log.record(i, make_access(i, i as f32, Tier::Tier1));
813 }
814
815 let recent = log.recent(3);
816 assert_eq!(recent.len(), 3);
817 assert_eq!(recent[0].timestamp, 7);
818 assert_eq!(recent[1].timestamp, 8);
819 assert_eq!(recent[2].timestamp, 9);
820 }
821
822 #[test]
823 fn test_recent_more_than_available() {
824 let mut log = WitnessLog::new(100);
825 log.record(1, make_access(1, 1.0, Tier::Tier1));
826 let recent = log.recent(50);
827 assert_eq!(recent.len(), 1);
828 }
829
830 #[test]
831 fn test_clear() {
832 let mut log = WitnessLog::new(100);
833 log.record(1, make_access(1, 1.0, Tier::Tier1));
834 log.record(2, make_eviction(2));
835 assert_eq!(log.len(), 2);
836
837 log.clear();
838 assert_eq!(log.len(), 0);
839 assert!(log.is_empty());
840 }
841
842 #[test]
847 fn test_count_tier_changes() {
848 let mut log = WitnessLog::new(100);
849 log.record(1, make_tier_change(1, Tier::Tier3, Tier::Tier2));
850 log.record(2, make_access(2, 1.0, Tier::Tier1));
851 log.record(3, make_tier_change(3, Tier::Tier2, Tier::Tier1));
852 log.record(4, make_eviction(4));
853
854 assert_eq!(log.count_tier_changes(), 2);
855 }
856
857 #[test]
858 fn test_count_evictions() {
859 let mut log = WitnessLog::new(100);
860 log.record(1, make_eviction(1));
861 log.record(2, make_eviction(2));
862 log.record(3, make_access(3, 1.0, Tier::Tier1));
863 log.record(4, make_eviction(3));
864
865 assert_eq!(log.count_evictions(), 3);
866 }
867
868 #[test]
869 fn test_count_checksum_failures() {
870 let mut log = WitnessLog::new(100);
871 log.record(1, make_checksum_failure(1));
872 log.record(2, make_access(2, 1.0, Tier::Tier1));
873 log.record(3, make_checksum_failure(3));
874
875 assert_eq!(log.count_checksum_failures(), 2);
876 }
877
878 #[test]
883 fn test_tier_flip_rate_basic() {
884 let mut log = WitnessLog::new(100);
885 for i in 0..4 {
887 log.record(100 + i, make_tier_change(i, Tier::Tier3, Tier::Tier2));
888 }
889 log.record(101, make_access(5, 1.0, Tier::Tier1));
891
892 let rate = log.tier_flip_rate(200, 10);
893 assert!((rate - 0.4).abs() < 1e-6, "rate={rate}");
895 }
896
897 #[test]
898 fn test_tier_flip_rate_windowed() {
899 let mut log = WitnessLog::new(100);
900 log.record(10, make_tier_change(1, Tier::Tier3, Tier::Tier2));
902 log.record(20, make_tier_change(2, Tier::Tier3, Tier::Tier1));
903 log.record(160, make_tier_change(3, Tier::Tier2, Tier::Tier1));
905 log.record(200, make_tier_change(4, Tier::Tier1, Tier::Tier2));
906
907 let rate = log.tier_flip_rate(50, 5);
908 assert!((rate - 0.4).abs() < 1e-6, "rate={rate}");
911 }
912
913 #[test]
914 fn test_tier_flip_rate_zero_blocks() {
915 let mut log = WitnessLog::new(100);
916 log.record(1, make_tier_change(1, Tier::Tier3, Tier::Tier2));
917 assert_eq!(log.tier_flip_rate(100, 0), 0.0);
918 }
919
920 #[test]
921 fn test_tier_flip_rate_empty_log() {
922 let log = WitnessLog::new(100);
923 assert_eq!(log.tier_flip_rate(100, 10), 0.0);
924 }
925
926 #[test]
931 fn test_compression_ratio_zero_bytes() {
932 let m = StoreMetrics::new();
933 assert_eq!(m.compression_ratio(), 0.0);
934 }
935
936 #[test]
937 fn test_compression_ratio_nonzero() {
938 let m = StoreMetrics {
939 tier1_bytes: 1000,
940 tier2_bytes: 500,
941 tier3_bytes: 200,
942 ..Default::default()
943 };
944 let ratio = m.compression_ratio();
948 assert!(ratio > 5.0 && ratio < 5.5, "ratio={ratio}");
949 }
950
951 #[test]
952 fn test_total_stored_bytes() {
953 let m = StoreMetrics {
954 tier1_bytes: 100,
955 tier2_bytes: 200,
956 tier3_bytes: 300,
957 ..Default::default()
958 };
959 assert_eq!(m.total_stored_bytes(), 600);
960 }
961
962 #[test]
967 fn test_snapshot_to_bytes_contains_keys() {
968 let snap = StoreSnapshot {
969 timestamp: 42,
970 metrics: StoreMetrics {
971 total_blocks: 10,
972 tier0_blocks: 2,
973 tier1_blocks: 3,
974 tier2_blocks: 3,
975 tier3_blocks: 2,
976 tier1_bytes: 1000,
977 tier2_bytes: 500,
978 tier3_bytes: 200,
979 total_reads: 100,
980 total_writes: 50,
981 ..Default::default()
982 },
983 tier_distribution: [2, 3, 3, 2],
984 byte_distribution: [8000, 1000, 500, 200],
985 };
986
987 let bytes = snap.to_bytes();
988 let text = core::str::from_utf8(&bytes).expect("valid utf-8");
989
990 assert!(text.contains("timestamp=42\n"), "missing timestamp");
991 assert!(text.contains("total_blocks=10\n"), "missing total_blocks");
992 assert!(text.contains("tier1_bytes=1000\n"), "missing tier1_bytes");
993 assert!(text.contains("total_reads=100\n"), "missing total_reads");
994 assert!(text.contains("total_writes=50\n"), "missing total_writes");
995 assert!(text.contains("tier_dist[0]=2\n"), "missing tier_dist[0]");
996 assert!(text.contains("tier_dist[3]=2\n"), "missing tier_dist[3]");
997 assert!(text.contains("byte_dist[1]=1000\n"), "missing byte_dist[1]");
998 assert!(
999 text.contains("compression_ratio="),
1000 "missing compression_ratio"
1001 );
1002 assert!(
1003 text.contains("total_stored_bytes=1700\n"),
1004 "missing total_stored_bytes"
1005 );
1006 }
1007
1008 #[test]
1009 fn test_snapshot_empty_metrics() {
1010 let snap = StoreSnapshot {
1011 timestamp: 0,
1012 metrics: StoreMetrics::default(),
1013 tier_distribution: [0; 4],
1014 byte_distribution: [0; 4],
1015 };
1016
1017 let bytes = snap.to_bytes();
1018 let text = core::str::from_utf8(&bytes).expect("valid utf-8");
1019
1020 assert!(text.contains("timestamp=0\n"));
1021 assert!(text.contains("total_blocks=0\n"));
1022 assert!(text.contains("total_stored_bytes=0\n"));
1023 }
1024
1025 #[test]
1030 fn test_empty_log_len() {
1031 let log = WitnessLog::new(10);
1032 assert_eq!(log.len(), 0);
1033 assert!(log.is_empty());
1034 }
1035
1036 #[test]
1037 fn test_empty_log_recent() {
1038 let log = WitnessLog::new(10);
1039 assert!(log.recent(5).is_empty());
1040 }
1041
1042 #[test]
1043 fn test_empty_log_counts() {
1044 let log = WitnessLog::new(10);
1045 assert_eq!(log.count_tier_changes(), 0);
1046 assert_eq!(log.count_evictions(), 0);
1047 assert_eq!(log.count_checksum_failures(), 0);
1048 }
1049
1050 #[test]
1051 fn test_empty_log_clear_is_noop() {
1052 let mut log = WitnessLog::new(10);
1053 log.clear();
1054 assert!(log.is_empty());
1055 }
1056
1057 #[test]
1062 fn test_push_u64_zero() {
1063 let mut buf = Vec::new();
1064 push_u64(&mut buf, 0);
1065 assert_eq!(&buf, b"0");
1066 }
1067
1068 #[test]
1069 fn test_push_u64_large() {
1070 let mut buf = Vec::new();
1071 push_u64(&mut buf, 123456789);
1072 assert_eq!(&buf, b"123456789");
1073 }
1074
1075 #[test]
1076 fn test_push_f32_positive() {
1077 let mut buf = Vec::new();
1078 push_f32(&mut buf, 3.14);
1079 let s = core::str::from_utf8(&buf).unwrap();
1080 assert!(s.starts_with("3."), "got: {s}");
1082 let frac: u64 = s.split('.').nth(1).unwrap().parse().unwrap();
1083 assert!(
1085 (frac as i64 - 140000).unsigned_abs() < 200,
1086 "frac={frac}, expected ~140000"
1087 );
1088 }
1089
1090 #[test]
1091 fn test_push_f32_negative() {
1092 let mut buf = Vec::new();
1093 push_f32(&mut buf, -1.5);
1094 let s = core::str::from_utf8(&buf).unwrap();
1095 assert!(s.starts_with("-1."), "got: {s}");
1096 }
1097
1098 #[test]
1103 fn test_format_report_contains_sections() {
1104 let m = StoreMetrics {
1105 total_blocks: 100,
1106 tier1_blocks: 50,
1107 tier2_blocks: 30,
1108 tier3_blocks: 20,
1109 tier1_bytes: 5000,
1110 tier2_bytes: 3000,
1111 tier3_bytes: 1000,
1112 total_reads: 1000,
1113 total_writes: 500,
1114 ..Default::default()
1115 };
1116 let report = m.format_report();
1117 assert!(report.contains("Temporal Tensor Store Report"));
1118 assert!(report.contains("Total blocks: 100"));
1119 assert!(report.contains("Reads: 1000"));
1120 assert!(report.contains("Compression ratio:"));
1121 }
1122
1123 #[test]
1124 fn test_format_json_valid_structure() {
1125 let m = StoreMetrics {
1126 total_blocks: 10,
1127 tier1_bytes: 100,
1128 ..Default::default()
1129 };
1130 let json = m.format_json();
1131 assert!(json.starts_with('{'));
1132 assert!(json.ends_with('}'));
1133 assert!(json.contains("\"total_blocks\":10"));
1134 assert!(json.contains("\"tier1_bytes\":100"));
1135 }
1136
1137 #[test]
1142 fn test_health_check_healthy() {
1143 let m = StoreMetrics {
1144 total_blocks: 100,
1145 total_reads: 1000,
1146 total_writes: 500,
1147 ..Default::default()
1148 };
1149 assert_eq!(m.health_check(), StoreHealthStatus::Healthy);
1150 }
1151
1152 #[test]
1153 fn test_health_check_critical_checksum() {
1154 let m = StoreMetrics {
1155 total_checksum_failures: 5,
1156 ..Default::default()
1157 };
1158 match m.health_check() {
1159 StoreHealthStatus::Critical(msg) => assert!(msg.contains("checksum")),
1160 other => panic!("expected Critical, got {:?}", other),
1161 }
1162 }
1163
1164 #[test]
1165 fn test_health_check_warning_flip_rate() {
1166 let m = StoreMetrics {
1167 tier_flips_last_minute: 0.8,
1168 ..Default::default()
1169 };
1170 match m.health_check() {
1171 StoreHealthStatus::Warning(msg) => assert!(msg.contains("flip rate")),
1172 other => panic!("expected Warning, got {:?}", other),
1173 }
1174 }
1175
1176 #[test]
1181 fn test_metrics_series_record_and_latest() {
1182 let mut series = MetricsSeries::new(10);
1183 assert!(series.is_empty());
1184 series.record(1, StoreMetrics { total_blocks: 10, ..Default::default() });
1185 series.record(2, StoreMetrics { total_blocks: 20, ..Default::default() });
1186 assert_eq!(series.len(), 2);
1187 assert_eq!(series.latest().unwrap().1.total_blocks, 20);
1188 }
1189
1190 #[test]
1191 fn test_metrics_series_capacity() {
1192 let mut series = MetricsSeries::new(3);
1193 for i in 0..5 {
1194 series.record(i as u64, StoreMetrics { total_blocks: i, ..Default::default() });
1195 }
1196 assert_eq!(series.len(), 3);
1197 assert_eq!(series.latest().unwrap().1.total_blocks, 4);
1198 }
1199
1200 #[test]
1201 fn test_metrics_trend_empty() {
1202 let series = MetricsSeries::new(10);
1203 let trend = series.trend();
1204 assert_eq!(trend.eviction_rate, 0.0);
1205 assert!(trend.tier_distribution_stable);
1206 }
1207
1208 #[test]
1209 fn test_metrics_trend_with_data() {
1210 let mut series = MetricsSeries::new(10);
1211 for i in 0..6u64 {
1212 series.record(i, StoreMetrics {
1213 total_blocks: 100,
1214 tier1_blocks: 50,
1215 total_evictions: i * 2,
1216 tier1_bytes: 5000 + i * 100,
1217 tier2_bytes: 3000,
1218 tier3_bytes: 1000,
1219 ..Default::default()
1220 });
1221 }
1222 let trend = series.trend();
1223 assert!(trend.eviction_rate > 0.0);
1224 }
1225}