1use crate::table::{OxurTable, Tabled};
7use oxur_repl::cache::CacheStats as ArtifactCacheStats;
8use oxur_repl::eval::{get_resource_stats, EvalMetrics, ExecutionTier};
9use oxur_repl::metrics::SessionStatsSnapshot;
10use oxur_repl::session::DirStats;
11
12#[allow(clippy::too_many_arguments)]
16pub fn show_all_stats(
17 collector: &EvalMetrics,
18 dir_stats: Option<&DirStats>,
19 cache_stats: Option<&ArtifactCacheStats>,
20 server_snapshot: Option<&oxur_repl::metrics::ServerMetricsSnapshot>,
21 client_snapshot: Option<&oxur_repl::metrics::ClientMetricsSnapshot>,
22 subprocess_snapshot: Option<&oxur_repl::metrics::SubprocessMetricsSnapshot>,
23 usage_snapshot: Option<&oxur_repl::metrics::UsageMetricsSnapshot>,
24 color_enabled: bool,
25) -> String {
26 let mut output = String::new();
27
28 output.push_str(&show_execution_details(collector, color_enabled));
30
31 output.push_str(&show_cache_stats(collector, color_enabled));
33
34 if dir_stats.is_some() || cache_stats.is_some() {
36 output.push('\n');
37 output.push_str(&show_resource_stats(dir_stats, cache_stats, color_enabled));
38 }
39
40 if let Some(client) = client_snapshot {
42 output.push_str(&show_client_stats(client, color_enabled));
43 }
44
45 if let Some(usage) = usage_snapshot {
47 output.push('\n');
48 output.push_str(&show_usage_stats(usage, color_enabled));
49 }
50
51 if let Some(subprocess) = subprocess_snapshot {
53 output.push_str(&show_subprocess_stats(subprocess, color_enabled));
54 }
55
56 if let Some(server) = server_snapshot {
58 output.push('\n');
59 output.push_str(&show_server_stats(server, color_enabled));
60 }
61
62 output
63}
64
65pub fn show_execution_details(collector: &EvalMetrics, color_enabled: bool) -> String {
67 let mut output = String::new();
68
69 output.push_str(&header("Execution Statistics", color_enabled));
70 output.push('\n');
71
72 for tier in [ExecutionTier::Calculator, ExecutionTier::CachedLoaded, ExecutionTier::JustInTime]
73 {
74 if let Some(p) = collector.percentiles(tier) {
75 #[derive(Tabled)]
76 struct Metric {
77 #[tabled(rename = "Metric")]
78 metric: String,
79 #[tabled(rename = "Value (ms) ")]
80 value: String,
81 }
82
83 let metrics = vec![
84 Metric { metric: " Count ".to_string(), value: format!(" {} ", p.count) },
85 Metric { metric: " Min ".to_string(), value: format!(" {:.2} ", p.min) },
86 Metric { metric: " p50 (median) ".to_string(), value: format!(" {:.2} ", p.p50) },
87 Metric { metric: " p95 ".to_string(), value: format!(" {:.2} ", p.p95) },
88 Metric { metric: " p99 ".to_string(), value: format!(" {:.2} ", p.p99) },
89 Metric { metric: " Max ".to_string(), value: format!(" {:.2} ", p.max) },
90 ];
91
92 output.push_str(
93 &OxurTable::new(metrics).with_title(tier_name(tier)).with_footer().render(),
94 );
95 output.push_str("\n\n");
96 }
97 }
98
99 output
100}
101
102pub fn show_cache_stats(collector: &EvalMetrics, color_enabled: bool) -> String {
104 let mut output = String::new();
105
106 output.push_str(&header("Cache Statistics", color_enabled));
107 output.push('\n');
108
109 let cache = collector.cache_stats();
111
112 #[derive(Tabled)]
113 struct CacheMetric {
114 #[tabled(rename = "Metric")]
115 metric: String,
116 #[tabled(rename = "Value ")]
117 value: String,
118 }
119
120 let metrics = vec![
121 CacheMetric { metric: " Hits ".to_string(), value: format!(" {} ", cache.hits) },
122 CacheMetric { metric: " Misses ".to_string(), value: format!(" {} ", cache.misses) },
123 CacheMetric {
124 metric: " Hit Rate ".to_string(),
125 value: format!(" {:.1}% ", cache.hit_rate),
126 },
127 ];
128
129 output.push_str(&OxurTable::new(metrics).with_title("EVALUATION CACHE").with_footer().render());
130 output.push('\n');
131
132 output
133}
134
135pub fn show_resource_stats(
137 dir_stats: Option<&DirStats>,
138 cache_stats: Option<&ArtifactCacheStats>,
139 color_enabled: bool,
140) -> String {
141 let mut output = String::new();
142
143 output.push_str(&header("Resource Usage", color_enabled));
144 output.push('\n');
145
146 if let Some(resource_stats) = get_resource_stats() {
148 #[derive(Tabled)]
149 struct MemoryMetric {
150 #[tabled(rename = "Metric")]
151 metric: String,
152 #[tabled(rename = "Value ")]
153 value: String,
154 }
155
156 let metrics = vec![
157 MemoryMetric {
158 metric: " Process RSS ".to_string(),
159 value: format!(" {} ", format_bytes(resource_stats.process_memory_bytes)),
160 },
161 MemoryMetric {
162 metric: " Virtual Memory ".to_string(),
163 value: format!(" {} ", format_bytes(resource_stats.virtual_memory_bytes)),
164 },
165 MemoryMetric {
166 metric: " Process ID ".to_string(),
167 value: format!(" {} ", resource_stats.pid),
168 },
169 ];
170
171 output.push_str(&OxurTable::new(metrics).with_title("MEMORY").with_footer().render());
172 output.push_str("\n\n");
173 } else {
174 output.push_str("Memory stats unavailable\n\n");
175 }
176
177 if let Some(dir_stats) = dir_stats {
179 let location_type = if dir_stats.is_tmpfs { " (tmpfs)" } else { "" };
180
181 #[derive(Tabled)]
182 struct DirMetric {
183 #[tabled(rename = "Metric")]
184 metric: String,
185 #[tabled(rename = "Value ")]
186 value: String,
187 }
188
189 let metrics = vec![
190 DirMetric {
191 metric: " Location ".to_string(),
192 value: format!(" {}{} ", dir_stats.path.display(), location_type),
193 },
194 DirMetric {
195 metric: " Files ".to_string(),
196 value: format!(" {} ", dir_stats.file_count),
197 },
198 DirMetric {
199 metric: " Disk Usage ".to_string(),
200 value: format!(" {} ", format_bytes(dir_stats.total_bytes)),
201 },
202 ];
203
204 output.push_str(
205 &OxurTable::new(metrics).with_title("SESSION DIRECTORY").with_footer().render(),
206 );
207 output.push_str("\n\n");
208 } else {
209 output.push_str("Session directory not initialized\n\n");
210 }
211
212 if let Some(cache_stats) = cache_stats {
214 #[derive(Tabled)]
215 struct ArtifactMetric {
216 #[tabled(rename = "Metric")]
217 metric: String,
218 #[tabled(rename = "Value ")]
219 value: String,
220 }
221
222 let age_seconds = if cache_stats.entry_count > 0 {
223 let now = std::time::SystemTime::now()
224 .duration_since(std::time::UNIX_EPOCH)
225 .unwrap()
226 .as_secs();
227 now.saturating_sub(cache_stats.oldest_entry_secs)
228 } else {
229 0
230 };
231
232 let metrics = vec![
233 ArtifactMetric {
234 metric: " Entries ".to_string(),
235 value: format!(" {} ", cache_stats.entry_count),
236 },
237 ArtifactMetric {
238 metric: " Total Size ".to_string(),
239 value: format!(" {} ", format_bytes(cache_stats.total_size_bytes)),
240 },
241 ArtifactMetric {
242 metric: " Oldest Entry ".to_string(),
243 value: if cache_stats.entry_count > 0 {
244 format!(" {} ", format_duration(age_seconds))
245 } else {
246 " N/A ".to_string()
247 },
248 },
249 ArtifactMetric {
250 metric: " Cache Directory ".to_string(),
251 value: format!(" {} ", cache_stats.cache_dir.display()),
252 },
253 ];
254
255 output.push_str(
256 &OxurTable::new(metrics).with_title("ARTIFACT CACHE (Global)").with_footer().render(),
257 );
258 output.push_str("\n\n");
259 } else {
260 output.push_str("Artifact cache not initialized\n\n");
261 }
262
263 output
264}
265
266pub fn show_sessions(
268 sessions: &[oxur_repl::server::SessionInfo],
269 current_session_id: &oxur_repl::protocol::SessionId,
270 color_enabled: bool,
271) -> String {
272 let mut output = String::new();
273
274 output.push_str(&header("Sessions", color_enabled));
275 output.push('\n');
276
277 if sessions.is_empty() {
278 output.push_str("No active sessions\n");
279 return output;
280 }
281
282 #[derive(Tabled)]
283 struct SessionRow {
284 #[tabled(rename = "ID")]
285 id: String,
286 #[tabled(rename = "Name")]
287 name: String,
288 #[tabled(rename = "Active")]
289 active: String,
290 #[tabled(rename = "Evals")]
291 evals: String,
292 #[tabled(rename = "Last Active")]
293 last_active: String,
294 }
295
296 let rows: Vec<SessionRow> = sessions
297 .iter()
298 .map(|s| {
299 let is_current = s.id == *current_session_id;
300 let active_marker = if is_current { " * " } else { " " };
301
302 let now = std::time::SystemTime::now()
304 .duration_since(std::time::UNIX_EPOCH)
305 .unwrap()
306 .as_millis() as u64;
307 let elapsed_ms = now.saturating_sub(s.last_active_at);
308 let last_active = if elapsed_ms < 60_000 {
309 " just now ".to_string()
310 } else if elapsed_ms < 3_600_000 {
311 format!(" {} min ago ", elapsed_ms / 60_000)
312 } else if elapsed_ms < 86_400_000 {
313 format!(" {} hr ago ", elapsed_ms / 3_600_000)
314 } else {
315 format!(" {} days ago ", elapsed_ms / 86_400_000)
316 };
317
318 SessionRow {
319 id: format!(" {} ", s.id),
320 name: format!(" {} ", s.name.clone().unwrap_or_else(|| "-".to_string())),
321 active: active_marker.to_string(),
322 evals: format!(" {} ", s.eval_count),
323 last_active,
324 }
325 })
326 .collect();
327
328 output.push_str(&OxurTable::new(rows).with_title("ACTIVE SESSIONS").with_footer().render());
329 output.push('\n');
330
331 output
332}
333
334pub fn show_usage_stats(
336 usage_snapshot: &oxur_repl::metrics::UsageMetricsSnapshot,
337 color_enabled: bool,
338) -> String {
339 let mut output = String::new();
340
341 output.push_str(&header("Usage Statistics", color_enabled));
342 output.push('\n');
343
344 #[derive(Tabled)]
345 struct CommandMetric {
346 #[tabled(rename = "Command")]
347 command: String,
348 #[tabled(rename = "Count")]
349 count: String,
350 #[tabled(rename = "Percentage")]
351 percentage: String,
352 }
353
354 let total = usage_snapshot.total_commands as f64;
355 let calc_pct = |count: u64| {
356 if total > 0.0 {
357 format!(" {:.1}% ", (count as f64 / total) * 100.0)
358 } else {
359 " 0.0% ".to_string()
360 }
361 };
362
363 let mut metrics = vec![
364 CommandMetric {
365 command: " Eval ".to_string(),
366 count: format!(" {} ", usage_snapshot.eval_count),
367 percentage: calc_pct(usage_snapshot.eval_count),
368 },
369 CommandMetric {
370 command: " Help ".to_string(),
371 count: format!(" {} ", usage_snapshot.help_count),
372 percentage: calc_pct(usage_snapshot.help_count),
373 },
374 CommandMetric {
375 command: " Stats ".to_string(),
376 count: format!(" {} ", usage_snapshot.stats_count),
377 percentage: calc_pct(usage_snapshot.stats_count),
378 },
379 CommandMetric {
380 command: " Info ".to_string(),
381 count: format!(" {} ", usage_snapshot.info_count),
382 percentage: calc_pct(usage_snapshot.info_count),
383 },
384 CommandMetric {
385 command: " Sessions ".to_string(),
386 count: format!(" {} ", usage_snapshot.sessions_count),
387 percentage: calc_pct(usage_snapshot.sessions_count),
388 },
389 CommandMetric {
390 command: " Clear ".to_string(),
391 count: format!(" {} ", usage_snapshot.clear_count),
392 percentage: calc_pct(usage_snapshot.clear_count),
393 },
394 CommandMetric {
395 command: " Banner ".to_string(),
396 count: format!(" {} ", usage_snapshot.banner_count),
397 percentage: calc_pct(usage_snapshot.banner_count),
398 },
399 ];
400
401 metrics.push(CommandMetric {
403 command: " Total Commands: ".to_string(),
404 count: format!(" {} ", usage_snapshot.total_commands),
405 percentage: " ".to_string(),
406 });
407
408 output
409 .push_str(&OxurTable::new(metrics).with_title("COMMAND FREQUENCY").with_footer().render());
410 output.push_str("\n\n");
411
412 output
413}
414
415pub fn show_client_stats(
417 client_snapshot: &oxur_repl::metrics::ClientMetricsSnapshot,
418 color_enabled: bool,
419) -> String {
420 let mut output = String::new();
421
422 output.push_str(&header("Client Statistics", color_enabled));
423 output.push('\n');
424
425 #[derive(Tabled)]
426 struct RequestMetric {
427 #[tabled(rename = "Metric")]
428 metric: String,
429 #[tabled(rename = "Value ")]
430 value: String,
431 }
432
433 let metrics = vec![
435 RequestMetric {
436 metric: " Total Requests ".to_string(),
437 value: format!(" {} ", client_snapshot.requests_total),
438 },
439 RequestMetric {
440 metric: " Total Responses ".to_string(),
441 value: format!(" {} ", client_snapshot.responses_total),
442 },
443 RequestMetric {
444 metric: " Success Responses ".to_string(),
445 value: format!(" {} ", client_snapshot.responses_success),
446 },
447 RequestMetric {
448 metric: " Error Responses ".to_string(),
449 value: format!(" {} ", client_snapshot.responses_error),
450 },
451 ];
452
453 output.push_str(
454 &OxurTable::new(metrics).with_title("REQUESTS & RESPONSES").with_footer().render(),
455 );
456 output.push_str("\n\n");
457
458 #[derive(Tabled)]
459 struct LatencyMetric {
460 #[tabled(rename = "Metric")]
461 metric: String,
462 #[tabled(rename = "Value (ms)")]
463 value: String,
464 }
465
466 let latency_metrics = vec![
468 LatencyMetric {
469 metric: " Average ".to_string(),
470 value: format!(" {:.2} ", client_snapshot.average_latency_ms),
471 },
472 LatencyMetric {
473 metric: " P50 ".to_string(),
474 value: format!(" {:.2} ", client_snapshot.p50_latency_ms),
475 },
476 LatencyMetric {
477 metric: " P95 ".to_string(),
478 value: format!(" {:.2} ", client_snapshot.p95_latency_ms),
479 },
480 LatencyMetric {
481 metric: " P99 ".to_string(),
482 value: format!(" {:.2} ", client_snapshot.p99_latency_ms),
483 },
484 LatencyMetric {
485 metric: " Min ".to_string(),
486 value: format!(" {:.2} ", client_snapshot.min_latency_ms),
487 },
488 LatencyMetric {
489 metric: " Max ".to_string(),
490 value: format!(" {:.2} ", client_snapshot.max_latency_ms),
491 },
492 ];
493
494 output.push_str(
495 &OxurTable::new(latency_metrics).with_title("LATENCY DISTRIBUTION").with_footer().render(),
496 );
497 output.push('\n');
498
499 output
500}
501
502pub fn show_server_stats(
504 server_snapshot: &oxur_repl::metrics::ServerMetricsSnapshot,
505 color_enabled: bool,
506) -> String {
507 let mut output = String::new();
508
509 output.push_str(&header("Server Statistics", color_enabled));
510 output.push('\n');
511
512 #[derive(Tabled)]
513 struct ConnectionMetric {
514 #[tabled(rename = "Metric")]
515 metric: String,
516 #[tabled(rename = "Value ")]
517 value: String,
518 }
519
520 let metrics = vec![
522 ConnectionMetric {
523 metric: " Total Connections ".to_string(),
524 value: format!(" {} ", server_snapshot.connections_total),
525 },
526 ConnectionMetric {
527 metric: " Active Connections ".to_string(),
528 value: format!(" {} ", server_snapshot.connections_active),
529 },
530 ];
531
532 output.push_str(&OxurTable::new(metrics).with_title("CONNECTIONS").with_footer().render());
533 output.push_str("\n\n");
534
535 let metrics = vec![
537 ConnectionMetric {
538 metric: " Total Sessions ".to_string(),
539 value: format!(" {} ", server_snapshot.sessions_total),
540 },
541 ConnectionMetric {
542 metric: " Active Sessions ".to_string(),
543 value: format!(" {} ", server_snapshot.sessions_active),
544 },
545 ];
546
547 output.push_str(&OxurTable::new(metrics).with_title("SESSIONS").with_footer().render());
548 output.push_str("\n\n");
549
550 let success_rate = if server_snapshot.responses_total > 0 {
552 (server_snapshot.responses_success as f64 / server_snapshot.responses_total as f64) * 100.0
553 } else {
554 0.0
555 };
556
557 let metrics = vec![
558 ConnectionMetric {
559 metric: " Total Requests ".to_string(),
560 value: format!(" {} ", server_snapshot.requests_total),
561 },
562 ConnectionMetric {
563 metric: " Total Responses ".to_string(),
564 value: format!(" {} ", server_snapshot.responses_total),
565 },
566 ConnectionMetric {
567 metric: " Successful ".to_string(),
568 value: format!(" {} ", server_snapshot.responses_success),
569 },
570 ConnectionMetric {
571 metric: " Errors ".to_string(),
572 value: format!(" {} ", server_snapshot.responses_error),
573 },
574 ConnectionMetric {
575 metric: " Success Rate ".to_string(),
576 value: format!(" {:.1}% ", success_rate),
577 },
578 ];
579
580 output.push_str(
581 &OxurTable::new(metrics).with_title("REQUESTS & RESPONSES").with_footer().render(),
582 );
583 output.push_str("\n\n");
584
585 output
586}
587
588pub fn show_subprocess_stats(
590 subprocess_snapshot: &oxur_repl::metrics::SubprocessMetricsSnapshot,
591 color_enabled: bool,
592) -> String {
593 let mut output = String::new();
594
595 output.push_str(&header("Subprocess Statistics", color_enabled));
596 output.push('\n');
597
598 #[derive(Tabled)]
599 struct SubprocessMetric {
600 #[tabled(rename = "Metric")]
601 metric: String,
602 #[tabled(rename = "Value ")]
603 value: String,
604 }
605
606 let status = if subprocess_snapshot.is_running { "Running" } else { "Stopped" };
607 let uptime = format_uptime_seconds(subprocess_snapshot.uptime_seconds);
608 let last_reason = subprocess_snapshot
609 .last_restart_reason
610 .map(|r| r.to_string())
611 .unwrap_or_else(|| "N/A".to_string());
612
613 let metrics = vec![
614 SubprocessMetric { metric: " Status ".to_string(), value: format!(" {} ", status) },
615 SubprocessMetric { metric: " Uptime ".to_string(), value: format!(" {} ", uptime) },
616 SubprocessMetric {
617 metric: " Restart Count ".to_string(),
618 value: format!(" {} ", subprocess_snapshot.restart_count),
619 },
620 SubprocessMetric {
621 metric: " Last Restart Reason ".to_string(),
622 value: format!(" {} ", last_reason),
623 },
624 ];
625
626 output.push_str(&OxurTable::new(metrics).with_title("STATUS").with_footer().render());
627 output.push_str("\n\n");
628
629 output
630}
631
632pub fn show_session_summary_from_snapshot(
640 snapshot: &SessionStatsSnapshot,
641 color_enabled: bool,
642) -> String {
643 let mut output = String::new();
644
645 output.push_str(&header("Session Statistics", color_enabled));
647 output.push('\n');
648
649 output.push_str(§ion("SUMMARY", color_enabled));
651 output.push_str(&format!("Total Evaluations: {}\n", snapshot.total_evaluations));
652 output.push_str(&format!(
653 "Cache Hit Rate: {:.1}% ({} hits, {} misses)\n\n",
654 snapshot.cache.hit_rate, snapshot.cache.hits, snapshot.cache.misses
655 ));
656
657 #[derive(Tabled)]
659 struct TierMetric {
660 #[tabled(rename = "Tier")]
661 tier: String,
662 #[tabled(rename = "Count ")]
663 count: String,
664 #[tabled(rename = "P50 (ms)")]
665 p50: String,
666 #[tabled(rename = "P95 (ms) ")]
667 p95: String,
668 #[tabled(rename = "P99 (ms) ")]
669 p99: String,
670 }
671
672 let mut metrics = Vec::new();
673
674 if let Some(ref p) = snapshot.tier1_percentiles {
676 metrics.push(TierMetric {
677 tier: " Calculator ".to_string(),
678 count: format!(" {} ", p.count),
679 p50: format!(" {:.2} ", p.p50),
680 p95: format!(" {:.2} ", p.p95),
681 p99: format!(" {:.2} ", p.p99),
682 });
683 }
684
685 if let Some(ref p) = snapshot.tier2_percentiles {
687 metrics.push(TierMetric {
688 tier: " Cached ".to_string(),
689 count: format!(" {} ", p.count),
690 p50: format!(" {:.2} ", p.p50),
691 p95: format!(" {:.2} ", p.p95),
692 p99: format!(" {:.2} ", p.p99),
693 });
694 }
695
696 if let Some(ref p) = snapshot.tier3_percentiles {
698 metrics.push(TierMetric {
699 tier: " JIT ".to_string(),
700 count: format!(" {} ", p.count),
701 p50: format!(" {:.2} ", p.p50),
702 p95: format!(" {:.2} ", p.p95),
703 p99: format!(" {:.2} ", p.p99),
704 });
705 }
706
707 if !metrics.is_empty() {
708 output.push_str(
709 &OxurTable::new(metrics).with_title("EXECUTION TIERS").with_footer().render(),
710 );
711 output.push('\n');
712 } else {
713 output.push_str("No execution data yet.\n\n");
714 }
715
716 output
717}
718
719pub fn show_execution_from_snapshot(
721 snapshot: &SessionStatsSnapshot,
722 color_enabled: bool,
723) -> String {
724 let mut output = String::new();
725
726 output.push_str(&header("Execution Statistics", color_enabled));
727 output.push('\n');
728
729 let display_tier =
731 |output: &mut String, name: &str, percentiles: &Option<oxur_repl::metrics::Percentiles>| {
732 if let Some(ref p) = percentiles {
733 #[derive(Tabled)]
734 struct Metric {
735 #[tabled(rename = "Metric")]
736 metric: String,
737 #[tabled(rename = "Value (ms)")]
738 value: String,
739 }
740
741 let metrics = vec![
742 Metric { metric: " Count ".to_string(), value: format!(" {} ", p.count) },
743 Metric { metric: " Min ".to_string(), value: format!(" {:.2} ", p.min) },
744 Metric {
745 metric: " p50 (median) ".to_string(),
746 value: format!(" {:.2} ", p.p50),
747 },
748 Metric { metric: " p95 ".to_string(), value: format!(" {:.2} ", p.p95) },
749 Metric { metric: " p99 ".to_string(), value: format!(" {:.2} ", p.p99) },
750 Metric { metric: " Max ".to_string(), value: format!(" {:.2} ", p.max) },
751 ];
752
753 output.push_str(&OxurTable::new(metrics).with_title(name).with_footer().render());
754 output.push_str("\n\n");
755 }
756 };
757
758 display_tier(&mut output, "TIER 1: CALCULATOR (~1ms)", &snapshot.tier1_percentiles);
759 display_tier(&mut output, "TIER 2: CACHED LOADED (~1-5ms)", &snapshot.tier2_percentiles);
760 display_tier(&mut output, "TIER 3: JUST-IN-TIME (~50-300ms)", &snapshot.tier3_percentiles);
761
762 output
763}
764
765pub fn show_cache_from_snapshot(snapshot: &SessionStatsSnapshot, color_enabled: bool) -> String {
767 let mut output = String::new();
768
769 output.push_str(&header("Cache Statistics", color_enabled));
770 output.push('\n');
771
772 #[derive(Tabled)]
774 struct CacheMetric {
775 #[tabled(rename = "Metric")]
776 metric: String,
777 #[tabled(rename = "Value")]
778 value: String,
779 }
780
781 let metrics = vec![
782 CacheMetric { metric: " Hits ".to_string(), value: format!(" {} ", snapshot.cache.hits) },
783 CacheMetric {
784 metric: " Misses ".to_string(),
785 value: format!(" {} ", snapshot.cache.misses),
786 },
787 CacheMetric {
788 metric: " Hit Rate ".to_string(),
789 value: format!(" {:.1}% ", snapshot.cache.hit_rate),
790 },
791 ];
792
793 output.push_str(&OxurTable::new(metrics).with_title("EVALUATION CACHE").with_footer().render());
794 output.push('\n');
795
796 output
797}
798
799pub fn parse_stats_command(
810 input: &str,
811 collector: &EvalMetrics,
812 color_enabled: bool,
813) -> Option<String> {
814 if input == "(stats)" {
815 return Some(show_all_stats(collector, None, None, None, None, None, None, color_enabled));
816 }
817
818 if input == "(stats execution)" {
819 return Some(show_execution_details(collector, color_enabled));
820 }
821
822 if input == "(stats cache)" {
823 return Some(show_cache_stats(collector, color_enabled));
824 }
825
826 None
827}
828
829pub fn parse_stats_command_with_resources(
833 input: &str,
834 collector: &EvalMetrics,
835 dir_stats: Option<&DirStats>,
836 cache_stats: Option<&ArtifactCacheStats>,
837 color_enabled: bool,
838) -> Option<String> {
839 if input == "(stats resources)" {
840 return Some(show_resource_stats(dir_stats, cache_stats, color_enabled));
841 }
842
843 parse_stats_command(input, collector, color_enabled)
845}
846
847fn header(text: &str, color_enabled: bool) -> String {
850 if color_enabled {
851 format!("\x1b[1;36m{}\x1b[0m\n{}\n", text, "═".repeat(text.len()))
852 } else {
853 format!("{}\n{}\n", text, "=".repeat(text.len()))
854 }
855}
856
857fn section(title: &str, color_enabled: bool) -> String {
858 if color_enabled {
859 format!("\x1b[1;36m{}\x1b[0m\n{}\n", title, "─".repeat(title.len()))
860 } else {
861 format!("{}\n{}\n", title, "-".repeat(title.len()))
862 }
863}
864
865fn tier_name(tier: ExecutionTier) -> String {
866 match tier {
867 ExecutionTier::Calculator => "TIER 1: CALCULATOR (~1ms)".to_string(),
868 ExecutionTier::CachedLoaded => "TIER 2: CACHED LOADED (~1-5ms)".to_string(),
869 ExecutionTier::JustInTime => "TIER 3: JUST-IN-TIME (~50-300ms)".to_string(),
870 _ => "UNKNOWN TIER".to_string(), }
872}
873
874fn format_bytes(bytes: u64) -> String {
875 const KB: u64 = 1024;
876 const MB: u64 = KB * 1024;
877 const GB: u64 = MB * 1024;
878
879 if bytes >= GB {
880 format!("{:.2} GB", bytes as f64 / GB as f64)
881 } else if bytes >= MB {
882 format!("{:.2} MB", bytes as f64 / MB as f64)
883 } else if bytes >= KB {
884 format!("{:.2} KB", bytes as f64 / KB as f64)
885 } else {
886 format!("{} bytes", bytes)
887 }
888}
889
890fn format_duration(seconds: u64) -> String {
891 const MINUTE: u64 = 60;
892 const HOUR: u64 = MINUTE * 60;
893 const DAY: u64 = HOUR * 24;
894
895 if seconds >= DAY {
896 format!("{} days ago", seconds / DAY)
897 } else if seconds >= HOUR {
898 format!("{} hours ago", seconds / HOUR)
899 } else if seconds >= MINUTE {
900 format!("{} minutes ago", seconds / MINUTE)
901 } else {
902 format!("{} seconds ago", seconds)
903 }
904}
905
906fn format_uptime_seconds(seconds: f64) -> String {
907 let secs = seconds as u64;
908 const MINUTE: u64 = 60;
909 const HOUR: u64 = MINUTE * 60;
910 const DAY: u64 = HOUR * 24;
911
912 if secs >= DAY {
913 let days = secs / DAY;
914 let hours = (secs % DAY) / HOUR;
915 format!("{}d {}h", days, hours)
916 } else if secs >= HOUR {
917 let hours = secs / HOUR;
918 let mins = (secs % HOUR) / MINUTE;
919 format!("{}h {}m", hours, mins)
920 } else if secs >= MINUTE {
921 let mins = secs / MINUTE;
922 let s = secs % MINUTE;
923 format!("{}m {}s", mins, s)
924 } else {
925 format!("{:.1}s", seconds)
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use std::time::Duration;
933
934 #[test]
935 fn test_display_session_summary() {
936 let mut collector = EvalMetrics::new("test");
937 collector.record(ExecutionTier::Calculator, false, Duration::from_millis(1));
938 collector.record(ExecutionTier::CachedLoaded, true, Duration::from_millis(2));
939
940 let output = show_all_stats(&collector, None, None, None, None, None, None, false);
941
942 assert!(output.contains("Execution Statistics"));
944 assert!(output.contains("Cache Statistics"));
945 assert!(output.contains("TIER 1: CALCULATOR"));
946 assert!(output.contains("EVALUATION CACHE"));
947 }
948
949 #[test]
950 fn test_display_execution_details() {
951 let mut collector = EvalMetrics::new("test");
952 collector.record(ExecutionTier::Calculator, false, Duration::from_millis(1));
953 collector.record(ExecutionTier::Calculator, false, Duration::from_millis(2));
954
955 let output = show_execution_details(&collector, false);
956
957 assert!(output.contains("Execution Statistics"));
958 assert!(output.contains("TIER 1: CALCULATOR"));
959 }
960
961 #[test]
962 fn test_display_cache_stats() {
963 let mut collector = EvalMetrics::new("test");
964 collector.record(ExecutionTier::CachedLoaded, true, Duration::from_millis(2));
965
966 let output = show_cache_stats(&collector, false);
967
968 assert!(output.contains("Cache Statistics"));
969 assert!(output.contains("EVALUATION CACHE"));
970 }
971
972 #[test]
973 fn test_parse_stats_command_summary() {
974 let collector = EvalMetrics::new("test");
975
976 let result = parse_stats_command("(stats)", &collector, false);
977 assert!(result.is_some());
978 let output = result.unwrap();
980 assert!(output.contains("Execution Statistics"));
981 assert!(output.contains("Cache Statistics"));
982 }
983
984 #[test]
985 fn test_parse_stats_command_execution() {
986 let collector = EvalMetrics::new("test");
987
988 let result = parse_stats_command("(stats execution)", &collector, false);
989 assert!(result.is_some());
990 assert!(result.unwrap().contains("Execution Statistics"));
991 }
992
993 #[test]
994 fn test_parse_stats_command_cache() {
995 let collector = EvalMetrics::new("test");
996
997 let result = parse_stats_command("(stats cache)", &collector, false);
998 assert!(result.is_some());
999 assert!(result.unwrap().contains("Cache Statistics"));
1000 }
1001
1002 #[test]
1003 fn test_parse_stats_command_invalid() {
1004 let collector = EvalMetrics::new("test");
1005
1006 let result = parse_stats_command("(stats invalid)", &collector, false);
1007 assert!(result.is_none());
1008
1009 let result = parse_stats_command("(not-stats)", &collector, false);
1010 assert!(result.is_none());
1011 }
1012
1013 #[test]
1016 fn test_format_bytes_bytes() {
1017 assert_eq!(format_bytes(0), "0 bytes");
1018 assert_eq!(format_bytes(100), "100 bytes");
1019 assert_eq!(format_bytes(1023), "1023 bytes");
1020 }
1021
1022 #[test]
1023 fn test_format_bytes_kilobytes() {
1024 assert_eq!(format_bytes(1024), "1.00 KB");
1025 assert_eq!(format_bytes(2048), "2.00 KB");
1026 assert_eq!(format_bytes(1536), "1.50 KB");
1027 assert_eq!(format_bytes(1024 * 1024 - 1), "1024.00 KB");
1028 }
1029
1030 #[test]
1031 fn test_format_bytes_megabytes() {
1032 assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
1033 assert_eq!(format_bytes(2 * 1024 * 1024), "2.00 MB");
1034 assert_eq!(format_bytes(1024 * 1024 * 1024 - 1), "1024.00 MB");
1035 }
1036
1037 #[test]
1038 fn test_format_bytes_gigabytes() {
1039 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
1040 assert_eq!(format_bytes(2 * 1024 * 1024 * 1024), "2.00 GB");
1041 }
1042
1043 #[test]
1044 fn test_format_duration_seconds() {
1045 assert_eq!(format_duration(0), "0 seconds ago");
1046 assert_eq!(format_duration(30), "30 seconds ago");
1047 assert_eq!(format_duration(59), "59 seconds ago");
1048 }
1049
1050 #[test]
1051 fn test_format_duration_minutes() {
1052 assert_eq!(format_duration(60), "1 minutes ago");
1053 assert_eq!(format_duration(120), "2 minutes ago");
1054 assert_eq!(format_duration(3599), "59 minutes ago");
1055 }
1056
1057 #[test]
1058 fn test_format_duration_hours() {
1059 assert_eq!(format_duration(3600), "1 hours ago");
1060 assert_eq!(format_duration(7200), "2 hours ago");
1061 assert_eq!(format_duration(86399), "23 hours ago");
1062 }
1063
1064 #[test]
1065 fn test_format_duration_days() {
1066 assert_eq!(format_duration(86400), "1 days ago");
1067 assert_eq!(format_duration(172800), "2 days ago");
1068 assert_eq!(format_duration(604800), "7 days ago");
1069 }
1070
1071 #[test]
1072 fn test_format_uptime_seconds_subsecond() {
1073 assert_eq!(format_uptime_seconds(0.5), "0.5s");
1074 assert_eq!(format_uptime_seconds(30.5), "30.5s");
1075 assert_eq!(format_uptime_seconds(59.9), "59.9s");
1076 }
1077
1078 #[test]
1079 fn test_format_uptime_seconds_minutes() {
1080 assert_eq!(format_uptime_seconds(60.0), "1m 0s");
1081 assert_eq!(format_uptime_seconds(90.0), "1m 30s");
1082 assert_eq!(format_uptime_seconds(3599.0), "59m 59s");
1083 }
1084
1085 #[test]
1086 fn test_format_uptime_seconds_hours() {
1087 assert_eq!(format_uptime_seconds(3600.0), "1h 0m");
1088 assert_eq!(format_uptime_seconds(5400.0), "1h 30m");
1089 assert_eq!(format_uptime_seconds(86399.0), "23h 59m");
1090 }
1091
1092 #[test]
1093 fn test_format_uptime_seconds_days() {
1094 assert_eq!(format_uptime_seconds(86400.0), "1d 0h");
1095 assert_eq!(format_uptime_seconds(129600.0), "1d 12h");
1096 assert_eq!(format_uptime_seconds(172800.0), "2d 0h");
1097 }
1098
1099 #[test]
1100 fn test_header_with_color() {
1101 let result = header("Test", true);
1102 assert!(result.contains("Test"));
1103 assert!(result.contains("\x1b[1;36m")); assert!(result.contains("\x1b[0m")); assert!(result.contains("═")); }
1107
1108 #[test]
1109 fn test_header_without_color() {
1110 let result = header("Test", false);
1111 assert!(result.contains("Test"));
1112 assert!(!result.contains("\x1b")); assert!(result.contains("=")); }
1115
1116 #[test]
1117 fn test_section_with_color() {
1118 let result = section("Section", true);
1119 assert!(result.contains("Section"));
1120 assert!(result.contains("\x1b[1;36m")); assert!(result.contains("─")); }
1123
1124 #[test]
1125 fn test_section_without_color() {
1126 let result = section("Section", false);
1127 assert!(result.contains("Section"));
1128 assert!(!result.contains("\x1b")); assert!(result.contains("-")); }
1131
1132 #[test]
1133 fn test_tier_name_calculator() {
1134 let name = tier_name(ExecutionTier::Calculator);
1135 assert!(name.contains("CALCULATOR"));
1136 assert!(name.contains("TIER 1"));
1137 }
1138
1139 #[test]
1140 fn test_tier_name_cached() {
1141 let name = tier_name(ExecutionTier::CachedLoaded);
1142 assert!(name.contains("CACHED"));
1143 assert!(name.contains("TIER 2"));
1144 }
1145
1146 #[test]
1147 fn test_tier_name_jit() {
1148 let name = tier_name(ExecutionTier::JustInTime);
1149 assert!(name.contains("JUST-IN-TIME"));
1150 assert!(name.contains("TIER 3"));
1151 }
1152
1153 #[test]
1156 fn test_show_resource_stats_no_data() {
1157 let output = show_resource_stats(None, None, false);
1158 assert!(output.contains("Resource Usage"));
1160 assert!(output.contains("Session directory not initialized"));
1161 assert!(output.contains("Artifact cache not initialized"));
1162 }
1163
1164 #[test]
1165 fn test_show_sessions_empty() {
1166 let sessions: Vec<oxur_repl::server::SessionInfo> = vec![];
1167 let current = oxur_repl::protocol::SessionId::new("test");
1168
1169 let output = show_sessions(&sessions, ¤t, false);
1170 assert!(output.contains("Sessions"));
1171 assert!(output.contains("No active sessions"));
1172 }
1173
1174 #[test]
1175 fn test_show_usage_stats() {
1176 let usage = oxur_repl::metrics::UsageMetricsSnapshot {
1177 session_id: "test".to_string(),
1178 eval_count: 10,
1179 help_count: 5,
1180 stats_count: 3,
1181 info_count: 2,
1182 sessions_count: 1,
1183 clear_count: 0,
1184 banner_count: 1,
1185 total_commands: 22,
1186 };
1187
1188 let output = show_usage_stats(&usage, false);
1189 assert!(output.contains("Usage Statistics"));
1190 assert!(output.contains("COMMAND FREQUENCY"));
1191 assert!(output.contains("Eval"));
1192 assert!(output.contains("10"));
1193 }
1194
1195 #[test]
1196 fn test_show_client_stats() {
1197 let client = oxur_repl::metrics::ClientMetricsSnapshot {
1198 requests_total: 100,
1199 responses_total: 98,
1200 responses_success: 95,
1201 responses_error: 3,
1202 average_latency_ms: 10.5,
1203 p50_latency_ms: 8.0,
1204 p95_latency_ms: 25.0,
1205 p99_latency_ms: 50.0,
1206 min_latency_ms: 1.0,
1207 max_latency_ms: 100.0,
1208 };
1209
1210 let output = show_client_stats(&client, false);
1211 assert!(output.contains("Client Statistics"));
1212 assert!(output.contains("REQUESTS & RESPONSES"));
1213 assert!(output.contains("LATENCY DISTRIBUTION"));
1214 assert!(output.contains("100")); }
1216
1217 #[test]
1218 fn test_show_server_stats() {
1219 let server = oxur_repl::metrics::ServerMetricsSnapshot {
1220 connections_total: 50,
1221 connections_active: 2,
1222 sessions_total: 30,
1223 sessions_active: 5,
1224 requests_total: 1000,
1225 responses_total: 998,
1226 responses_success: 990,
1227 responses_error: 8,
1228 };
1229
1230 let output = show_server_stats(&server, false);
1231 assert!(output.contains("Server Statistics"));
1232 assert!(output.contains("CONNECTIONS"));
1233 assert!(output.contains("SESSIONS"));
1234 assert!(output.contains("Success Rate"));
1235 }
1236
1237 #[test]
1238 fn test_show_subprocess_stats_running() {
1239 let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1240 is_running: true,
1241 uptime_seconds: 3600.0,
1242 restart_count: 2,
1243 last_restart_reason: None,
1244 };
1245
1246 let output = show_subprocess_stats(&subprocess, false);
1247 assert!(output.contains("Subprocess Statistics"));
1248 assert!(output.contains("Running"));
1249 assert!(output.contains("1h 0m")); }
1251
1252 #[test]
1253 fn test_show_subprocess_stats_stopped() {
1254 let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1255 is_running: false,
1256 uptime_seconds: 0.0,
1257 restart_count: 0,
1258 last_restart_reason: None,
1259 };
1260
1261 let output = show_subprocess_stats(&subprocess, false);
1262 assert!(output.contains("Stopped"));
1263 }
1264
1265 #[test]
1266 fn test_parse_stats_command_with_resources() {
1267 let collector = EvalMetrics::new("test");
1268
1269 let result =
1271 parse_stats_command_with_resources("(stats resources)", &collector, None, None, false);
1272 assert!(result.is_some());
1273 assert!(result.unwrap().contains("Resource Usage"));
1274
1275 let result = parse_stats_command_with_resources("(stats)", &collector, None, None, false);
1277 assert!(result.is_some());
1278
1279 let result =
1281 parse_stats_command_with_resources("(stats invalid)", &collector, None, None, false);
1282 assert!(result.is_none());
1283 }
1284
1285 #[test]
1288 fn test_show_session_summary_from_snapshot_empty() {
1289 let snapshot = SessionStatsSnapshot {
1290 session_id: "test".to_string(),
1291 total_evaluations: 0,
1292 cache: oxur_repl::metrics::CacheStats { hits: 0, misses: 0, hit_rate: 0.0 },
1293 tier1_percentiles: None,
1294 tier2_percentiles: None,
1295 tier3_percentiles: None,
1296 parse_errors: 0,
1297 compile_errors: 0,
1298 runtime_errors: 0,
1299 average_eval_time_ms: 0.0,
1300 };
1301
1302 let output = show_session_summary_from_snapshot(&snapshot, false);
1303 assert!(output.contains("Session Statistics"));
1304 assert!(output.contains("Total Evaluations: 0"));
1305 assert!(output.contains("No execution data yet"));
1306 }
1307
1308 #[test]
1309 fn test_show_session_summary_from_snapshot_with_data() {
1310 let snapshot = SessionStatsSnapshot {
1311 session_id: "test".to_string(),
1312 total_evaluations: 100,
1313 cache: oxur_repl::metrics::CacheStats { hits: 80, misses: 20, hit_rate: 80.0 },
1314 tier1_percentiles: Some(oxur_repl::metrics::Percentiles {
1315 count: 50,
1316 min: 0.5,
1317 p50: 1.0,
1318 p95: 2.0,
1319 p99: 3.0,
1320 max: 5.0,
1321 }),
1322 tier2_percentiles: None,
1323 tier3_percentiles: None,
1324 parse_errors: 0,
1325 compile_errors: 0,
1326 runtime_errors: 0,
1327 average_eval_time_ms: 1.5,
1328 };
1329
1330 let output = show_session_summary_from_snapshot(&snapshot, false);
1331 assert!(output.contains("Total Evaluations: 100"));
1332 assert!(output.contains("80.0%")); assert!(output.contains("EXECUTION TIERS"));
1334 assert!(output.contains("Calculator"));
1335 }
1336
1337 #[test]
1338 fn test_show_execution_from_snapshot() {
1339 let snapshot = SessionStatsSnapshot {
1340 session_id: "test".to_string(),
1341 total_evaluations: 10,
1342 cache: oxur_repl::metrics::CacheStats { hits: 5, misses: 5, hit_rate: 50.0 },
1343 tier1_percentiles: Some(oxur_repl::metrics::Percentiles {
1344 count: 5,
1345 min: 0.5,
1346 p50: 1.0,
1347 p95: 2.0,
1348 p99: 3.0,
1349 max: 5.0,
1350 }),
1351 tier2_percentiles: Some(oxur_repl::metrics::Percentiles {
1352 count: 3,
1353 min: 1.0,
1354 p50: 2.0,
1355 p95: 4.0,
1356 p99: 5.0,
1357 max: 8.0,
1358 }),
1359 tier3_percentiles: Some(oxur_repl::metrics::Percentiles {
1360 count: 2,
1361 min: 50.0,
1362 p50: 100.0,
1363 p95: 200.0,
1364 p99: 250.0,
1365 max: 300.0,
1366 }),
1367 parse_errors: 0,
1368 compile_errors: 0,
1369 runtime_errors: 0,
1370 average_eval_time_ms: 50.0,
1371 };
1372
1373 let output = show_execution_from_snapshot(&snapshot, false);
1374 assert!(output.contains("Execution Statistics"));
1375 assert!(output.contains("TIER 1: CALCULATOR"));
1376 assert!(output.contains("TIER 2: CACHED LOADED"));
1377 assert!(output.contains("TIER 3: JUST-IN-TIME"));
1378 }
1379
1380 #[test]
1381 fn test_show_cache_from_snapshot() {
1382 let snapshot = SessionStatsSnapshot {
1383 session_id: "test".to_string(),
1384 total_evaluations: 100,
1385 cache: oxur_repl::metrics::CacheStats { hits: 75, misses: 25, hit_rate: 75.0 },
1386 tier1_percentiles: None,
1387 tier2_percentiles: None,
1388 tier3_percentiles: None,
1389 parse_errors: 0,
1390 compile_errors: 0,
1391 runtime_errors: 0,
1392 average_eval_time_ms: 0.0,
1393 };
1394
1395 let output = show_cache_from_snapshot(&snapshot, false);
1396 assert!(output.contains("Cache Statistics"));
1397 assert!(output.contains("EVALUATION CACHE"));
1398 assert!(output.contains("75")); assert!(output.contains("25")); assert!(output.contains("75.0%")); }
1402
1403 #[test]
1404 fn test_show_all_stats_with_optional_data() {
1405 let collector = EvalMetrics::new("test");
1406
1407 let server = oxur_repl::metrics::ServerMetricsSnapshot {
1409 connections_total: 10,
1410 connections_active: 1,
1411 sessions_total: 5,
1412 sessions_active: 1,
1413 requests_total: 100,
1414 responses_total: 100,
1415 responses_success: 99,
1416 responses_error: 1,
1417 };
1418
1419 let client = oxur_repl::metrics::ClientMetricsSnapshot {
1420 requests_total: 50,
1421 responses_total: 50,
1422 responses_success: 49,
1423 responses_error: 1,
1424 average_latency_ms: 5.0,
1425 p50_latency_ms: 4.0,
1426 p95_latency_ms: 10.0,
1427 p99_latency_ms: 20.0,
1428 min_latency_ms: 1.0,
1429 max_latency_ms: 30.0,
1430 };
1431
1432 let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1433 is_running: true,
1434 uptime_seconds: 120.0,
1435 restart_count: 1,
1436 last_restart_reason: None,
1437 };
1438
1439 let usage = oxur_repl::metrics::UsageMetricsSnapshot {
1440 session_id: "test".to_string(),
1441 eval_count: 25,
1442 help_count: 5,
1443 stats_count: 3,
1444 info_count: 2,
1445 sessions_count: 1,
1446 clear_count: 0,
1447 banner_count: 1,
1448 total_commands: 37,
1449 };
1450
1451 let output = show_all_stats(
1452 &collector,
1453 None,
1454 None,
1455 Some(&server),
1456 Some(&client),
1457 Some(&subprocess),
1458 Some(&usage),
1459 false,
1460 );
1461
1462 assert!(output.contains("Execution Statistics"));
1463 assert!(output.contains("Cache Statistics"));
1464 assert!(output.contains("Server Statistics"));
1465 assert!(output.contains("Client Statistics"));
1466 assert!(output.contains("Subprocess Statistics"));
1467 assert!(output.contains("Usage Statistics"));
1468 }
1469
1470 #[test]
1473 fn test_show_sessions_with_data() {
1474 use oxur_repl::protocol::ReplMode;
1475 use std::time::{SystemTime, UNIX_EPOCH};
1476
1477 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
1478
1479 let sessions = vec![
1480 oxur_repl::server::SessionInfo {
1481 id: oxur_repl::protocol::SessionId::new("session-1"),
1482 name: Some("Main".to_string()),
1483 mode: ReplMode::Lisp,
1484 eval_count: 42,
1485 created_at: now - 3600_000, last_active_at: now - 30_000, timeout_ms: 300_000,
1488 },
1489 oxur_repl::server::SessionInfo {
1490 id: oxur_repl::protocol::SessionId::new("session-2"),
1491 name: None, mode: ReplMode::Sexpr,
1493 eval_count: 10,
1494 created_at: now - 7200_000,
1495 last_active_at: now - 120_000, timeout_ms: 300_000,
1497 },
1498 oxur_repl::server::SessionInfo {
1499 id: oxur_repl::protocol::SessionId::new("session-3"),
1500 name: Some("Old".to_string()),
1501 mode: ReplMode::Lisp,
1502 eval_count: 5,
1503 created_at: now - 172800_000,
1504 last_active_at: now - 7200_000, timeout_ms: 300_000,
1506 },
1507 oxur_repl::server::SessionInfo {
1508 id: oxur_repl::protocol::SessionId::new("session-4"),
1509 name: Some("Very Old".to_string()),
1510 mode: ReplMode::Lisp,
1511 eval_count: 1,
1512 created_at: now - 259200_000,
1513 last_active_at: now - 172800_000, timeout_ms: 300_000,
1515 },
1516 ];
1517 let current = oxur_repl::protocol::SessionId::new("session-1");
1518
1519 let output = show_sessions(&sessions, ¤t, false);
1520 assert!(output.contains("Sessions"));
1521 assert!(output.contains("ACTIVE SESSIONS"));
1522 assert!(output.contains("session-1"));
1523 assert!(output.contains("Main"));
1524 assert!(output.contains("42")); assert!(output.contains("just now")); assert!(output.contains("min ago")); assert!(output.contains("hr ago")); assert!(output.contains("days ago")); }
1530
1531 #[test]
1532 fn test_show_resource_stats_with_dir_stats() {
1533 use std::path::PathBuf;
1534
1535 let dir_stats = oxur_repl::session::DirStats {
1536 file_count: 15,
1537 total_bytes: 102400,
1538 is_tmpfs: true,
1539 path: PathBuf::from("/tmp/oxur-session"),
1540 };
1541
1542 let output = show_resource_stats(Some(&dir_stats), None, false);
1543 assert!(output.contains("Resource Usage"));
1544 assert!(output.contains("SESSION DIRECTORY"));
1545 assert!(output.contains("15")); assert!(output.contains("100.00 KB")); assert!(output.contains("tmpfs")); }
1549
1550 #[test]
1551 fn test_show_resource_stats_with_cache_stats() {
1552 use std::path::PathBuf;
1553 use std::time::{SystemTime, UNIX_EPOCH};
1554
1555 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
1556
1557 let cache_stats = oxur_repl::cache::CacheStats {
1558 entry_count: 25,
1559 total_size_bytes: 5242880,
1560 oldest_entry_secs: now - 3600, newest_entry_secs: now,
1562 cache_dir: PathBuf::from("/home/user/.cache/oxur"),
1563 };
1564
1565 let output = show_resource_stats(None, Some(&cache_stats), false);
1566 assert!(output.contains("Resource Usage"));
1567 assert!(output.contains("ARTIFACT CACHE"));
1568 assert!(output.contains("25")); assert!(output.contains("5.00 MB")); }
1571
1572 #[test]
1573 fn test_show_resource_stats_with_empty_cache() {
1574 use std::path::PathBuf;
1575
1576 let cache_stats = oxur_repl::cache::CacheStats {
1577 entry_count: 0,
1578 total_size_bytes: 0,
1579 oldest_entry_secs: 0,
1580 newest_entry_secs: 0,
1581 cache_dir: PathBuf::from("/home/user/.cache/oxur"),
1582 };
1583
1584 let output = show_resource_stats(None, Some(&cache_stats), false);
1585 assert!(output.contains("ARTIFACT CACHE"));
1586 assert!(output.contains("N/A")); }
1588
1589 #[test]
1590 fn test_show_resource_stats_dir_not_tmpfs() {
1591 use std::path::PathBuf;
1592
1593 let dir_stats = oxur_repl::session::DirStats {
1594 file_count: 5,
1595 total_bytes: 1024,
1596 is_tmpfs: false, path: PathBuf::from("/var/oxur-session"),
1598 };
1599
1600 let output = show_resource_stats(Some(&dir_stats), None, false);
1601 assert!(output.contains("SESSION DIRECTORY"));
1602 assert!(!output.contains("tmpfs")); }
1604
1605 #[test]
1606 fn test_show_usage_stats_zero_commands() {
1607 let usage = oxur_repl::metrics::UsageMetricsSnapshot {
1608 session_id: "test".to_string(),
1609 eval_count: 0,
1610 help_count: 0,
1611 stats_count: 0,
1612 info_count: 0,
1613 sessions_count: 0,
1614 clear_count: 0,
1615 banner_count: 0,
1616 total_commands: 0,
1617 };
1618
1619 let output = show_usage_stats(&usage, false);
1620 assert!(output.contains("Usage Statistics"));
1621 assert!(output.contains("0.0%")); }
1623
1624 #[test]
1625 fn test_show_subprocess_stats_with_restart_reason() {
1626 let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1627 is_running: true,
1628 uptime_seconds: 7200.0,
1629 restart_count: 3,
1630 last_restart_reason: Some(oxur_repl::metrics::RestartReason::UserRequested),
1631 };
1632
1633 let output = show_subprocess_stats(&subprocess, false);
1634 assert!(output.contains("Subprocess Statistics"));
1635 assert!(output.contains("2h 0m")); assert!(output.contains("3")); assert!(output.contains("user requested")); }
1639
1640 #[test]
1641 fn test_show_server_stats_zero_responses() {
1642 let server = oxur_repl::metrics::ServerMetricsSnapshot {
1643 connections_total: 5,
1644 connections_active: 0,
1645 sessions_total: 2,
1646 sessions_active: 0,
1647 requests_total: 10,
1648 responses_total: 0, responses_success: 0,
1650 responses_error: 0,
1651 };
1652
1653 let output = show_server_stats(&server, false);
1654 assert!(output.contains("Server Statistics"));
1655 assert!(output.contains("0.0%")); }
1657
1658 #[test]
1659 fn test_show_session_summary_from_snapshot_with_tier2_and_tier3() {
1660 let snapshot = SessionStatsSnapshot {
1661 session_id: "test".to_string(),
1662 total_evaluations: 100,
1663 cache: oxur_repl::metrics::CacheStats { hits: 50, misses: 50, hit_rate: 50.0 },
1664 tier1_percentiles: None, tier2_percentiles: Some(oxur_repl::metrics::Percentiles {
1666 count: 30,
1667 min: 1.0,
1668 p50: 5.0,
1669 p95: 10.0,
1670 p99: 15.0,
1671 max: 20.0,
1672 }),
1673 tier3_percentiles: Some(oxur_repl::metrics::Percentiles {
1674 count: 20,
1675 min: 50.0,
1676 p50: 100.0,
1677 p95: 200.0,
1678 p99: 300.0,
1679 max: 400.0,
1680 }),
1681 parse_errors: 5,
1682 compile_errors: 3,
1683 runtime_errors: 2,
1684 average_eval_time_ms: 25.0,
1685 };
1686
1687 let output = show_session_summary_from_snapshot(&snapshot, false);
1688 assert!(output.contains("Cached")); assert!(output.contains("JIT")); }
1691}