use crate::collector::types::{
KeyboardEvent, KeyboardEventType, MouseEvent, MouseEventType, ShortcutEvent,
};
use crate::core::windowing::EventWindow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct KeyboardFeatures {
pub typing_rate: f64,
pub pause_count: u32,
pub mean_pause_ms: f64,
pub latency_variability: f64,
pub hold_time_mean: f64,
pub burst_index: f64,
pub session_continuity: f64,
pub typing_tap_count: u32,
pub typing_cadence_stability: f64,
pub typing_gap_ratio: f64,
pub typing_interaction_intensity: f64,
pub keyboard_scroll_rate: f64,
pub navigation_key_count: u32,
pub backspace_count: u32,
pub delete_count: u32,
pub correction_rate: f64,
pub typing_efficiency: f64,
pub enter_count: u32,
pub tab_count: u32,
pub escape_count: u32,
pub modifier_key_count: u32,
pub function_key_count: u32,
pub shortcut_count: u32,
pub shortcut_rate: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MouseFeatures {
pub mouse_activity_rate: f64,
pub mean_velocity: f64,
pub velocity_variability: f64,
pub acceleration_spikes: u32,
pub click_rate: f64,
pub scroll_rate: f64,
pub idle_ratio: f64,
pub micro_adjustment_ratio: f64,
pub idle_time_ms: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BehavioralSignals {
pub interaction_rhythm: f64,
pub friction: f64,
pub motor_stability: f64,
pub focus_continuity_proxy: f64,
pub burstiness: f64,
pub deep_focus_block: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WindowFeatures {
pub keyboard: KeyboardFeatures,
pub mouse: MouseFeatures,
pub behavioral: BehavioralSignals,
}
const PAUSE_THRESHOLD_MS: i64 = 500;
const MICRO_ADJUSTMENT_THRESHOLD: f64 = 5.0;
const ACCELERATION_SPIKE_THRESHOLD: f64 = 50.0;
pub fn compute_features(window: &EventWindow) -> WindowFeatures {
let keyboard = compute_keyboard_features(
&window.keyboard_events,
&window.shortcut_events,
window.duration_secs(),
);
let mouse = compute_mouse_features(&window.mouse_events, window.duration_secs());
let behavioral = compute_behavioral_signals(&keyboard, &mouse);
WindowFeatures {
keyboard,
mouse,
behavioral,
}
}
fn compute_keyboard_features(
events: &[KeyboardEvent],
shortcuts: &[ShortcutEvent],
window_duration: f64,
) -> KeyboardFeatures {
if (events.is_empty() && shortcuts.is_empty()) || window_duration <= 0.0 {
return KeyboardFeatures::default();
}
let typing_events: Vec<&KeyboardEvent> = events
.iter()
.filter(|e| e.event_type == KeyboardEventType::TypingTap)
.collect();
let navigation_events: Vec<&KeyboardEvent> = events
.iter()
.filter(|e| e.event_type == KeyboardEventType::NavigationKey)
.collect();
let navigation_key_presses: Vec<&KeyboardEvent> = navigation_events
.iter()
.filter(|e| e.is_key_down)
.copied()
.collect();
let navigation_key_count = navigation_key_presses.len() as u32;
let keyboard_scroll_rate = navigation_key_count as f64 / window_duration;
let backspace_count = events
.iter()
.filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Backspace)
.count() as u32;
let delete_count = events
.iter()
.filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Delete)
.count() as u32;
let enter_count = events
.iter()
.filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Enter)
.count() as u32;
let tab_count = events
.iter()
.filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Tab)
.count() as u32;
let escape_count = events
.iter()
.filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Escape)
.count() as u32;
let modifier_key_count = events
.iter()
.filter(|e| e.is_key_down && e.event_type == KeyboardEventType::ModifierKey)
.count() as u32;
let function_key_count = events
.iter()
.filter(|e| e.is_key_down && e.event_type == KeyboardEventType::FunctionKey)
.count() as u32;
let typing_key_presses: Vec<&KeyboardEvent> = typing_events
.iter()
.filter(|e| e.is_key_down)
.copied()
.collect();
let typing_tap_count = typing_key_presses.len() as u32;
let correction_rate = (backspace_count + delete_count) as f64 / typing_tap_count.max(1) as f64;
let typing_efficiency = (1.0 - correction_rate).clamp(0.0, 1.0);
let shortcut_count = shortcuts.len() as u32;
let shortcut_rate = shortcut_count as f64 / window_duration;
let typing_rate = typing_tap_count as f64 / window_duration;
let intervals: Vec<i64> = typing_key_presses
.windows(2)
.map(|pair| (pair[1].timestamp - pair[0].timestamp).num_milliseconds())
.collect();
let pauses: Vec<i64> = intervals
.iter()
.filter(|&&i| i > PAUSE_THRESHOLD_MS)
.copied()
.collect();
let pause_count = pauses.len() as u32;
let mean_pause_ms = if pauses.is_empty() {
0.0
} else {
pauses.iter().sum::<i64>() as f64 / pauses.len() as f64
};
let latency_variability = std_dev(&intervals.iter().map(|&i| i as f64).collect::<Vec<_>>());
let hold_times = compute_hold_times(&typing_events);
let hold_time_mean = if hold_times.is_empty() {
0.0
} else {
hold_times.iter().sum::<f64>() / hold_times.len() as f64
};
let short_interval_count = intervals.iter().filter(|&&i| i < 100).count();
let burst_index = if intervals.is_empty() {
0.0
} else {
short_interval_count as f64 / intervals.len() as f64
};
let active_intervals: Vec<i64> = intervals
.iter()
.filter(|&&i| i <= PAUSE_THRESHOLD_MS * 2) .copied()
.collect();
let active_time_ms: i64 = active_intervals.iter().sum();
let session_continuity = (active_time_ms as f64 / 1000.0) / window_duration;
let typing_cadence_stability = 1.0 / (1.0 + latency_variability / 100.0);
let typing_gap_ratio = if intervals.is_empty() {
0.0
} else {
pause_count as f64 / intervals.len() as f64
};
let normalized_speed = (typing_rate / 10.0).min(1.0); let typing_interaction_intensity =
(normalized_speed * 0.4 + typing_cadence_stability * 0.3 + (1.0 - typing_gap_ratio) * 0.3)
.clamp(0.0, 1.0);
KeyboardFeatures {
typing_rate,
pause_count,
mean_pause_ms,
latency_variability,
hold_time_mean,
burst_index,
session_continuity: session_continuity.min(1.0), typing_tap_count,
typing_cadence_stability,
typing_gap_ratio,
typing_interaction_intensity,
keyboard_scroll_rate,
navigation_key_count,
backspace_count,
delete_count,
correction_rate,
typing_efficiency,
enter_count,
tab_count,
escape_count,
modifier_key_count,
function_key_count,
shortcut_count,
shortcut_rate,
}
}
fn compute_hold_times(events: &[&KeyboardEvent]) -> Vec<f64> {
let mut hold_times = Vec::new();
let mut last_down: Option<&KeyboardEvent> = None;
for event in events {
if event.is_key_down {
last_down = Some(event);
} else if let Some(down) = last_down {
let hold_ms = (event.timestamp - down.timestamp).num_milliseconds() as f64;
if (20.0..=2000.0).contains(&hold_ms) {
hold_times.push(hold_ms);
}
last_down = None;
}
}
hold_times
}
fn compute_mouse_features(events: &[MouseEvent], window_duration: f64) -> MouseFeatures {
if events.is_empty() || window_duration <= 0.0 {
return MouseFeatures::default();
}
let move_events: Vec<&MouseEvent> = events
.iter()
.filter(|e| e.event_type == MouseEventType::Move)
.collect();
let click_events: Vec<&MouseEvent> = events
.iter()
.filter(|e| {
e.event_type == MouseEventType::LeftClick || e.event_type == MouseEventType::RightClick
})
.collect();
let scroll_events: Vec<&MouseEvent> = events
.iter()
.filter(|e| e.event_type == MouseEventType::Scroll)
.collect();
let mouse_activity_rate = move_events.len() as f64 / window_duration;
let velocities: Vec<f64> = move_events
.iter()
.filter_map(|e| e.delta_magnitude)
.collect();
let mean_velocity = if velocities.is_empty() {
0.0
} else {
velocities.iter().sum::<f64>() / velocities.len() as f64
};
let velocity_variability = std_dev(&velocities);
let acceleration_spikes = velocities
.windows(2)
.filter(|pair| (pair[1] - pair[0]).abs() > ACCELERATION_SPIKE_THRESHOLD)
.count() as u32;
let click_rate = click_events.len() as f64 / window_duration;
let scroll_rate = scroll_events.len() as f64 / window_duration;
let (idle_ratio, idle_time_ms, _has_long_gap) =
estimate_idle_metrics(&move_events, window_duration);
let micro_count = velocities
.iter()
.filter(|&&v| v < MICRO_ADJUSTMENT_THRESHOLD)
.count();
let micro_adjustment_ratio = if velocities.is_empty() {
0.0
} else {
micro_count as f64 / velocities.len() as f64
};
MouseFeatures {
mouse_activity_rate,
mean_velocity,
velocity_variability,
acceleration_spikes,
click_rate,
scroll_rate,
idle_ratio,
micro_adjustment_ratio,
idle_time_ms,
}
}
fn estimate_idle_metrics(move_events: &[&MouseEvent], window_duration: f64) -> (f64, u64, bool) {
if move_events.len() < 2 {
let total_idle = (window_duration * 1000.0) as u64;
return (1.0, total_idle, true);
}
const IDLE_THRESHOLD_MS: i64 = 1000;
const LONG_GAP_THRESHOLD_MS: i64 = 2000;
let mut idle_time_ms: i64 = 0;
let mut has_long_gap = false;
for pair in move_events.windows(2) {
let gap = (pair[1].timestamp - pair[0].timestamp).num_milliseconds();
if gap > IDLE_THRESHOLD_MS {
idle_time_ms += gap - IDLE_THRESHOLD_MS; }
if gap > LONG_GAP_THRESHOLD_MS {
has_long_gap = true;
}
}
let idle_secs = idle_time_ms as f64 / 1000.0;
let idle_ratio = (idle_secs / window_duration).min(1.0);
(idle_ratio, idle_time_ms.max(0) as u64, has_long_gap)
}
fn compute_behavioral_signals(
keyboard: &KeyboardFeatures,
mouse: &MouseFeatures,
) -> BehavioralSignals {
let typing_rhythm = 1.0 / (1.0 + keyboard.latency_variability / 100.0);
let mouse_rhythm = 1.0 / (1.0 + mouse.velocity_variability / 50.0);
let interaction_rhythm = (typing_rhythm + mouse_rhythm) / 2.0;
let friction = (keyboard.pause_count as f64 * 0.1)
+ (1.0 - keyboard.burst_index) * 0.2
+ mouse.micro_adjustment_ratio * 0.2
+ keyboard.correction_rate.min(1.0) * 0.3;
let motor_stability = 1.0
- (keyboard.latency_variability / 200.0).min(0.5)
- (mouse.velocity_variability / 100.0).min(0.5);
let focus_continuity_proxy = keyboard.session_continuity * 0.5 + (1.0 - mouse.idle_ratio) * 0.5;
let keyboard_burstiness = keyboard.burst_index;
let mouse_burstiness = if mouse.mouse_activity_rate > 0.0 {
mouse.idle_ratio * (1.0 - mouse.micro_adjustment_ratio)
} else {
0.0
};
let burstiness = (keyboard_burstiness * 0.6 + mouse_burstiness * 0.4).clamp(0.0, 1.0);
let has_activity = keyboard.typing_tap_count > 0 || mouse.mouse_activity_rate > 0.5;
let sustained_typing = keyboard.session_continuity > 0.7;
let minimal_idle = mouse.idle_ratio < 0.3;
let deep_focus_block = has_activity && sustained_typing && minimal_idle;
BehavioralSignals {
interaction_rhythm: interaction_rhythm.clamp(0.0, 1.0),
friction: friction.clamp(0.0, 1.0),
motor_stability: motor_stability.clamp(0.0, 1.0),
focus_continuity_proxy: focus_continuity_proxy.clamp(0.0, 1.0),
burstiness,
deep_focus_block,
}
}
fn std_dev(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
variance.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Duration, Utc};
fn make_keyboard_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
KeyboardEvent {
timestamp: Utc::now() + Duration::milliseconds(offset_ms),
is_key_down: is_down,
event_type: KeyboardEventType::TypingTap,
}
}
fn make_navigation_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
KeyboardEvent {
timestamp: Utc::now() + Duration::milliseconds(offset_ms),
is_key_down: is_down,
event_type: KeyboardEventType::NavigationKey,
}
}
#[test]
fn test_keyboard_features_empty() {
let features = compute_keyboard_features(&[], &[], 10.0);
assert_eq!(features.typing_rate, 0.0);
}
#[test]
fn test_keyboard_features_basic() {
let events = vec![
make_keyboard_event(true, 0),
make_keyboard_event(false, 50),
make_keyboard_event(true, 100),
make_keyboard_event(false, 150),
make_keyboard_event(true, 200),
make_keyboard_event(false, 250),
];
let features = compute_keyboard_features(&events, &[], 1.0);
assert_eq!(features.typing_rate, 3.0); }
#[test]
fn test_std_dev() {
let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let sd = std_dev(&values);
assert!((sd - 2.0).abs() < 0.1);
}
#[test]
fn test_behavioral_signals_bounds() {
let keyboard = KeyboardFeatures::default();
let mouse = MouseFeatures::default();
let signals = compute_behavioral_signals(&keyboard, &mouse);
assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
}
#[test]
fn test_typing_tap_count() {
let events = vec![
make_keyboard_event(true, 0),
make_keyboard_event(false, 50),
make_keyboard_event(true, 100),
make_keyboard_event(false, 150),
make_keyboard_event(true, 200),
make_keyboard_event(false, 250),
];
let features = compute_keyboard_features(&events, &[], 1.0);
assert_eq!(features.typing_tap_count, 3); }
#[test]
fn test_typing_cadence_stability_bounds() {
let features_empty = compute_keyboard_features(&[], &[], 10.0);
assert!(
features_empty.typing_cadence_stability >= 0.0
&& features_empty.typing_cadence_stability <= 1.0
);
let events = vec![
make_keyboard_event(true, 0),
make_keyboard_event(false, 50),
make_keyboard_event(true, 100),
make_keyboard_event(false, 150),
make_keyboard_event(true, 200),
make_keyboard_event(false, 250),
];
let features = compute_keyboard_features(&events, &[], 1.0);
assert!(
features.typing_cadence_stability >= 0.0 && features.typing_cadence_stability <= 1.0
);
assert!(features.typing_cadence_stability > 0.5);
}
#[test]
fn test_typing_gap_ratio_bounds() {
let features_empty = compute_keyboard_features(&[], &[], 10.0);
assert_eq!(features_empty.typing_gap_ratio, 0.0);
let events = vec![
make_keyboard_event(true, 0),
make_keyboard_event(false, 50),
make_keyboard_event(true, 100),
make_keyboard_event(false, 150),
];
let features = compute_keyboard_features(&events, &[], 1.0);
assert!(features.typing_gap_ratio >= 0.0 && features.typing_gap_ratio <= 1.0);
assert_eq!(features.typing_gap_ratio, 0.0);
let events_with_gaps = vec![
make_keyboard_event(true, 0),
make_keyboard_event(false, 50),
make_keyboard_event(true, 600), make_keyboard_event(false, 650),
];
let features_gaps = compute_keyboard_features(&events_with_gaps, &[], 1.0);
assert!(features_gaps.typing_gap_ratio > 0.0); }
#[test]
fn test_typing_interaction_intensity_bounds() {
let features_empty = compute_keyboard_features(&[], &[], 10.0);
assert!(
features_empty.typing_interaction_intensity >= 0.0
&& features_empty.typing_interaction_intensity <= 1.0
);
let fast_events = vec![
make_keyboard_event(true, 0),
make_keyboard_event(false, 30),
make_keyboard_event(true, 60),
make_keyboard_event(false, 90),
make_keyboard_event(true, 120),
make_keyboard_event(false, 150),
make_keyboard_event(true, 180),
make_keyboard_event(false, 210),
make_keyboard_event(true, 240),
make_keyboard_event(false, 270),
];
let features = compute_keyboard_features(&fast_events, &[], 1.0);
assert!(
features.typing_interaction_intensity >= 0.0
&& features.typing_interaction_intensity <= 1.0
);
assert!(features.typing_interaction_intensity > 0.3);
}
#[test]
fn test_navigation_key_separation() {
let events = vec![
make_keyboard_event(true, 0), make_keyboard_event(false, 50), make_navigation_event(true, 100), make_navigation_event(false, 150), make_keyboard_event(true, 200), make_keyboard_event(false, 250), make_navigation_event(true, 300), make_navigation_event(false, 350), ];
let features = compute_keyboard_features(&events, &[], 1.0);
assert_eq!(features.typing_tap_count, 2);
assert_eq!(features.typing_rate, 2.0);
assert_eq!(features.navigation_key_count, 2);
assert_eq!(features.keyboard_scroll_rate, 2.0);
}
#[test]
fn test_navigation_keys_dont_inflate_typing_metrics() {
let nav_only_events = vec![
make_navigation_event(true, 0),
make_navigation_event(false, 50),
make_navigation_event(true, 100),
make_navigation_event(false, 150),
make_navigation_event(true, 200),
make_navigation_event(false, 250),
];
let features = compute_keyboard_features(&nav_only_events, &[], 1.0);
assert_eq!(features.typing_tap_count, 0);
assert_eq!(features.typing_rate, 0.0);
assert_eq!(features.navigation_key_count, 3);
assert_eq!(features.keyboard_scroll_rate, 3.0);
}
#[test]
fn test_keyboard_scroll_rate_bounds() {
let features_empty = compute_keyboard_features(&[], &[], 10.0);
assert_eq!(features_empty.keyboard_scroll_rate, 0.0);
assert_eq!(features_empty.navigation_key_count, 0);
let nav_events = vec![
make_navigation_event(true, 0),
make_navigation_event(false, 30),
make_navigation_event(true, 60),
make_navigation_event(false, 90),
make_navigation_event(true, 120),
make_navigation_event(false, 150),
];
let features = compute_keyboard_features(&nav_events, &[], 1.0);
assert_eq!(features.navigation_key_count, 3);
assert!(features.keyboard_scroll_rate > 0.0);
}
#[test]
fn test_burstiness_bounds() {
let keyboard = KeyboardFeatures::default();
let mouse = MouseFeatures::default();
let signals = compute_behavioral_signals(&keyboard, &mouse);
assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
}
#[test]
fn test_burstiness_high_burst_index() {
let keyboard = KeyboardFeatures {
burst_index: 0.9, ..Default::default()
};
let mouse = MouseFeatures::default();
let signals = compute_behavioral_signals(&keyboard, &mouse);
assert!(signals.burstiness > 0.4);
assert!(signals.burstiness <= 1.0);
}
#[test]
fn test_deep_focus_block_detection() {
let keyboard = KeyboardFeatures::default();
let mouse = MouseFeatures::default();
let signals = compute_behavioral_signals(&keyboard, &mouse);
assert!(!signals.deep_focus_block);
let keyboard_active = KeyboardFeatures {
session_continuity: 0.9, typing_tap_count: 50, ..Default::default()
};
let mouse_active = MouseFeatures {
idle_ratio: 0.1, mouse_activity_rate: 2.0,
..Default::default()
};
let signals_active = compute_behavioral_signals(&keyboard_active, &mouse_active);
assert!(signals_active.deep_focus_block);
}
#[test]
fn test_deep_focus_block_requires_low_idle() {
let keyboard = KeyboardFeatures {
session_continuity: 0.9,
typing_tap_count: 50,
..Default::default()
};
let mouse = MouseFeatures {
idle_ratio: 0.5, ..Default::default()
};
let signals = compute_behavioral_signals(&keyboard, &mouse);
assert!(!signals.deep_focus_block);
}
#[test]
fn test_idle_time_ms_computation() {
use crate::collector::types::MouseEvent;
let base_time = chrono::Utc::now();
let events = vec![
MouseEvent {
timestamp: base_time,
event_type: MouseEventType::Move,
delta_magnitude: Some(10.0),
scroll_direction: None,
scroll_magnitude: None,
},
MouseEvent {
timestamp: base_time + chrono::Duration::milliseconds(500),
event_type: MouseEventType::Move,
delta_magnitude: Some(10.0),
scroll_direction: None,
scroll_magnitude: None,
},
MouseEvent {
timestamp: base_time + chrono::Duration::milliseconds(2000), event_type: MouseEventType::Move,
delta_magnitude: Some(10.0),
scroll_direction: None,
scroll_magnitude: None,
},
];
let features = compute_mouse_features(&events, 2.0);
assert!(features.idle_time_ms > 0);
assert!(features.idle_ratio > 0.0);
}
#[test]
fn test_behavioral_signals_new_fields_bounds() {
let keyboard = KeyboardFeatures {
burst_index: 0.5,
session_continuity: 0.5,
typing_tap_count: 10,
..Default::default()
};
let mouse = MouseFeatures {
idle_ratio: 0.5,
mouse_activity_rate: 1.0,
..Default::default()
};
let signals = compute_behavioral_signals(&keyboard, &mouse);
assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
}
fn make_special_key_event(
event_type: KeyboardEventType,
is_down: bool,
offset_ms: i64,
) -> KeyboardEvent {
KeyboardEvent {
timestamp: Utc::now() + Duration::milliseconds(offset_ms),
is_key_down: is_down,
event_type,
}
}
#[test]
fn test_correction_rate_computation() {
let events = vec![
make_keyboard_event(true, 0),
make_keyboard_event(false, 50),
make_keyboard_event(true, 100),
make_keyboard_event(false, 150),
make_keyboard_event(true, 200),
make_keyboard_event(false, 250),
make_special_key_event(KeyboardEventType::Backspace, true, 300),
make_special_key_event(KeyboardEventType::Backspace, false, 350),
];
let features = compute_keyboard_features(&events, &[], 1.0);
assert_eq!(features.typing_tap_count, 3);
assert_eq!(features.backspace_count, 1);
assert_eq!(features.delete_count, 0);
assert!((features.correction_rate - 1.0 / 3.0).abs() < 0.01);
assert!((features.typing_efficiency - (1.0 - 1.0 / 3.0)).abs() < 0.01);
}
#[test]
fn test_correction_rate_zero_typing() {
let events = vec![
make_special_key_event(KeyboardEventType::Backspace, true, 0),
make_special_key_event(KeyboardEventType::Backspace, false, 50),
];
let features = compute_keyboard_features(&events, &[], 1.0);
assert_eq!(features.typing_tap_count, 0);
assert_eq!(features.backspace_count, 1);
assert!((features.correction_rate - 1.0).abs() < 0.01);
assert_eq!(features.typing_efficiency, 0.0);
}
#[test]
fn test_special_key_counts() {
let events = vec![
make_special_key_event(KeyboardEventType::Enter, true, 0),
make_special_key_event(KeyboardEventType::Enter, false, 50),
make_special_key_event(KeyboardEventType::Tab, true, 100),
make_special_key_event(KeyboardEventType::Tab, false, 150),
make_special_key_event(KeyboardEventType::Escape, true, 200),
make_special_key_event(KeyboardEventType::Escape, false, 250),
make_special_key_event(KeyboardEventType::ModifierKey, true, 300),
make_special_key_event(KeyboardEventType::FunctionKey, true, 400),
make_special_key_event(KeyboardEventType::FunctionKey, false, 450),
];
let features = compute_keyboard_features(&events, &[], 1.0);
assert_eq!(features.enter_count, 1);
assert_eq!(features.tab_count, 1);
assert_eq!(features.escape_count, 1);
assert_eq!(features.modifier_key_count, 1);
assert_eq!(features.function_key_count, 1);
assert_eq!(features.typing_tap_count, 0); }
#[test]
fn test_shortcut_metrics() {
use crate::collector::types::{ShortcutEvent, ShortcutType};
let shortcuts = vec![
ShortcutEvent {
timestamp: Utc::now(),
shortcut_type: ShortcutType::Copy,
},
ShortcutEvent {
timestamp: Utc::now() + Duration::milliseconds(500),
shortcut_type: ShortcutType::Paste,
},
];
let features = compute_keyboard_features(&[], &shortcuts, 10.0);
assert_eq!(features.shortcut_count, 2);
assert!((features.shortcut_rate - 0.2).abs() < 0.01); }
#[test]
fn test_friction_includes_correction_rate() {
let keyboard_with_corrections = KeyboardFeatures {
correction_rate: 0.5,
burst_index: 0.5,
..Default::default()
};
let keyboard_without = KeyboardFeatures {
correction_rate: 0.0,
burst_index: 0.5,
..Default::default()
};
let mouse = MouseFeatures::default();
let signals_with = compute_behavioral_signals(&keyboard_with_corrections, &mouse);
let signals_without = compute_behavioral_signals(&keyboard_without, &mouse);
assert!(signals_with.friction > signals_without.friction);
}
}