1#[cfg(feature = "presentar-terminal")]
12use std::collections::VecDeque;
13#[cfg(feature = "presentar-terminal")]
14use std::io::{self, Write};
15#[cfg(feature = "presentar-terminal")]
16use std::time::Duration;
17
18#[cfg(feature = "presentar-terminal")]
19use crossterm::{
20 cursor,
21 event::{self, Event, KeyCode, KeyEventKind},
22 execute,
23 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
24};
25
26#[cfg(feature = "presentar-terminal")]
27use presentar_terminal::{CellBuffer, Color, DiffRenderer, Modifiers};
28
29#[cfg(feature = "presentar-terminal")]
30use super::types::{IndexHealthMetrics, RelevanceMetrics};
31
32#[cfg(feature = "presentar-terminal")]
34const CYAN: Color = Color { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
35
36#[derive(Debug, Clone)]
38pub struct QueryRecord {
39 pub timestamp_ms: u64,
41 pub query: String,
43 pub component: String,
45 pub latency_ms: u64,
47 pub success: bool,
49}
50
51#[cfg(feature = "presentar-terminal")]
53pub struct OracleDashboard {
54 pub index_health: IndexHealthMetrics,
56 pub query_history: VecDeque<QueryRecord>,
58 pub latency_samples: Vec<u64>,
60 pub retrieval_metrics: RelevanceMetrics,
62 selected_component: usize,
64 max_history: usize,
66 refresh_interval: Duration,
68 buffer: CellBuffer,
70 renderer: DiffRenderer,
72 width: u16,
74 height: u16,
76}
77
78#[cfg(feature = "presentar-terminal")]
79impl OracleDashboard {
80 pub fn new() -> Self {
82 let (width, height) = crossterm::terminal::size().unwrap_or((100, 30));
83 Self {
84 index_health: IndexHealthMetrics::default(),
85 query_history: VecDeque::new(),
86 latency_samples: Vec::new(),
87 retrieval_metrics: RelevanceMetrics::default(),
88 selected_component: 0,
89 max_history: 100,
90 refresh_interval: Duration::from_millis(100),
91 buffer: CellBuffer::new(width, height),
92 renderer: DiffRenderer::new(),
93 width,
94 height,
95 }
96 }
97
98 pub fn record_query(&mut self, record: QueryRecord) {
100 self.latency_samples.push(record.latency_ms);
101 if self.latency_samples.len() > 50 {
102 self.latency_samples.remove(0);
103 }
104 self.query_history.push_front(record);
105 if self.query_history.len() > self.max_history {
106 self.query_history.pop_back();
107 }
108 }
109
110 pub fn update_health(&mut self, health: IndexHealthMetrics) {
112 self.index_health = health;
113 }
114
115 pub fn run(&mut self) -> anyhow::Result<()> {
117 enable_raw_mode()?;
118 let mut stdout = io::stdout();
119 execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
120
121 let result = self.run_loop(&mut stdout);
122
123 disable_raw_mode()?;
124 execute!(stdout, LeaveAlternateScreen, cursor::Show)?;
125
126 result
127 }
128
129 fn run_loop(&mut self, stdout: &mut io::Stdout) -> anyhow::Result<()> {
131 loop {
132 let (w, h) = crossterm::terminal::size().unwrap_or((100, 30));
134 if w != self.width || h != self.height {
135 self.width = w;
136 self.height = h;
137 self.buffer.resize(w, h);
138 self.renderer.reset();
139 }
140
141 self.buffer.clear();
143 self.render();
144
145 self.renderer.flush(&mut self.buffer, stdout)?;
147 stdout.flush()?;
148
149 if event::poll(self.refresh_interval)? {
150 let event = event::read()?;
151 let Event::Key(key) = event else { continue };
152 if key.kind != KeyEventKind::Press {
153 continue;
154 }
155 match key.code {
156 KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
157 KeyCode::Up | KeyCode::Char('k') => {
158 if self.selected_component > 0 {
159 self.selected_component -= 1;
160 }
161 }
162 KeyCode::Down | KeyCode::Char('j') => {
163 let max = self.index_health.docs_per_component.len().saturating_sub(1);
164 if self.selected_component < max {
165 self.selected_component += 1;
166 }
167 }
168 KeyCode::Char('r') => {
169 }
171 _ => {}
172 }
173 }
174 }
175 }
176
177 fn render(&mut self) {
179 let w = self.width;
180 let h = self.height;
181
182 let header_h: u16 = 3;
184 let help_h: u16 = 1;
185 let history_h: u16 = 8;
186 let panels_h = h.saturating_sub(header_h + history_h + help_h);
187
188 self.render_header(0, 0, w, header_h);
189 self.render_panels(0, header_h, w, panels_h);
190 self.render_history(0, header_h + panels_h, w, history_h);
191 self.render_help(0, h.saturating_sub(help_h), w, help_h);
192 }
193
194 fn write_str(&mut self, x: u16, y: u16, s: &str, fg: Color) {
196 let mut cx = x;
197 for ch in s.chars() {
198 if cx >= self.width {
199 break;
200 }
201 let mut buf = [0u8; 4];
202 let s = ch.encode_utf8(&mut buf);
203 self.buffer.update(cx, y, s, fg, Color::TRANSPARENT, Modifiers::NONE);
204 cx = cx.saturating_add(1);
205 }
206 }
207
208 fn write_str_clipped(&mut self, x: u16, y: u16, s: &str, panel_w: u16, fg: Color) {
210 let max_len = panel_w.saturating_sub(2) as usize;
211 self.write_str(x, y, &s[..s.len().min(max_len)], fg);
212 }
213
214 fn set_char(&mut self, x: u16, y: u16, ch: char, fg: Color) {
216 if x < self.width && y < self.height {
217 let mut buf = [0u8; 4];
218 let s = ch.encode_utf8(&mut buf);
219 self.buffer.update(x, y, s, fg, Color::TRANSPARENT, Modifiers::NONE);
220 }
221 }
222
223 fn render_header(&mut self, x: u16, y: u16, w: u16, _h: u16) {
225 let coverage = self.index_health.coverage_percent;
226 let total_docs: usize = self.index_health.docs_per_component.iter().map(|(_, c)| c).sum();
227
228 self.draw_box(x, y, w, 3, " Oracle RAG Dashboard ");
230
231 let bar_width = w.saturating_sub(4) as usize;
233 let filled = ((coverage as usize) * bar_width / 100).min(bar_width);
234 let color = self.health_color(coverage);
235
236 let label = format!("Index Health: {}% | Docs: {}", coverage, total_docs);
237
238 let bar = format_bar_segments(filled, bar_width);
240 self.write_str(x + 2, y + 1, &bar[..bar_width.min(bar.len())], color);
241
242 let label_x = x + 2 + ((bar_width.saturating_sub(label.len())) / 2) as u16;
244 self.write_str(label_x, y + 1, &label, color);
245 }
246
247 fn render_panels(&mut self, x: u16, y: u16, w: u16, h: u16) {
249 let panel_w = w / 3;
250
251 self.render_index_status(x, y, panel_w, h);
252 self.render_latency(x + panel_w, y, panel_w, h);
253 self.render_quality(x + 2 * panel_w, y, w.saturating_sub(2 * panel_w), h);
254 }
255
256 fn render_index_status(&mut self, x: u16, y: u16, w: u16, h: u16) {
258 self.draw_box(x, y, w, h, " Index Status ");
259
260 let content_y = y + 1;
261 let content_h = h.saturating_sub(2) as usize;
262
263 let rows: Vec<_> = self
265 .index_health
266 .docs_per_component
267 .iter()
268 .take(content_h)
269 .enumerate()
270 .map(|(i, (name, count))| {
271 let bar = render_bar(*count, 500, 15);
272 let (marker, color) = if i == self.selected_component {
273 (">", Color::YELLOW)
274 } else {
275 (" ", Color::WHITE)
276 };
277 let line = format!("{} {:12} {} {}", marker, name, bar, count);
278 (i, line, color)
279 })
280 .collect();
281
282 for (i, line, color) in rows {
283 self.write_str_clipped(x + 1, content_y + i as u16, &line, w, color);
284 }
285 }
286
287 fn render_latency(&mut self, x: u16, y: u16, w: u16, h: u16) {
289 self.draw_box(x, y, w, h, " Query Latency ");
290
291 let sparkline_h = h.saturating_sub(4) as usize;
293 let spark_w = w.saturating_sub(2) as usize;
294
295 let points: Vec<(u16, u16)> = if !self.latency_samples.is_empty() && sparkline_h > 0 {
296 let max_val = *self.latency_samples.iter().max().unwrap_or(&1);
297 self.latency_samples
298 .iter()
299 .rev()
300 .take(spark_w)
301 .enumerate()
302 .flat_map(|(i, &val)| {
303 let bar_h = if max_val > 0 {
304 ((val as usize) * sparkline_h / (max_val as usize)).min(sparkline_h)
305 } else {
306 0
307 };
308 (0..bar_h).filter_map(move |j| {
309 let cy = y + 1 + (sparkline_h - 1 - j) as u16;
310 let cx = x + 1 + i as u16;
311 if cx < x + w - 1 {
312 Some((cx, cy))
313 } else {
314 None
315 }
316 })
317 })
318 .collect()
319 } else {
320 Vec::new()
321 };
322
323 for (cx, cy) in points {
324 self.set_char(cx, cy, '▄', CYAN);
325 }
326
327 let (avg, p99) = if !self.latency_samples.is_empty() {
329 let sum: u64 = self.latency_samples.iter().sum();
330 let avg = sum / self.latency_samples.len() as u64;
331 let mut sorted = self.latency_samples.clone();
332 sorted.sort();
333 let p99_idx = (sorted.len() as f64 * 0.99) as usize;
334 let p99 = sorted.get(p99_idx.min(sorted.len() - 1)).copied().unwrap_or(0);
335 (avg, p99)
336 } else {
337 (0, 0)
338 };
339
340 let stats = format!("avg: {}ms p99: {}ms", avg, p99);
341 self.write_str(x + 1, y + h - 2, &stats, Color::WHITE);
342 }
343
344 fn render_quality(&mut self, x: u16, y: u16, w: u16, h: u16) {
346 self.draw_box(x, y, w, h, " Retrieval Quality ");
347
348 let metrics = &self.retrieval_metrics;
349 let content_y = y + 1;
350
351 let rows =
352 [("MRR", metrics.mrr), ("NDCG", metrics.ndcg_at_k), ("R@10", metrics.recall_at_k)];
353
354 for (i, (label, value)) in rows.iter().enumerate() {
355 let bar = render_bar((*value * 100.0) as usize, 100, 12);
356 let line = format!("{:5} {:.3} {}", label, value, bar);
357 self.write_str_clipped(x + 1, content_y + i as u16, &line, w, Color::WHITE);
358 }
359 }
360
361 fn render_history(&mut self, x: u16, y: u16, w: u16, h: u16) {
363 self.draw_box(x, y, w, h, " Recent Queries ");
364
365 let header = "Time Query Component Latency";
367 self.write_str_clipped(x + 1, y + 1, header, w, Color::YELLOW);
368
369 let rows: Vec<_> = self
371 .query_history
372 .iter()
373 .take(h.saturating_sub(3) as usize)
374 .enumerate()
375 .map(|(i, record)| {
376 let time = format_timestamp(record.timestamp_ms);
377 let (status_char, color) =
378 if record.success { ('+', Color::GREEN) } else { ('x', Color::RED) };
379 let line = format!(
380 "{} {:30} {:12} {:>6}ms {}",
381 time,
382 truncate_query(&record.query, 30),
383 record.component,
384 record.latency_ms,
385 status_char
386 );
387 (i, line, color)
388 })
389 .collect();
390
391 let content_y = y + 2;
392 for (i, line, color) in rows {
393 self.write_str_clipped(x + 1, content_y + i as u16, &line, w, color);
394 }
395 }
396
397 fn render_help(&mut self, x: u16, y: u16, w: u16, _h: u16) {
399 let help = " [q]uit [r]efresh [↑/↓]navigate ";
400 let gray = Color::new(0.5, 0.5, 0.5, 1.0);
401 self.write_str(x, y, &help[..help.len().min(w as usize)], gray);
402 }
403
404 fn draw_box(&mut self, x: u16, y: u16, w: u16, h: u16, title: &str) {
406 if w < 2 || h < 2 {
407 return;
408 }
409
410 self.set_char(x, y, '┌', Color::WHITE);
412 for i in 1..w - 1 {
413 self.set_char(x + i, y, '─', Color::WHITE);
414 }
415 self.set_char(x + w - 1, y, '┐', Color::WHITE);
416
417 if !title.is_empty() && w > title.len() as u16 + 2 {
419 let title_x = x + 2;
420 self.write_str(title_x, y, title, CYAN);
421 }
422
423 for i in 1..h - 1 {
425 self.set_char(x, y + i, '│', Color::WHITE);
426 self.set_char(x + w - 1, y + i, '│', Color::WHITE);
427 }
428
429 self.set_char(x, y + h - 1, '└', Color::WHITE);
431 for i in 1..w - 1 {
432 self.set_char(x + i, y + h - 1, '─', Color::WHITE);
433 }
434 self.set_char(x + w - 1, y + h - 1, '┘', Color::WHITE);
435 }
436
437 fn health_color(&self, percent: u16) -> Color {
439 match percent {
440 0..=60 => Color::RED,
441 61..=80 => Color::YELLOW,
442 _ => Color::GREEN,
443 }
444 }
445}
446
447#[cfg(feature = "presentar-terminal")]
448impl Default for OracleDashboard {
449 fn default() -> Self {
450 Self::new()
451 }
452}
453
454fn format_bar_segments(filled: usize, width: usize) -> String {
456 let clamped = filled.min(width);
457 let empty = width.saturating_sub(clamped);
458 format!("{}{}", "\u{2588}".repeat(clamped), "\u{2591}".repeat(empty))
459}
460
461fn render_bar(value: usize, max: usize, width: usize) -> String {
463 let filled = if max > 0 { (value * width / max).min(width) } else { 0 };
464 format_bar_segments(filled, width)
465}
466
467fn format_timestamp(timestamp_ms: u64) -> String {
469 let secs = timestamp_ms / 1000;
470 let hours = (secs / 3600) % 24;
471 let mins = (secs / 60) % 60;
472 let secs = secs % 60;
473 format!("{:02}:{:02}:{:02}", hours, mins, secs)
474}
475
476fn truncate_query(query: &str, max_len: usize) -> String {
478 if query.len() <= max_len {
479 query.to_string()
480 } else {
481 format!("{}...", &query[..max_len - 3])
482 }
483}
484
485pub mod inline {
487 use super::format_bar_segments;
488
489 pub fn bar(value: f64, max: f64, width: usize) -> String {
491 let filled = if max > 0.0 { ((value / max) * width as f64) as usize } else { 0 };
492 format_bar_segments(filled, width)
493 }
494
495 pub fn sparkline(values: &[f64]) -> String {
497 const BARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
498
499 if values.is_empty() {
500 return String::new();
501 }
502
503 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
504 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
505 let range = max - min;
506
507 values
508 .iter()
509 .map(|v| {
510 let idx = if range == 0.0 { 0 } else { ((v - min) / range * 7.0) as usize };
511 BARS[idx.min(7)]
512 })
513 .collect()
514 }
515
516 pub fn score_bar(score: f64, width: usize) -> String {
518 let pct = (score * 100.0) as usize;
519 format!("{} {:3}%", bar(score, 1.0, width), pct)
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 fn make_record(query: &str, latency_ms: u64, success: bool) -> QueryRecord {
529 QueryRecord {
530 timestamp_ms: 0,
531 query: query.to_string(),
532 component: "test".to_string(),
533 latency_ms,
534 success,
535 }
536 }
537
538 fn count_bar_char(bar: &str, ch: char) -> usize {
540 bar.chars().filter(|c| *c == ch).count()
541 }
542
543 #[test]
544 fn test_render_bar() {
545 let bar = render_bar(50, 100, 10);
546 assert_eq!(count_bar_char(&bar, '\u{2588}'), 5);
547 assert_eq!(count_bar_char(&bar, '\u{2591}'), 5);
548 }
549
550 #[test]
551 fn test_render_bar_full() {
552 let bar = render_bar(100, 100, 10);
553 assert_eq!(count_bar_char(&bar, '\u{2588}'), 10);
554 }
555
556 #[test]
557 fn test_render_bar_empty() {
558 let bar = render_bar(0, 100, 10);
559 assert_eq!(count_bar_char(&bar, '\u{2591}'), 10);
560 }
561
562 #[test]
563 fn test_format_timestamp() {
564 let ts = format_timestamp(45296000); assert_eq!(ts, "12:34:56");
566 }
567
568 #[test]
569 fn test_truncate_query_short() {
570 let q = truncate_query("short", 10);
571 assert_eq!(q, "short");
572 }
573
574 #[test]
575 fn test_truncate_query_long() {
576 let q = truncate_query("this is a very long query", 15);
577 assert!(q.ends_with("..."));
578 assert!(q.len() <= 15);
579 }
580
581 #[test]
582 fn test_inline_bar() {
583 let bar = inline::bar(0.5, 1.0, 10);
584 assert_eq!(count_bar_char(&bar, '\u{2588}'), 5);
585 }
586
587 #[test]
588 fn test_inline_sparkline() {
589 let spark = inline::sparkline(&[0.0, 0.5, 1.0, 0.5, 0.0]);
590 assert_eq!(spark.chars().count(), 5);
591 assert!(spark.contains('\u{2581}'));
592 assert!(spark.contains('\u{2588}'));
593 }
594
595 #[test]
596 fn test_inline_sparkline_empty() {
597 let spark = inline::sparkline(&[]);
598 assert!(spark.is_empty());
599 }
600
601 #[test]
602 fn test_inline_score_bar() {
603 let bar = inline::score_bar(0.85, 10);
604 assert!(bar.contains("85%"));
605 }
606
607 #[cfg(feature = "presentar-terminal")]
608 #[test]
609 fn test_dashboard_creation() {
610 let dashboard = OracleDashboard::new();
611 assert!(dashboard.query_history.is_empty());
612 assert!(dashboard.latency_samples.is_empty());
613 }
614
615 #[cfg(feature = "presentar-terminal")]
616 #[test]
617 fn test_dashboard_record_query() {
618 let mut dashboard = OracleDashboard::new();
619 let mut record = make_record("test query", 50, true);
620 record.timestamp_ms = 1234567890;
621 record.component = "trueno".to_string();
622 dashboard.record_query(record);
623
624 assert_eq!(dashboard.query_history.len(), 1);
625 assert_eq!(dashboard.latency_samples.len(), 1);
626 }
627
628 #[cfg(feature = "presentar-terminal")]
629 #[test]
630 fn test_dashboard_default() {
631 let dashboard = OracleDashboard::default();
632 assert!(dashboard.query_history.is_empty());
633 assert_eq!(dashboard.selected_component, 0);
634 }
635
636 #[cfg(feature = "presentar-terminal")]
637 #[test]
638 fn test_dashboard_update_health() {
639 let mut dashboard = OracleDashboard::new();
640 let health = IndexHealthMetrics {
641 coverage_percent: 85,
642 docs_per_component: vec![("trueno".to_string(), 100)],
643 component_names: vec!["trueno".to_string()],
644 latency_samples: vec![10, 20, 30],
645 mrr_history: vec![0.8, 0.85],
646 ndcg_history: vec![0.9, 0.92],
647 freshness_score: 95.0,
648 };
649
650 dashboard.update_health(health);
651 assert_eq!(dashboard.index_health.coverage_percent, 85);
652 assert_eq!(dashboard.index_health.docs_per_component.len(), 1);
653 }
654
655 #[cfg(feature = "presentar-terminal")]
656 #[test]
657 fn test_dashboard_latency_samples_bounded() {
658 let mut dashboard = OracleDashboard::new();
659
660 for i in 0..60 {
661 let mut record = make_record(&format!("query {}", i), i as u64 * 10, true);
662 record.timestamp_ms = i as u64;
663 dashboard.record_query(record);
664 }
665
666 assert_eq!(dashboard.latency_samples.len(), 50);
667 }
668
669 #[cfg(feature = "presentar-terminal")]
670 #[test]
671 fn test_dashboard_query_history_bounded() {
672 let mut dashboard = OracleDashboard::new();
673
674 for i in 0..110 {
675 let mut record = make_record(&format!("query {}", i), 10, i % 2 == 0);
676 record.timestamp_ms = i as u64;
677 dashboard.record_query(record);
678 }
679
680 assert_eq!(dashboard.query_history.len(), 100);
681 }
682
683 #[cfg(feature = "presentar-terminal")]
684 #[test]
685 fn test_dashboard_query_order() {
686 let mut dashboard = OracleDashboard::new();
687
688 let mut first = make_record("first", 10, true);
689 first.timestamp_ms = 100;
690 dashboard.record_query(first);
691
692 let mut second = make_record("second", 20, true);
693 second.timestamp_ms = 200;
694 dashboard.record_query(second);
695
696 assert_eq!(dashboard.query_history.front().expect("unexpected failure").query, "second");
697 assert_eq!(dashboard.query_history.back().expect("unexpected failure").query, "first");
698 }
699
700 #[test]
701 fn test_render_bar_overflow() {
702 let bar = render_bar(200, 100, 10);
703 assert_eq!(count_bar_char(&bar, '\u{2588}'), 10);
704 }
705
706 #[test]
707 fn test_render_bar_zero_max() {
708 let bar = render_bar(50, 0, 10);
709 assert_eq!(count_bar_char(&bar, '\u{2591}'), 10);
710 }
711
712 #[test]
713 fn test_format_timestamp_edge() {
714 assert_eq!(format_timestamp(0), "00:00:00");
715 assert_eq!(format_timestamp(86399000), "23:59:59");
716 }
717
718 #[test]
719 fn test_truncate_query_exact() {
720 let q = truncate_query("exactly_ten", 10);
721 assert!(q.len() <= 10);
722 }
723
724 #[test]
725 fn test_truncate_query_unicode() {
726 let q = truncate_query("hello world test", 10);
727 assert!(q.len() <= 10);
728 }
729
730 #[test]
731 fn test_inline_bar_zero() {
732 let bar = inline::bar(0.0, 1.0, 10);
733 assert_eq!(count_bar_char(&bar, '\u{2591}'), 10);
734 }
735
736 #[test]
737 fn test_inline_bar_full() {
738 let bar = inline::bar(1.0, 1.0, 10);
739 assert_eq!(count_bar_char(&bar, '\u{2588}'), 10);
740 }
741
742 #[test]
743 fn test_inline_bar_zero_max() {
744 let bar = inline::bar(0.5, 0.0, 10);
745 assert_eq!(count_bar_char(&bar, '\u{2591}'), 10);
746 }
747
748 #[test]
749 fn test_inline_sparkline_constant() {
750 let spark = inline::sparkline(&[5.0, 5.0, 5.0]);
751 assert_eq!(spark.chars().count(), 3);
752 let chars: Vec<char> = spark.chars().collect();
753 assert_eq!(chars[0], chars[1]);
754 assert_eq!(chars[1], chars[2]);
755 }
756
757 #[test]
758 fn test_inline_sparkline_single() {
759 let spark = inline::sparkline(&[1.0]);
760 assert_eq!(spark.chars().count(), 1);
761 }
762
763 #[test]
764 fn test_inline_score_bar_zero() {
765 let bar = inline::score_bar(0.0, 10);
766 assert!(bar.contains("0%"));
767 }
768
769 #[test]
770 fn test_inline_score_bar_full() {
771 let bar = inline::score_bar(1.0, 10);
772 assert!(bar.contains("100%"));
773 }
774
775 #[test]
776 fn test_query_record_fields() {
777 let mut record = make_record("test", 50, false);
778 record.timestamp_ms = 1000;
779 record.component = "comp".to_string();
780
781 assert_eq!(record.timestamp_ms, 1000);
782 assert_eq!(record.query, "test");
783 assert_eq!(record.component, "comp");
784 assert_eq!(record.latency_ms, 50);
785 assert!(!record.success);
786 }
787
788 #[cfg(feature = "presentar-terminal")]
789 #[test]
790 fn test_health_color_red() {
791 let dashboard = OracleDashboard::new();
792 let color = dashboard.health_color(50);
793 assert_eq!(color, Color::RED);
794 }
795
796 #[cfg(feature = "presentar-terminal")]
797 #[test]
798 fn test_health_color_yellow() {
799 let dashboard = OracleDashboard::new();
800 let color = dashboard.health_color(75);
801 assert_eq!(color, Color::YELLOW);
802 }
803
804 #[cfg(feature = "presentar-terminal")]
805 #[test]
806 fn test_health_color_green() {
807 let dashboard = OracleDashboard::new();
808 let color = dashboard.health_color(90);
809 assert_eq!(color, Color::GREEN);
810 }
811}