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 { key: BlockKey, chain_len_before: u8 },
79 ChecksumFailure {
81 key: BlockKey,
82 expected: u32,
83 actual: u32,
84 },
85 Reconstruction {
87 key: BlockKey,
88 policy: ReconstructPolicy,
89 success: bool,
90 },
91}
92
93#[derive(Clone, Debug, PartialEq, Eq)]
95pub enum TierChangeReason {
96 ScoreUpgrade,
98 ScoreDowngrade,
100 ByteCapPressure,
102 ManualOverride,
104}
105
106#[derive(Clone, Debug, Default)]
115pub struct StoreMetrics {
116 pub total_blocks: u64,
118 pub tier0_blocks: u64,
120 pub tier1_blocks: u64,
122 pub tier2_blocks: u64,
124 pub tier3_blocks: u64,
126 pub tier1_bytes: u64,
128 pub tier2_bytes: u64,
130 pub tier3_bytes: u64,
132 pub total_reads: u64,
134 pub total_writes: u64,
136 pub total_evictions: u64,
138 pub total_upgrades: u64,
140 pub total_downgrades: u64,
142 pub total_reconstructions: u64,
144 pub total_checksum_failures: u64,
146 pub total_compactions: u64,
148 pub tier_flips_last_minute: f32,
150 pub avg_score_tier1: f32,
152 pub avg_score_tier2: f32,
154 pub avg_score_tier3: f32,
156}
157
158impl StoreMetrics {
159 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn compression_ratio(&self) -> f32 {
178 let stored = self.total_stored_bytes();
179 if stored == 0 {
180 return 0.0;
181 }
182 let raw_estimate = (self.tier1_bytes as f64 * 4.0)
183 + (self.tier2_bytes as f64 * 5.5)
184 + (self.tier3_bytes as f64 * 10.67);
185 raw_estimate as f32 / stored as f32
186 }
187
188 pub fn total_stored_bytes(&self) -> u64 {
193 self.tier1_bytes + self.tier2_bytes + self.tier3_bytes
194 }
195
196 pub fn format_report(&self) -> String {
198 let mut s = String::with_capacity(512);
199 s.push_str("=== Temporal Tensor Store Report ===\n");
200 s.push_str(&format_line("Total blocks", self.total_blocks));
201 s.push_str(&format_line(" Tier0 (raw)", self.tier0_blocks));
202 s.push_str(&format_line(" Tier1 (hot)", self.tier1_blocks));
203 s.push_str(&format_line(" Tier2 (warm)", self.tier2_blocks));
204 s.push_str(&format_line(" Tier3 (cold)", self.tier3_blocks));
205 s.push_str("--- Storage ---\n");
206 s.push_str(&format_line("Tier1 bytes", self.tier1_bytes));
207 s.push_str(&format_line("Tier2 bytes", self.tier2_bytes));
208 s.push_str(&format_line("Tier3 bytes", self.tier3_bytes));
209 s.push_str(&format_line("Total stored", self.total_stored_bytes()));
210 s.push_str(&format!(
211 "Compression ratio: {:.2}x\n",
212 self.compression_ratio()
213 ));
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(
223 "Checksum failures",
224 self.total_checksum_failures,
225 ));
226 s.push_str(&format!(
227 "Tier flip rate: {:.4}/block/min\n",
228 self.tier_flips_last_minute
229 ));
230 s
231 }
232
233 pub fn format_json(&self) -> String {
235 format!(
236 concat!(
237 "{{",
238 "\"total_blocks\":{},",
239 "\"tier0_blocks\":{},",
240 "\"tier1_blocks\":{},",
241 "\"tier2_blocks\":{},",
242 "\"tier3_blocks\":{},",
243 "\"tier1_bytes\":{},",
244 "\"tier2_bytes\":{},",
245 "\"tier3_bytes\":{},",
246 "\"total_reads\":{},",
247 "\"total_writes\":{},",
248 "\"total_evictions\":{},",
249 "\"total_upgrades\":{},",
250 "\"total_downgrades\":{},",
251 "\"total_reconstructions\":{},",
252 "\"total_checksum_failures\":{},",
253 "\"total_compactions\":{},",
254 "\"compression_ratio\":{:.4},",
255 "\"tier_flips_last_minute\":{:.4},",
256 "\"avg_score_tier1\":{:.4},",
257 "\"avg_score_tier2\":{:.4},",
258 "\"avg_score_tier3\":{:.4}",
259 "}}"
260 ),
261 self.total_blocks,
262 self.tier0_blocks,
263 self.tier1_blocks,
264 self.tier2_blocks,
265 self.tier3_blocks,
266 self.tier1_bytes,
267 self.tier2_bytes,
268 self.tier3_bytes,
269 self.total_reads,
270 self.total_writes,
271 self.total_evictions,
272 self.total_upgrades,
273 self.total_downgrades,
274 self.total_reconstructions,
275 self.total_checksum_failures,
276 self.total_compactions,
277 self.compression_ratio(),
278 self.tier_flips_last_minute,
279 self.avg_score_tier1,
280 self.avg_score_tier2,
281 self.avg_score_tier3,
282 )
283 }
284
285 pub fn health_check(&self) -> StoreHealthStatus {
287 if self.total_checksum_failures > 0 {
289 return StoreHealthStatus::Critical(format!(
290 "{} checksum failures detected",
291 self.total_checksum_failures
292 ));
293 }
294 if self.tier_flips_last_minute > 0.5 {
296 return StoreHealthStatus::Warning(format!(
297 "High tier flip rate: {:.3}/block/min",
298 self.tier_flips_last_minute
299 ));
300 }
301 if self.total_evictions > 0 && self.total_blocks > 0 {
303 let eviction_ratio =
304 self.total_evictions as f32 / (self.total_reads + self.total_writes).max(1) as f32;
305 if eviction_ratio > 0.3 {
306 return StoreHealthStatus::Warning(format!(
307 "High eviction ratio: {:.1}%",
308 eviction_ratio * 100.0
309 ));
310 }
311 }
312 StoreHealthStatus::Healthy
313 }
314}
315
316#[derive(Clone, Debug, PartialEq)]
318pub enum StoreHealthStatus {
319 Healthy,
321 Warning(String),
323 Critical(String),
325}
326
327pub struct WitnessLog {
338 records: Vec<WitnessRecord>,
339 capacity: usize,
340}
341
342impl WitnessLog {
343 pub fn new(capacity: usize) -> Self {
347 let capacity = capacity.max(1);
348 Self {
349 records: Vec::with_capacity(capacity.min(1024)),
350 capacity,
351 }
352 }
353
354 pub fn record(&mut self, timestamp: u64, event: WitnessEvent) {
358 if self.records.len() >= self.capacity {
359 self.records.remove(0);
360 }
361 self.records.push(WitnessRecord { timestamp, event });
362 }
363
364 pub fn len(&self) -> usize {
366 self.records.len()
367 }
368
369 pub fn is_empty(&self) -> bool {
371 self.records.is_empty()
372 }
373
374 pub fn recent(&self, n: usize) -> &[WitnessRecord] {
378 let start = self.records.len().saturating_sub(n);
379 &self.records[start..]
380 }
381
382 pub fn all(&self) -> &[WitnessRecord] {
384 &self.records
385 }
386
387 pub fn clear(&mut self) {
389 self.records.clear();
390 }
391
392 pub fn count_tier_changes(&self) -> usize {
394 self.records
395 .iter()
396 .filter(|r| matches!(r.event, WitnessEvent::TierChange { .. }))
397 .count()
398 }
399
400 pub fn count_evictions(&self) -> usize {
402 self.records
403 .iter()
404 .filter(|r| matches!(r.event, WitnessEvent::Eviction { .. }))
405 .count()
406 }
407
408 pub fn count_checksum_failures(&self) -> usize {
410 self.records
411 .iter()
412 .filter(|r| matches!(r.event, WitnessEvent::ChecksumFailure { .. }))
413 .count()
414 }
415
416 pub fn tier_flip_rate(&self, window_ticks: u64, num_blocks: u64) -> f32 {
425 if num_blocks == 0 || self.records.is_empty() {
426 return 0.0;
427 }
428
429 let max_ts = self.records.iter().map(|r| r.timestamp).max().unwrap_or(0);
430 let min_ts = max_ts.saturating_sub(window_ticks);
431
432 let flips = self
433 .records
434 .iter()
435 .filter(|r| r.timestamp >= min_ts)
436 .filter(|r| matches!(r.event, WitnessEvent::TierChange { .. }))
437 .count() as f32;
438
439 flips / num_blocks as f32
440 }
441}
442
443#[derive(Clone, Debug)]
452pub struct StoreSnapshot {
453 pub timestamp: u64,
455 pub metrics: StoreMetrics,
457 pub tier_distribution: [u64; 4],
459 pub byte_distribution: [u64; 4],
461}
462
463impl StoreSnapshot {
464 pub fn to_bytes(&self) -> Vec<u8> {
471 let mut buf = Vec::with_capacity(512);
472
473 push_kv(&mut buf, "timestamp", self.timestamp);
474 push_kv(&mut buf, "total_blocks", self.metrics.total_blocks);
475 push_kv(&mut buf, "tier0_blocks", self.metrics.tier0_blocks);
476 push_kv(&mut buf, "tier1_blocks", self.metrics.tier1_blocks);
477 push_kv(&mut buf, "tier2_blocks", self.metrics.tier2_blocks);
478 push_kv(&mut buf, "tier3_blocks", self.metrics.tier3_blocks);
479 push_kv(&mut buf, "tier1_bytes", self.metrics.tier1_bytes);
480 push_kv(&mut buf, "tier2_bytes", self.metrics.tier2_bytes);
481 push_kv(&mut buf, "tier3_bytes", self.metrics.tier3_bytes);
482 push_kv(&mut buf, "total_reads", self.metrics.total_reads);
483 push_kv(&mut buf, "total_writes", self.metrics.total_writes);
484 push_kv(&mut buf, "total_evictions", self.metrics.total_evictions);
485 push_kv(&mut buf, "total_upgrades", self.metrics.total_upgrades);
486 push_kv(&mut buf, "total_downgrades", self.metrics.total_downgrades);
487 push_kv(
488 &mut buf,
489 "total_reconstructions",
490 self.metrics.total_reconstructions,
491 );
492 push_kv(
493 &mut buf,
494 "total_checksum_failures",
495 self.metrics.total_checksum_failures,
496 );
497 push_kv(
498 &mut buf,
499 "total_compactions",
500 self.metrics.total_compactions,
501 );
502 push_kv_f32(
503 &mut buf,
504 "tier_flips_last_minute",
505 self.metrics.tier_flips_last_minute,
506 );
507 push_kv_f32(&mut buf, "avg_score_tier1", self.metrics.avg_score_tier1);
508 push_kv_f32(&mut buf, "avg_score_tier2", self.metrics.avg_score_tier2);
509 push_kv_f32(&mut buf, "avg_score_tier3", self.metrics.avg_score_tier3);
510 push_kv_f32(
511 &mut buf,
512 "compression_ratio",
513 self.metrics.compression_ratio(),
514 );
515 push_kv(
516 &mut buf,
517 "total_stored_bytes",
518 self.metrics.total_stored_bytes(),
519 );
520
521 for (i, &count) in self.tier_distribution.iter().enumerate() {
523 push_kv_indexed(&mut buf, "tier_dist", i, count);
524 }
525 for (i, &bytes) in self.byte_distribution.iter().enumerate() {
526 push_kv_indexed(&mut buf, "byte_dist", i, bytes);
527 }
528
529 buf
530 }
531}
532
533pub struct MetricsSeries {
539 snapshots: Vec<(u64, StoreMetrics)>,
540 capacity: usize,
541}
542
543#[derive(Clone, Debug)]
545pub struct MetricsTrend {
546 pub eviction_rate: f32,
548 pub compression_improving: bool,
550 pub tier_distribution_stable: bool,
552}
553
554impl MetricsSeries {
555 pub fn new(capacity: usize) -> Self {
557 Self {
558 snapshots: Vec::with_capacity(capacity.min(256)),
559 capacity: capacity.max(1),
560 }
561 }
562
563 pub fn record(&mut self, timestamp: u64, metrics: StoreMetrics) {
565 if self.snapshots.len() >= self.capacity {
566 self.snapshots.remove(0);
567 }
568 self.snapshots.push((timestamp, metrics));
569 }
570
571 pub fn len(&self) -> usize {
573 self.snapshots.len()
574 }
575
576 pub fn is_empty(&self) -> bool {
578 self.snapshots.is_empty()
579 }
580
581 pub fn latest(&self) -> Option<&(u64, StoreMetrics)> {
583 self.snapshots.last()
584 }
585
586 pub fn trend(&self) -> MetricsTrend {
588 if self.snapshots.len() < 2 {
589 return MetricsTrend {
590 eviction_rate: 0.0,
591 compression_improving: false,
592 tier_distribution_stable: true,
593 };
594 }
595
596 let n = self.snapshots.len();
597 let first = &self.snapshots[0].1;
598 let last = &self.snapshots[n - 1].1;
599
600 let eviction_delta = last.total_evictions.saturating_sub(first.total_evictions);
602 let eviction_rate = eviction_delta as f32 / n as f32;
603
604 let mid = n / 2;
606 let first_half_ratio: f32 = self.snapshots[..mid]
607 .iter()
608 .map(|(_, m)| m.compression_ratio())
609 .sum::<f32>()
610 / mid as f32;
611 let second_half_ratio: f32 = self.snapshots[mid..]
612 .iter()
613 .map(|(_, m)| m.compression_ratio())
614 .sum::<f32>()
615 / (n - mid) as f32;
616 let compression_improving = second_half_ratio > first_half_ratio;
617
618 let avg_tier1: f64 = self
620 .snapshots
621 .iter()
622 .map(|(_, m)| m.tier1_blocks as f64)
623 .sum::<f64>()
624 / n as f64;
625 let var_tier1: f64 = self
626 .snapshots
627 .iter()
628 .map(|(_, m)| {
629 let d = m.tier1_blocks as f64 - avg_tier1;
630 d * d
631 })
632 .sum::<f64>()
633 / n as f64;
634 let tier_distribution_stable = var_tier1.sqrt() < avg_tier1.max(1.0) * 0.3;
635
636 MetricsTrend {
637 eviction_rate,
638 compression_improving,
639 tier_distribution_stable,
640 }
641 }
642}
643
644fn format_line(key: &str, value: u64) -> String {
651 format!("{}: {}\n", key, value)
652}
653
654fn push_kv(buf: &mut Vec<u8>, key: &str, value: u64) {
656 buf.extend_from_slice(key.as_bytes());
657 buf.push(b'=');
658 push_u64(buf, value);
659 buf.push(b'\n');
660}
661
662fn push_kv_f32(buf: &mut Vec<u8>, key: &str, value: f32) {
664 buf.extend_from_slice(key.as_bytes());
665 buf.push(b'=');
666 push_f32(buf, value);
667 buf.push(b'\n');
668}
669
670fn push_kv_indexed(buf: &mut Vec<u8>, key: &str, index: usize, value: u64) {
672 buf.extend_from_slice(key.as_bytes());
673 buf.push(b'[');
674 push_u64(buf, index as u64);
675 buf.push(b']');
676 buf.push(b'=');
677 push_u64(buf, value);
678 buf.push(b'\n');
679}
680
681fn push_u64(buf: &mut Vec<u8>, mut v: u64) {
683 if v == 0 {
684 buf.push(b'0');
685 return;
686 }
687 let start = buf.len();
688 while v > 0 {
689 buf.push(b'0' + (v % 10) as u8);
690 v /= 10;
691 }
692 buf[start..].reverse();
693}
694
695fn push_f32(buf: &mut Vec<u8>, v: f32) {
697 if v < 0.0 {
698 buf.push(b'-');
699 push_f32(buf, -v);
700 return;
701 }
702 let int_part = v as u64;
703 push_u64(buf, int_part);
704 buf.push(b'.');
705 let frac = ((v - int_part as f32) * 1_000_000.0).round() as u64;
706 let s = frac;
708 let digits = if s == 0 {
709 1
710 } else {
711 ((s as f64).log10().floor() as usize) + 1
712 };
713 for _ in 0..(6usize.saturating_sub(digits)) {
714 buf.push(b'0');
715 }
716 push_u64(buf, s);
717}
718
719#[cfg(test)]
724mod tests {
725 use super::*;
726 use crate::store::{BlockKey, Tier};
727
728 fn bk(id: u64) -> BlockKey {
733 BlockKey {
734 tensor_id: id as u128,
735 block_index: 0,
736 }
737 }
738
739 fn make_access(key: u64, score: f32, tier: Tier) -> WitnessEvent {
740 WitnessEvent::Access {
741 key: bk(key),
742 score,
743 tier,
744 }
745 }
746
747 fn make_tier_change(key: u64, from: Tier, to: Tier) -> WitnessEvent {
748 WitnessEvent::TierChange {
749 key: bk(key),
750 from_tier: from,
751 to_tier: to,
752 score: 100.0,
753 reason: TierChangeReason::ScoreUpgrade,
754 }
755 }
756
757 fn make_eviction(key: u64) -> WitnessEvent {
758 WitnessEvent::Eviction {
759 key: bk(key),
760 score: 0.5,
761 bytes_freed: 1024,
762 }
763 }
764
765 fn make_checksum_failure(key: u64) -> WitnessEvent {
766 WitnessEvent::ChecksumFailure {
767 key: bk(key),
768 expected: 0xDEAD,
769 actual: 0xBEEF,
770 }
771 }
772
773 #[test]
778 fn test_capacity_enforcement() {
779 let mut log = WitnessLog::new(3);
780 log.record(1, make_access(1, 1.0, Tier::Tier1));
781 log.record(2, make_access(2, 2.0, Tier::Tier2));
782 log.record(3, make_access(3, 3.0, Tier::Tier3));
783 assert_eq!(log.len(), 3);
784
785 log.record(4, make_access(4, 4.0, Tier::Tier1));
787 assert_eq!(log.len(), 3);
788 assert_eq!(log.all()[0].timestamp, 2);
789 assert_eq!(log.all()[2].timestamp, 4);
790 }
791
792 #[test]
793 fn test_capacity_zero_treated_as_one() {
794 let mut log = WitnessLog::new(0);
795 log.record(1, make_access(1, 1.0, Tier::Tier1));
796 assert_eq!(log.len(), 1);
797 log.record(2, make_access(2, 2.0, Tier::Tier2));
798 assert_eq!(log.len(), 1);
799 assert_eq!(log.all()[0].timestamp, 2);
800 }
801
802 #[test]
807 fn test_record_and_retrieve_all() {
808 let mut log = WitnessLog::new(100);
809 log.record(10, make_access(1, 1.0, Tier::Tier1));
810 log.record(20, make_eviction(2));
811 log.record(30, make_tier_change(3, Tier::Tier3, Tier::Tier2));
812
813 let all = log.all();
814 assert_eq!(all.len(), 3);
815 assert_eq!(all[0].timestamp, 10);
816 assert_eq!(all[1].timestamp, 20);
817 assert_eq!(all[2].timestamp, 30);
818 }
819
820 #[test]
821 fn test_recent_returns_tail() {
822 let mut log = WitnessLog::new(100);
823 for i in 0..10 {
824 log.record(i, make_access(i, i as f32, Tier::Tier1));
825 }
826
827 let recent = log.recent(3);
828 assert_eq!(recent.len(), 3);
829 assert_eq!(recent[0].timestamp, 7);
830 assert_eq!(recent[1].timestamp, 8);
831 assert_eq!(recent[2].timestamp, 9);
832 }
833
834 #[test]
835 fn test_recent_more_than_available() {
836 let mut log = WitnessLog::new(100);
837 log.record(1, make_access(1, 1.0, Tier::Tier1));
838 let recent = log.recent(50);
839 assert_eq!(recent.len(), 1);
840 }
841
842 #[test]
843 fn test_clear() {
844 let mut log = WitnessLog::new(100);
845 log.record(1, make_access(1, 1.0, Tier::Tier1));
846 log.record(2, make_eviction(2));
847 assert_eq!(log.len(), 2);
848
849 log.clear();
850 assert_eq!(log.len(), 0);
851 assert!(log.is_empty());
852 }
853
854 #[test]
859 fn test_count_tier_changes() {
860 let mut log = WitnessLog::new(100);
861 log.record(1, make_tier_change(1, Tier::Tier3, Tier::Tier2));
862 log.record(2, make_access(2, 1.0, Tier::Tier1));
863 log.record(3, make_tier_change(3, Tier::Tier2, Tier::Tier1));
864 log.record(4, make_eviction(4));
865
866 assert_eq!(log.count_tier_changes(), 2);
867 }
868
869 #[test]
870 fn test_count_evictions() {
871 let mut log = WitnessLog::new(100);
872 log.record(1, make_eviction(1));
873 log.record(2, make_eviction(2));
874 log.record(3, make_access(3, 1.0, Tier::Tier1));
875 log.record(4, make_eviction(3));
876
877 assert_eq!(log.count_evictions(), 3);
878 }
879
880 #[test]
881 fn test_count_checksum_failures() {
882 let mut log = WitnessLog::new(100);
883 log.record(1, make_checksum_failure(1));
884 log.record(2, make_access(2, 1.0, Tier::Tier1));
885 log.record(3, make_checksum_failure(3));
886
887 assert_eq!(log.count_checksum_failures(), 2);
888 }
889
890 #[test]
895 fn test_tier_flip_rate_basic() {
896 let mut log = WitnessLog::new(100);
897 for i in 0..4 {
899 log.record(100 + i, make_tier_change(i, Tier::Tier3, Tier::Tier2));
900 }
901 log.record(101, make_access(5, 1.0, Tier::Tier1));
903
904 let rate = log.tier_flip_rate(200, 10);
905 assert!((rate - 0.4).abs() < 1e-6, "rate={rate}");
907 }
908
909 #[test]
910 fn test_tier_flip_rate_windowed() {
911 let mut log = WitnessLog::new(100);
912 log.record(10, make_tier_change(1, Tier::Tier3, Tier::Tier2));
914 log.record(20, make_tier_change(2, Tier::Tier3, Tier::Tier1));
915 log.record(160, make_tier_change(3, Tier::Tier2, Tier::Tier1));
917 log.record(200, make_tier_change(4, Tier::Tier1, Tier::Tier2));
918
919 let rate = log.tier_flip_rate(50, 5);
920 assert!((rate - 0.4).abs() < 1e-6, "rate={rate}");
923 }
924
925 #[test]
926 fn test_tier_flip_rate_zero_blocks() {
927 let mut log = WitnessLog::new(100);
928 log.record(1, make_tier_change(1, Tier::Tier3, Tier::Tier2));
929 assert_eq!(log.tier_flip_rate(100, 0), 0.0);
930 }
931
932 #[test]
933 fn test_tier_flip_rate_empty_log() {
934 let log = WitnessLog::new(100);
935 assert_eq!(log.tier_flip_rate(100, 10), 0.0);
936 }
937
938 #[test]
943 fn test_compression_ratio_zero_bytes() {
944 let m = StoreMetrics::new();
945 assert_eq!(m.compression_ratio(), 0.0);
946 }
947
948 #[test]
949 fn test_compression_ratio_nonzero() {
950 let m = StoreMetrics {
951 tier1_bytes: 1000,
952 tier2_bytes: 500,
953 tier3_bytes: 200,
954 ..Default::default()
955 };
956 let ratio = m.compression_ratio();
960 assert!(ratio > 5.0 && ratio < 5.5, "ratio={ratio}");
961 }
962
963 #[test]
964 fn test_total_stored_bytes() {
965 let m = StoreMetrics {
966 tier1_bytes: 100,
967 tier2_bytes: 200,
968 tier3_bytes: 300,
969 ..Default::default()
970 };
971 assert_eq!(m.total_stored_bytes(), 600);
972 }
973
974 #[test]
979 fn test_snapshot_to_bytes_contains_keys() {
980 let snap = StoreSnapshot {
981 timestamp: 42,
982 metrics: StoreMetrics {
983 total_blocks: 10,
984 tier0_blocks: 2,
985 tier1_blocks: 3,
986 tier2_blocks: 3,
987 tier3_blocks: 2,
988 tier1_bytes: 1000,
989 tier2_bytes: 500,
990 tier3_bytes: 200,
991 total_reads: 100,
992 total_writes: 50,
993 ..Default::default()
994 },
995 tier_distribution: [2, 3, 3, 2],
996 byte_distribution: [8000, 1000, 500, 200],
997 };
998
999 let bytes = snap.to_bytes();
1000 let text = core::str::from_utf8(&bytes).expect("valid utf-8");
1001
1002 assert!(text.contains("timestamp=42\n"), "missing timestamp");
1003 assert!(text.contains("total_blocks=10\n"), "missing total_blocks");
1004 assert!(text.contains("tier1_bytes=1000\n"), "missing tier1_bytes");
1005 assert!(text.contains("total_reads=100\n"), "missing total_reads");
1006 assert!(text.contains("total_writes=50\n"), "missing total_writes");
1007 assert!(text.contains("tier_dist[0]=2\n"), "missing tier_dist[0]");
1008 assert!(text.contains("tier_dist[3]=2\n"), "missing tier_dist[3]");
1009 assert!(text.contains("byte_dist[1]=1000\n"), "missing byte_dist[1]");
1010 assert!(
1011 text.contains("compression_ratio="),
1012 "missing compression_ratio"
1013 );
1014 assert!(
1015 text.contains("total_stored_bytes=1700\n"),
1016 "missing total_stored_bytes"
1017 );
1018 }
1019
1020 #[test]
1021 fn test_snapshot_empty_metrics() {
1022 let snap = StoreSnapshot {
1023 timestamp: 0,
1024 metrics: StoreMetrics::default(),
1025 tier_distribution: [0; 4],
1026 byte_distribution: [0; 4],
1027 };
1028
1029 let bytes = snap.to_bytes();
1030 let text = core::str::from_utf8(&bytes).expect("valid utf-8");
1031
1032 assert!(text.contains("timestamp=0\n"));
1033 assert!(text.contains("total_blocks=0\n"));
1034 assert!(text.contains("total_stored_bytes=0\n"));
1035 }
1036
1037 #[test]
1042 fn test_empty_log_len() {
1043 let log = WitnessLog::new(10);
1044 assert_eq!(log.len(), 0);
1045 assert!(log.is_empty());
1046 }
1047
1048 #[test]
1049 fn test_empty_log_recent() {
1050 let log = WitnessLog::new(10);
1051 assert!(log.recent(5).is_empty());
1052 }
1053
1054 #[test]
1055 fn test_empty_log_counts() {
1056 let log = WitnessLog::new(10);
1057 assert_eq!(log.count_tier_changes(), 0);
1058 assert_eq!(log.count_evictions(), 0);
1059 assert_eq!(log.count_checksum_failures(), 0);
1060 }
1061
1062 #[test]
1063 fn test_empty_log_clear_is_noop() {
1064 let mut log = WitnessLog::new(10);
1065 log.clear();
1066 assert!(log.is_empty());
1067 }
1068
1069 #[test]
1074 fn test_push_u64_zero() {
1075 let mut buf = Vec::new();
1076 push_u64(&mut buf, 0);
1077 assert_eq!(&buf, b"0");
1078 }
1079
1080 #[test]
1081 fn test_push_u64_large() {
1082 let mut buf = Vec::new();
1083 push_u64(&mut buf, 123456789);
1084 assert_eq!(&buf, b"123456789");
1085 }
1086
1087 #[test]
1088 fn test_push_f32_positive() {
1089 let mut buf = Vec::new();
1090 push_f32(&mut buf, 3.14);
1091 let s = core::str::from_utf8(&buf).unwrap();
1092 assert!(s.starts_with("3."), "got: {s}");
1094 let frac: u64 = s.split('.').nth(1).unwrap().parse().unwrap();
1095 assert!(
1097 (frac as i64 - 140000).unsigned_abs() < 200,
1098 "frac={frac}, expected ~140000"
1099 );
1100 }
1101
1102 #[test]
1103 fn test_push_f32_negative() {
1104 let mut buf = Vec::new();
1105 push_f32(&mut buf, -1.5);
1106 let s = core::str::from_utf8(&buf).unwrap();
1107 assert!(s.starts_with("-1."), "got: {s}");
1108 }
1109
1110 #[test]
1115 fn test_format_report_contains_sections() {
1116 let m = StoreMetrics {
1117 total_blocks: 100,
1118 tier1_blocks: 50,
1119 tier2_blocks: 30,
1120 tier3_blocks: 20,
1121 tier1_bytes: 5000,
1122 tier2_bytes: 3000,
1123 tier3_bytes: 1000,
1124 total_reads: 1000,
1125 total_writes: 500,
1126 ..Default::default()
1127 };
1128 let report = m.format_report();
1129 assert!(report.contains("Temporal Tensor Store Report"));
1130 assert!(report.contains("Total blocks: 100"));
1131 assert!(report.contains("Reads: 1000"));
1132 assert!(report.contains("Compression ratio:"));
1133 }
1134
1135 #[test]
1136 fn test_format_json_valid_structure() {
1137 let m = StoreMetrics {
1138 total_blocks: 10,
1139 tier1_bytes: 100,
1140 ..Default::default()
1141 };
1142 let json = m.format_json();
1143 assert!(json.starts_with('{'));
1144 assert!(json.ends_with('}'));
1145 assert!(json.contains("\"total_blocks\":10"));
1146 assert!(json.contains("\"tier1_bytes\":100"));
1147 }
1148
1149 #[test]
1154 fn test_health_check_healthy() {
1155 let m = StoreMetrics {
1156 total_blocks: 100,
1157 total_reads: 1000,
1158 total_writes: 500,
1159 ..Default::default()
1160 };
1161 assert_eq!(m.health_check(), StoreHealthStatus::Healthy);
1162 }
1163
1164 #[test]
1165 fn test_health_check_critical_checksum() {
1166 let m = StoreMetrics {
1167 total_checksum_failures: 5,
1168 ..Default::default()
1169 };
1170 match m.health_check() {
1171 StoreHealthStatus::Critical(msg) => assert!(msg.contains("checksum")),
1172 other => panic!("expected Critical, got {:?}", other),
1173 }
1174 }
1175
1176 #[test]
1177 fn test_health_check_warning_flip_rate() {
1178 let m = StoreMetrics {
1179 tier_flips_last_minute: 0.8,
1180 ..Default::default()
1181 };
1182 match m.health_check() {
1183 StoreHealthStatus::Warning(msg) => assert!(msg.contains("flip rate")),
1184 other => panic!("expected Warning, got {:?}", other),
1185 }
1186 }
1187
1188 #[test]
1193 fn test_metrics_series_record_and_latest() {
1194 let mut series = MetricsSeries::new(10);
1195 assert!(series.is_empty());
1196 series.record(
1197 1,
1198 StoreMetrics {
1199 total_blocks: 10,
1200 ..Default::default()
1201 },
1202 );
1203 series.record(
1204 2,
1205 StoreMetrics {
1206 total_blocks: 20,
1207 ..Default::default()
1208 },
1209 );
1210 assert_eq!(series.len(), 2);
1211 assert_eq!(series.latest().unwrap().1.total_blocks, 20);
1212 }
1213
1214 #[test]
1215 fn test_metrics_series_capacity() {
1216 let mut series = MetricsSeries::new(3);
1217 for i in 0..5 {
1218 series.record(
1219 i as u64,
1220 StoreMetrics {
1221 total_blocks: i,
1222 ..Default::default()
1223 },
1224 );
1225 }
1226 assert_eq!(series.len(), 3);
1227 assert_eq!(series.latest().unwrap().1.total_blocks, 4);
1228 }
1229
1230 #[test]
1231 fn test_metrics_trend_empty() {
1232 let series = MetricsSeries::new(10);
1233 let trend = series.trend();
1234 assert_eq!(trend.eviction_rate, 0.0);
1235 assert!(trend.tier_distribution_stable);
1236 }
1237
1238 #[test]
1239 fn test_metrics_trend_with_data() {
1240 let mut series = MetricsSeries::new(10);
1241 for i in 0..6u64 {
1242 series.record(
1243 i,
1244 StoreMetrics {
1245 total_blocks: 100,
1246 tier1_blocks: 50,
1247 total_evictions: i * 2,
1248 tier1_bytes: 5000 + i * 100,
1249 tier2_bytes: 3000,
1250 tier3_bytes: 1000,
1251 ..Default::default()
1252 },
1253 );
1254 }
1255 let trend = series.trend();
1256 assert!(trend.eviction_rate > 0.0);
1257 }
1258}