use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use super::state::{InputRouting, LauncherState, ViewMode}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PerformanceSample { pub rtt_ms: f32, pub input_latency_ms: f32, pub left_fps: f32, pub right_fps: f32, pub dropped_frames: u64, pub queue_depth: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiagnosticsLog { capacity: usize, history: VecDeque, } impl DiagnosticsLog { pub fn new(capacity: usize) -> Self { let capacity = capacity.max(1); Self { capacity, history: VecDeque::with_capacity(capacity), } } pub fn record(&mut self, sample: PerformanceSample) { if self.history.len() == self.capacity { let _ = self.history.pop_front(); } self.history.push_back(sample); } pub fn latest(&self) -> Option<&PerformanceSample> { self.history.back() } pub fn len(&self) -> usize { self.history.len() } pub fn is_empty(&self) -> bool { self.history.is_empty() } pub fn iter(&self) -> impl Iterator { self.history.iter() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SnapshotReport { pub routing: InputRouting, pub view_mode: ViewMode, pub remote_active: bool, pub selected_camera: Option, pub selected_microphone: Option, pub selected_speaker: Option, pub status: String, pub recent_samples: Vec, pub notes: Vec, pub probe_command: String, } impl SnapshotReport { pub fn from_state(state: &LauncherState, log: &DiagnosticsLog, probe_command: String) -> Self { Self { routing: state.routing, view_mode: state.view_mode, remote_active: state.remote_active, selected_camera: state.devices.camera.clone(), selected_microphone: state.devices.microphone.clone(), selected_speaker: state.devices.speaker.clone(), status: state.status_line(), recent_samples: log.iter().cloned().collect(), notes: state.notes.clone(), probe_command, } } pub fn to_pretty_json(&self) -> Result { serde_json::to_string_pretty(self) } } pub fn quality_probe_command() -> &'static str { "scripts/ci/hygiene_gate.sh && scripts/ci/quality_gate.sh" } #[cfg(test)] mod tests { use super::*; use crate::launcher::state::{DeviceSelection, LauncherState}; fn sample(n: u64) -> PerformanceSample { PerformanceSample { rtt_ms: 20.0 + n as f32, input_latency_ms: 10.0 + n as f32, left_fps: 30.0, right_fps: 30.0, dropped_frames: n, queue_depth: n as u32, } } #[test] fn diagnostics_log_keeps_only_latest_samples_with_capacity() { let mut log = DiagnosticsLog::new(2); log.record(sample(1)); log.record(sample(2)); log.record(sample(3)); let kept: Vec = log.iter().map(|item| item.dropped_frames).collect(); assert_eq!(kept, vec![2, 3]); assert_eq!(log.latest().map(|s| s.dropped_frames), Some(3)); } #[test] fn diagnostics_log_enforces_minimum_capacity() { let mut log = DiagnosticsLog::new(0); log.record(sample(1)); log.record(sample(2)); assert_eq!(log.len(), 1); assert_eq!(log.latest().map(|s| s.dropped_frames), Some(2)); } #[test] fn snapshot_report_contains_state_fields_and_samples() { let mut state = LauncherState::new(); state.devices = DeviceSelection { camera: Some("/dev/video0".to_string()), microphone: Some("alsa_input.usb".to_string()), speaker: Some("alsa_output.usb".to_string()), }; state.push_note("first note"); let mut log = DiagnosticsLog::new(4); log.record(sample(7)); let report = SnapshotReport::from_state(&state, &log, quality_probe_command().to_string()); assert_eq!(report.selected_camera.as_deref(), Some("/dev/video0")); assert_eq!( report.selected_microphone.as_deref(), Some("alsa_input.usb") ); assert_eq!(report.selected_speaker.as_deref(), Some("alsa_output.usb")); assert_eq!(report.recent_samples.len(), 1); assert_eq!(report.notes, vec!["first note".to_string()]); assert!(report.status.contains("mode=remote")); } #[test] fn snapshot_json_is_serializable_and_mentions_probe_command() { let report = SnapshotReport::from_state( &LauncherState::new(), &DiagnosticsLog::new(1), quality_probe_command().to_string(), ); let json = report.to_pretty_json().expect("serialize"); assert!(json.contains("quality_gate.sh")); assert!(json.contains("routing")); assert!(json.contains("view_mode")); } #[test] fn quality_probe_command_mentions_both_gates() { let cmd = quality_probe_command(); assert!(cmd.contains("hygiene_gate.sh")); assert!(cmd.contains("quality_gate.sh")); } }