2026-04-13 23:11:35 -03:00
use serde ::{ Deserialize , Serialize } ;
use std ::collections ::VecDeque ;
2026-04-16 19:19:37 -03:00
use std ::fmt ::Write as _ ;
2026-04-13 23:11:35 -03:00
use super ::state ::{ InputRouting , LauncherState , ViewMode } ;
#[ derive(Debug, Clone, Serialize, Deserialize, PartialEq) ]
pub struct PerformanceSample {
pub rtt_ms : f32 ,
2026-04-17 01:09:33 -03:00
pub probe_spread_ms : f32 ,
2026-04-13 23:11:35 -03:00
pub input_latency_ms : f32 ,
2026-04-16 21:18:34 -03:00
pub probe_loss_pct : f32 ,
pub video_loss_pct : f32 ,
pub left_receive_fps : f32 ,
pub left_present_fps : f32 ,
pub left_server_fps : f32 ,
2026-04-17 05:50:24 -03:00
pub left_stream_spread_ms : f32 ,
pub left_packet_gap_peak_ms : f32 ,
pub left_present_gap_peak_ms : f32 ,
pub left_queue_depth : u32 ,
pub left_queue_peak : u32 ,
pub left_decoder_label : String ,
2026-04-16 21:18:34 -03:00
pub right_receive_fps : f32 ,
pub right_present_fps : f32 ,
pub right_server_fps : f32 ,
2026-04-17 05:50:24 -03:00
pub right_stream_spread_ms : f32 ,
pub right_packet_gap_peak_ms : f32 ,
pub right_present_gap_peak_ms : f32 ,
pub right_queue_depth : u32 ,
pub right_queue_peak : u32 ,
pub right_decoder_label : String ,
2026-04-13 23:11:35 -03:00
pub dropped_frames : u64 ,
pub queue_depth : u32 ,
}
#[ derive(Debug, Clone, Serialize, Deserialize) ]
pub struct DiagnosticsLog {
capacity : usize ,
history : VecDeque < PerformanceSample > ,
}
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 < Item = & PerformanceSample > {
self . history . iter ( )
}
}
#[ derive(Debug, Clone, Serialize, Deserialize) ]
pub struct SnapshotReport {
2026-04-16 19:19:37 -03:00
pub client_version : String ,
pub server_version : Option < String > ,
pub server_available : bool ,
2026-04-13 23:11:35 -03:00
pub routing : InputRouting ,
pub view_mode : ViewMode ,
pub remote_active : bool ,
2026-04-16 19:19:37 -03:00
pub power_state : String ,
pub preview_source : String ,
2026-04-17 00:21:18 -03:00
pub client_display_limit : String ,
2026-04-16 19:19:37 -03:00
pub left_surface : String ,
pub left_capture_profile : String ,
2026-04-16 22:15:59 -03:00
pub left_capture_transport : String ,
2026-04-16 19:19:37 -03:00
pub left_breakout_profile : String ,
2026-04-17 05:50:24 -03:00
pub left_decoder_label : String ,
pub left_stream_spread_ms : f32 ,
pub left_packet_gap_peak_ms : f32 ,
pub left_present_gap_peak_ms : f32 ,
pub left_queue_depth : u32 ,
pub left_queue_peak : u32 ,
2026-04-16 19:19:37 -03:00
pub right_surface : String ,
pub right_capture_profile : String ,
2026-04-16 22:15:59 -03:00
pub right_capture_transport : String ,
2026-04-16 19:19:37 -03:00
pub right_breakout_profile : String ,
2026-04-17 05:50:24 -03:00
pub right_decoder_label : String ,
pub right_stream_spread_ms : f32 ,
pub right_packet_gap_peak_ms : f32 ,
pub right_present_gap_peak_ms : f32 ,
pub right_queue_depth : u32 ,
pub right_queue_peak : u32 ,
2026-04-13 23:11:35 -03:00
pub selected_camera : Option < String > ,
pub selected_microphone : Option < String > ,
pub selected_speaker : Option < String > ,
2026-04-16 19:19:37 -03:00
pub selected_keyboard : Option < String > ,
pub selected_mouse : Option < String > ,
2026-04-13 23:11:35 -03:00
pub status : String ,
pub recent_samples : Vec < PerformanceSample > ,
pub notes : Vec < String > ,
2026-04-16 19:19:37 -03:00
pub recommendations : Vec < String > ,
2026-04-13 23:11:35 -03:00
pub probe_command : String ,
}
impl SnapshotReport {
pub fn from_state ( state : & LauncherState , log : & DiagnosticsLog , probe_command : String ) -> Self {
2026-04-16 19:19:37 -03:00
let left_capture = state . capture_size_choice ( 0 ) ;
let right_capture = state . capture_size_choice ( 1 ) ;
let left_breakout = state . breakout_size_choice ( 0 ) ;
let right_breakout = state . breakout_size_choice ( 1 ) ;
2026-04-17 05:50:24 -03:00
let latest = log . latest ( ) ;
2026-04-13 23:11:35 -03:00
Self {
2026-04-16 19:19:37 -03:00
client_version : crate ::VERSION . to_string ( ) ,
server_version : state . server_version . clone ( ) ,
server_available : state . server_available ,
2026-04-13 23:11:35 -03:00
routing : state . routing ,
view_mode : state . view_mode ,
remote_active : state . remote_active ,
2026-04-16 19:19:37 -03:00
power_state : format ! (
" {} | {} | leases {} " ,
state . capture_power . mode ,
state . capture_power . detail ,
state . capture_power . active_leases
) ,
preview_source : format ! (
" {}x{} @ {} fps " ,
state . preview_source . width , state . preview_source . height , state . preview_source . fps
) ,
2026-04-17 00:21:18 -03:00
client_display_limit : format ! (
2026-04-16 19:19:37 -03:00
" {}x{} " ,
state . breakout_display . width , state . breakout_display . height
) ,
left_surface : state . display_surface ( 0 ) . label ( ) . to_string ( ) ,
left_capture_profile : format ! (
" {} | {}x{} | {} fps | {} kbit " ,
2026-04-16 22:15:59 -03:00
left_capture . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
left_capture . width ,
left_capture . height ,
left_capture . fps ,
left_capture . max_bitrate_kbit
) ,
2026-04-16 22:15:59 -03:00
left_capture_transport : left_capture . preset . transport_label ( ) . to_string ( ) ,
2026-04-16 19:19:37 -03:00
left_breakout_profile : format ! (
" {} | {}x{} " ,
2026-04-16 22:15:59 -03:00
left_breakout . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
left_breakout . width ,
left_breakout . height
) ,
2026-04-17 05:50:24 -03:00
left_decoder_label : latest
. map ( | sample | {
if sample . left_decoder_label . is_empty ( ) {
" pending " . to_string ( )
} else {
sample . left_decoder_label . clone ( )
}
} )
. unwrap_or_else ( | | " pending " . to_string ( ) ) ,
left_stream_spread_ms : latest
. map ( | sample | sample . left_stream_spread_ms )
. unwrap_or ( 0.0 ) ,
left_packet_gap_peak_ms : latest
. map ( | sample | sample . left_packet_gap_peak_ms )
. unwrap_or ( 0.0 ) ,
left_present_gap_peak_ms : latest
. map ( | sample | sample . left_present_gap_peak_ms )
. unwrap_or ( 0.0 ) ,
left_queue_depth : latest . map ( | sample | sample . left_queue_depth ) . unwrap_or ( 0 ) ,
left_queue_peak : latest . map ( | sample | sample . left_queue_peak ) . unwrap_or ( 0 ) ,
2026-04-16 19:19:37 -03:00
right_surface : state . display_surface ( 1 ) . label ( ) . to_string ( ) ,
right_capture_profile : format ! (
" {} | {}x{} | {} fps | {} kbit " ,
2026-04-16 22:15:59 -03:00
right_capture . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
right_capture . width ,
right_capture . height ,
right_capture . fps ,
right_capture . max_bitrate_kbit
) ,
2026-04-16 22:15:59 -03:00
right_capture_transport : right_capture . preset . transport_label ( ) . to_string ( ) ,
2026-04-16 19:19:37 -03:00
right_breakout_profile : format ! (
" {} | {}x{} " ,
2026-04-16 22:15:59 -03:00
right_breakout . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
right_breakout . width ,
right_breakout . height
) ,
2026-04-17 05:50:24 -03:00
right_decoder_label : latest
. map ( | sample | {
if sample . right_decoder_label . is_empty ( ) {
" pending " . to_string ( )
} else {
sample . right_decoder_label . clone ( )
}
} )
. unwrap_or_else ( | | " pending " . to_string ( ) ) ,
right_stream_spread_ms : latest
. map ( | sample | sample . right_stream_spread_ms )
. unwrap_or ( 0.0 ) ,
right_packet_gap_peak_ms : latest
. map ( | sample | sample . right_packet_gap_peak_ms )
. unwrap_or ( 0.0 ) ,
right_present_gap_peak_ms : latest
. map ( | sample | sample . right_present_gap_peak_ms )
. unwrap_or ( 0.0 ) ,
right_queue_depth : latest . map ( | sample | sample . right_queue_depth ) . unwrap_or ( 0 ) ,
right_queue_peak : latest . map ( | sample | sample . right_queue_peak ) . unwrap_or ( 0 ) ,
2026-04-13 23:11:35 -03:00
selected_camera : state . devices . camera . clone ( ) ,
selected_microphone : state . devices . microphone . clone ( ) ,
selected_speaker : state . devices . speaker . clone ( ) ,
2026-04-16 19:19:37 -03:00
selected_keyboard : state . devices . keyboard . clone ( ) ,
selected_mouse : state . devices . mouse . clone ( ) ,
2026-04-13 23:11:35 -03:00
status : state . status_line ( ) ,
recent_samples : log . iter ( ) . cloned ( ) . collect ( ) ,
notes : state . notes . clone ( ) ,
2026-04-16 19:19:37 -03:00
recommendations : recommendations_for ( state , log ) ,
2026-04-13 23:11:35 -03:00
probe_command ,
}
}
pub fn to_pretty_json ( & self ) -> Result < String , serde_json ::Error > {
serde_json ::to_string_pretty ( self )
}
2026-04-16 19:19:37 -03:00
pub fn to_pretty_text ( & self ) -> String {
let mut text = String ::new ( ) ;
let server_version = self . server_version . as_deref ( ) . unwrap_or ( " unknown " ) ;
let server_state = if self . server_available {
" reachable "
} else {
" unreachable "
} ;
let _ = writeln! ( text , " Lesavka Diagnostics " ) ;
let _ = writeln! ( text , " client: v{} " , self . client_version ) ;
let _ = writeln! ( text , " server: {server_version} ({server_state}) " ) ;
let _ = writeln! (
text ,
" session: routing={:?} view={:?} relay_active={} power={} " ,
self . routing , self . view_mode , self . remote_active , self . power_state
) ;
let _ = writeln! ( text , " source feed: {} " , self . preview_source ) ;
2026-04-17 00:21:18 -03:00
let _ = writeln! ( text , " client display limit: {} " , self . client_display_limit ) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " left eye " ) ;
let _ = writeln! ( text , " surface: {} " , self . left_surface ) ;
let _ = writeln! ( text , " capture: {} " , self . left_capture_profile ) ;
2026-04-16 22:15:59 -03:00
let _ = writeln! ( text , " transport: {} " , self . left_capture_transport ) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text , " breakout: {} " , self . left_breakout_profile ) ;
2026-04-17 05:50:24 -03:00
let _ = writeln! (
text ,
" live: decoder={} spread={:.1}ms gaps={:.0}/{:.0}ms queue={}/{} " ,
self . left_decoder_label ,
self . left_stream_spread_ms ,
self . left_packet_gap_peak_ms ,
self . left_present_gap_peak_ms ,
self . left_queue_depth ,
self . left_queue_peak
) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text , " right eye " ) ;
let _ = writeln! ( text , " surface: {} " , self . right_surface ) ;
let _ = writeln! ( text , " capture: {} " , self . right_capture_profile ) ;
2026-04-16 22:15:59 -03:00
let _ = writeln! ( text , " transport: {} " , self . right_capture_transport ) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text , " breakout: {} " , self . right_breakout_profile ) ;
2026-04-17 05:50:24 -03:00
let _ = writeln! (
text ,
" live: decoder={} spread={:.1}ms gaps={:.0}/{:.0}ms queue={}/{} " ,
self . right_decoder_label ,
self . right_stream_spread_ms ,
self . right_packet_gap_peak_ms ,
self . right_present_gap_peak_ms ,
self . right_queue_depth ,
self . right_queue_peak
) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " device staging " ) ;
let _ = writeln! (
text ,
" camera: {} " ,
self . selected_camera . as_deref ( ) . unwrap_or ( " auto " )
) ;
let _ = writeln! (
text ,
" microphone: {} " ,
self . selected_microphone . as_deref ( ) . unwrap_or ( " auto " )
) ;
let _ = writeln! (
text ,
" speaker: {} " ,
self . selected_speaker . as_deref ( ) . unwrap_or ( " auto " )
) ;
let _ = writeln! (
text ,
" keyboard: {} " ,
self . selected_keyboard . as_deref ( ) . unwrap_or ( " all " )
) ;
let _ = writeln! (
text ,
" mouse: {} " ,
self . selected_mouse . as_deref ( ) . unwrap_or ( " all " )
) ;
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " launcher status " ) ;
let _ = writeln! ( text , " {} " , self . status ) ;
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " recent samples " ) ;
if self . recent_samples . is_empty ( ) {
let _ = writeln! (
text ,
2026-04-17 01:09:33 -03:00
" no live RTT/probe-spread/loss samples yet; this report is currently a launcher state snapshot. "
2026-04-16 19:19:37 -03:00
) ;
} else {
for sample in & self . recent_samples {
let _ = writeln! (
text ,
2026-04-17 05:50:24 -03:00
" rtt={:.1}ms probe-spread={:.1}ms input-floor={:.1}ms probe-loss={:.1}% video-loss={:.1}% left={:.1}/{:.1}/{:.1}fps right={:.1}/{:.1}/{:.1}fps dropped={} queue={}/{} peaks=l{:.0}/{:.0}ms r{:.0}/{:.0}ms " ,
2026-04-16 19:19:37 -03:00
sample . rtt_ms ,
2026-04-17 01:09:33 -03:00
sample . probe_spread_ms ,
2026-04-16 19:19:37 -03:00
sample . input_latency_ms ,
2026-04-16 21:18:34 -03:00
sample . probe_loss_pct ,
sample . video_loss_pct ,
sample . left_receive_fps ,
sample . left_present_fps ,
sample . left_server_fps ,
sample . right_receive_fps ,
sample . right_present_fps ,
sample . right_server_fps ,
2026-04-16 19:19:37 -03:00
sample . dropped_frames ,
2026-04-17 05:50:24 -03:00
sample . queue_depth ,
sample . left_queue_peak . max ( sample . right_queue_peak ) ,
sample . left_packet_gap_peak_ms ,
sample . left_present_gap_peak_ms ,
sample . right_packet_gap_peak_ms ,
sample . right_present_gap_peak_ms
2026-04-16 19:19:37 -03:00
) ;
}
}
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " recommendations " ) ;
for item in & self . recommendations {
let _ = writeln! ( text , " - {item} " ) ;
}
if ! self . notes . is_empty ( ) {
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " notes " ) ;
for item in & self . notes {
let _ = writeln! ( text , " - {item} " ) ;
}
}
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " quality probe " ) ;
let _ = writeln! ( text , " {} " , self . probe_command ) ;
text
}
2026-04-13 23:11:35 -03:00
}
pub fn quality_probe_command ( ) -> & 'static str {
" scripts/ci/hygiene_gate.sh && scripts/ci/quality_gate.sh "
}
2026-04-16 19:19:37 -03:00
fn recommendations_for ( state : & LauncherState , log : & DiagnosticsLog ) -> Vec < String > {
let mut items = Vec ::new ( ) ;
if ! state . server_available {
items . push (
" The server is not reachable from this launcher yet, so stream-quality results would not be meaningful. "
. to_string ( ) ,
) ;
}
if log . is_empty ( ) {
items . push (
2026-04-17 01:09:33 -03:00
" Live stream samples will appear here after the launcher collects a few probe windows. Leave the relay up for a few seconds to populate RTT, probe spread, loss, and fps. "
2026-04-16 19:19:37 -03:00
. to_string ( ) ,
) ;
}
2026-04-16 21:18:34 -03:00
if let Some ( sample ) = log . latest ( ) {
2026-04-17 01:09:33 -03:00
if sample . probe_loss_pct > = 3.0 | | sample . probe_spread_ms > = 18.0 {
2026-04-16 21:18:34 -03:00
items . push (
2026-04-17 01:09:33 -03:00
" Control-plane probe spread or loss is elevated. That can come from the network or from server stalls, so compare it against the eye fps before blaming the WAN. "
2026-04-16 21:18:34 -03:00
. to_string ( ) ,
) ;
}
if sample . video_loss_pct > = 2.0 | | sample . dropped_frames > 0 {
items . push (
" Video packets are arriving with gaps or server-side drops. Try 900p or 720p capture first, then watch whether dropped frames and video-loss fall. "
. to_string ( ) ,
) ;
}
if sample . left_present_fps + 1.0 < sample . left_receive_fps
| | sample . right_present_fps + 1.0 < sample . right_receive_fps
{
items . push (
" The client is receiving more frames than it is presenting. That points at local decode/render pressure, so prefer lighter breakout sizes or hardware decode. "
. to_string ( ) ,
) ;
}
2026-04-17 05:50:24 -03:00
if ( sample . left_present_gap_peak_ms - sample . left_packet_gap_peak_ms ) > 40.0
| | ( sample . right_present_gap_peak_ms - sample . right_packet_gap_peak_ms ) > 40.0
{
items . push (
" Present-gap spikes are materially larger than packet-gap spikes. That usually means the client decode/render path is stalling after packets arrive. "
. to_string ( ) ,
) ;
}
2026-04-16 21:18:34 -03:00
if sample . queue_depth > 8 {
items . push (
" The preview queue is backing up. When queue depth climbs, expect laggy mouse feel and delayed visual response even if raw fps still looks okay. "
. to_string ( ) ,
) ;
}
2026-04-17 05:50:24 -03:00
if sample . left_queue_peak > = 4 | | sample . right_queue_peak > = 4 {
items . push (
" Queue depth is spiking even if the latest sample looks calm. That points at bursty backpressure rather than steady-state overload. "
. to_string ( ) ,
) ;
}
2026-04-16 21:18:34 -03:00
}
2026-04-16 19:19:37 -03:00
let heavy_capture = state . capture_sizes . iter ( ) . any ( | preset | {
matches! (
preset ,
super ::state ::CaptureSizePreset ::Source
| super ::state ::CaptureSizePreset ::P1080
| super ::state ::CaptureSizePreset ::P1440
)
} ) ;
if heavy_capture {
items . push (
" If motion artifacting spikes, try a 900p or 720p capture profile before shrinking the breakout window; that usually lowers WAN pressure faster. "
. to_string ( ) ,
) ;
}
2026-04-16 22:15:59 -03:00
let source_passthrough = state
. capture_sizes
. iter ( )
. any ( | preset | matches! ( preset , super ::state ::CaptureSizePreset ::Source ) ) ;
if source_passthrough {
items . push (
" Source capture uses the HDMI device's own H.264 stream. If motion damage lingers for seconds, switch that eye to 1080p, 900p, or 720p so the server re-encodes with a tighter keyframe cadence. "
. to_string ( ) ,
) ;
}
if let Some ( sample ) = log . latest ( )
& & sample . video_loss_pct < 0.5
& & sample . dropped_frames = = 0
& & ( ( sample . left_server_fps - sample . left_receive_fps ) > 6.0
| | ( sample . right_server_fps - sample . right_receive_fps ) > 6.0 )
{
items . push (
" Receive fps is well below the target without packet loss. That usually points at source cadence or local decode pressure more than WAN loss, so compare Source against 1080p/900p and watch which side stays steadier. "
. to_string ( ) ,
) ;
}
2026-04-17 05:50:24 -03:00
if let Some ( sample ) = log . latest ( )
& & sample . video_loss_pct < 0.5
& & sample . dropped_frames = = 0
& & ( sample . left_packet_gap_peak_ms > = 140.0 | | sample . right_packet_gap_peak_ms > = 140.0 )
{
items . push (
" Packet-gap spikes are high without packet loss. That means the stream is arriving in bursts, which usually points at source cadence, encoder stalls, or local decoder starvation more than raw WAN loss. "
. to_string ( ) ,
) ;
}
if let Some ( sample ) = log . latest ( )
& & ( ( sample . left_decoder_label . contains ( " avdec " )
& & sample . left_present_fps + 1.0 < sample . left_receive_fps )
| | ( sample . right_decoder_label . contains ( " avdec " )
& & sample . right_present_fps + 1.0 < sample . right_receive_fps ) )
{
items . push (
" At least one eye is falling back to `avdec_*` while presentation lags behind receive. A hardware decode path would likely help more than extra bitrate. "
. to_string ( ) ,
) ;
}
2026-04-16 19:19:37 -03:00
if state . breakout_count ( ) = = 2 {
items . push (
" Both eye feeds are broken out right now. If the client starts struggling, compare in-launcher preview smoothness against full-window decode. "
. to_string ( ) ,
) ;
}
if items . is_empty ( ) {
items . push ( " Session state looks stable. Collect a few real samples before changing capture settings. " . to_string ( ) ) ;
}
items
}
2026-04-13 23:11:35 -03:00
#[ 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 ,
2026-04-17 01:09:33 -03:00
probe_spread_ms : 3.0 + n as f32 ,
2026-04-13 23:11:35 -03:00
input_latency_ms : 10.0 + n as f32 ,
2026-04-16 21:18:34 -03:00
probe_loss_pct : n as f32 ,
video_loss_pct : ( n as f32 ) * 0.5 ,
left_receive_fps : 30.0 ,
left_present_fps : 29.0 ,
left_server_fps : 30.0 ,
2026-04-17 05:50:24 -03:00
left_stream_spread_ms : 4.0 ,
left_packet_gap_peak_ms : 55.0 ,
left_present_gap_peak_ms : 60.0 ,
left_queue_depth : n as u32 ,
left_queue_peak : n as u32 ,
left_decoder_label : " decodebin " . to_string ( ) ,
2026-04-16 21:18:34 -03:00
right_receive_fps : 30.0 ,
right_present_fps : 28.0 ,
right_server_fps : 30.0 ,
2026-04-17 05:50:24 -03:00
right_stream_spread_ms : 5.0 ,
right_packet_gap_peak_ms : 65.0 ,
right_present_gap_peak_ms : 75.0 ,
right_queue_depth : n as u32 ,
right_queue_peak : n as u32 ,
right_decoder_label : " decodebin " . to_string ( ) ,
2026-04-13 23:11:35 -03:00
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 < u64 > = 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 ( ) ) ,
2026-04-16 12:58:05 -03:00
keyboard : Some ( " /dev/input/event10 " . to_string ( ) ) ,
mouse : Some ( " /dev/input/event11 " . to_string ( ) ) ,
2026-04-13 23:11:35 -03:00
} ;
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 " ) ) ;
2026-04-14 23:03:18 -03:00
assert_eq! (
report . selected_microphone . as_deref ( ) ,
Some ( " alsa_input.usb " )
) ;
2026-04-13 23:11:35 -03:00
assert_eq! ( report . selected_speaker . as_deref ( ) , Some ( " alsa_output.usb " ) ) ;
2026-04-16 19:19:37 -03:00
assert_eq! (
report . selected_keyboard . as_deref ( ) ,
Some ( " /dev/input/event10 " )
) ;
assert_eq! ( report . selected_mouse . as_deref ( ) , Some ( " /dev/input/event11 " ) ) ;
2026-04-13 23:11:35 -03:00
assert_eq! ( report . recent_samples . len ( ) , 1 ) ;
assert_eq! ( report . notes , vec! [ " first note " . to_string ( ) ] ) ;
2026-04-14 18:44:40 -03:00
assert! ( report . status . contains ( " mode=remote " ) ) ;
2026-04-16 19:19:37 -03:00
assert! ( report . client_version . starts_with ( " 0. " ) ) ;
assert! ( report . left_capture_profile . contains ( " fps " ) ) ;
2026-04-16 22:15:59 -03:00
assert_eq! ( report . left_capture_transport , " source pass-through " ) ;
2026-04-17 05:50:24 -03:00
assert_eq! ( report . left_decoder_label , " decodebin " ) ;
2026-04-13 23:11:35 -03:00
}
#[ 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 " ) ) ;
}
2026-04-16 19:19:37 -03:00
#[ test ]
fn snapshot_text_mentions_versions_profiles_and_recommendations ( ) {
let report = SnapshotReport ::from_state (
& LauncherState ::new ( ) ,
& DiagnosticsLog ::new ( 1 ) ,
quality_probe_command ( ) . to_string ( ) ,
) ;
let text = report . to_pretty_text ( ) ;
assert! ( text . contains ( " Lesavka Diagnostics " ) ) ;
assert! ( text . contains ( " client: v " ) ) ;
assert! ( text . contains ( " left eye " ) ) ;
2026-04-16 22:15:59 -03:00
assert! ( text . contains ( " transport: " ) ) ;
2026-04-17 05:50:24 -03:00
assert! ( text . contains ( " live: decoder= " ) ) ;
2026-04-16 19:19:37 -03:00
assert! ( text . contains ( " recommendations " ) ) ;
}
2026-04-13 23:11:35 -03:00
#[ 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 " ) ) ;
}
}