1use crate::collector::types::{
7 KeyboardEvent, KeyboardEventType, MouseEvent, MouseEventType, ShortcutEvent,
8};
9use crate::core::windowing::EventWindow;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
19pub struct KeyboardFeatures {
20 pub typing_rate: f64,
22 pub pause_count: u32,
24 pub mean_pause_ms: f64,
26 pub latency_variability: f64,
28 pub hold_time_mean: f64,
30 pub burst_index: f64,
32 pub session_continuity: f64,
34 pub typing_tap_count: u32,
36 pub typing_cadence_stability: f64,
38 pub typing_gap_ratio: f64,
40 pub typing_interaction_intensity: f64,
42 pub keyboard_scroll_rate: f64,
45 pub navigation_key_count: u32,
47
48 pub backspace_count: u32,
51 pub delete_count: u32,
53 pub correction_rate: f64,
55 pub typing_efficiency: f64,
57
58 pub enter_count: u32,
61 pub tab_count: u32,
63 pub escape_count: u32,
65 pub modifier_key_count: u32,
67 pub function_key_count: u32,
69
70 pub shortcut_count: u32,
73 pub shortcut_rate: f64,
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct MouseFeatures {
80 pub mouse_activity_rate: f64,
82 pub mean_velocity: f64,
84 pub velocity_variability: f64,
86 pub acceleration_spikes: u32,
88 pub click_rate: f64,
90 pub scroll_rate: f64,
92 pub idle_ratio: f64,
94 pub micro_adjustment_ratio: f64,
96 pub idle_time_ms: u64,
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct BehavioralSignals {
108 pub interaction_rhythm: f64,
110 pub friction: f64,
112 pub motor_stability: f64,
114 pub focus_continuity_proxy: f64,
116 pub burstiness: f64,
119 pub deep_focus_block: bool,
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct WindowFeatures {
129 pub keyboard: KeyboardFeatures,
131 pub mouse: MouseFeatures,
133 pub behavioral: BehavioralSignals,
135}
136
137const PAUSE_THRESHOLD_MS: i64 = 500;
139
140const MICRO_ADJUSTMENT_THRESHOLD: f64 = 5.0;
142
143const ACCELERATION_SPIKE_THRESHOLD: f64 = 50.0;
145
146pub fn compute_features(window: &EventWindow) -> WindowFeatures {
148 let keyboard = compute_keyboard_features(
149 &window.keyboard_events,
150 &window.shortcut_events,
151 window.duration_secs(),
152 );
153 let mouse = compute_mouse_features(&window.mouse_events, window.duration_secs());
154 let behavioral = compute_behavioral_signals(&keyboard, &mouse);
155
156 WindowFeatures {
157 keyboard,
158 mouse,
159 behavioral,
160 }
161}
162
163fn compute_keyboard_features(
169 events: &[KeyboardEvent],
170 shortcuts: &[ShortcutEvent],
171 window_duration: f64,
172) -> KeyboardFeatures {
173 if (events.is_empty() && shortcuts.is_empty()) || window_duration <= 0.0 {
174 return KeyboardFeatures::default();
175 }
176
177 let typing_events: Vec<&KeyboardEvent> = events
179 .iter()
180 .filter(|e| e.event_type == KeyboardEventType::TypingTap)
181 .collect();
182
183 let navigation_events: Vec<&KeyboardEvent> = events
184 .iter()
185 .filter(|e| e.event_type == KeyboardEventType::NavigationKey)
186 .collect();
187
188 let navigation_key_presses: Vec<&KeyboardEvent> = navigation_events
190 .iter()
191 .filter(|e| e.is_key_down)
192 .copied()
193 .collect();
194 let navigation_key_count = navigation_key_presses.len() as u32;
195 let keyboard_scroll_rate = navigation_key_count as f64 / window_duration;
196
197 let backspace_count = events
199 .iter()
200 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Backspace)
201 .count() as u32;
202 let delete_count = events
203 .iter()
204 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Delete)
205 .count() as u32;
206 let enter_count = events
207 .iter()
208 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Enter)
209 .count() as u32;
210 let tab_count = events
211 .iter()
212 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Tab)
213 .count() as u32;
214 let escape_count = events
215 .iter()
216 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Escape)
217 .count() as u32;
218 let modifier_key_count = events
219 .iter()
220 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::ModifierKey)
221 .count() as u32;
222 let function_key_count = events
223 .iter()
224 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::FunctionKey)
225 .count() as u32;
226
227 let typing_key_presses: Vec<&KeyboardEvent> = typing_events
229 .iter()
230 .filter(|e| e.is_key_down)
231 .copied()
232 .collect();
233 let typing_tap_count = typing_key_presses.len() as u32;
234
235 let correction_rate = (backspace_count + delete_count) as f64 / typing_tap_count.max(1) as f64;
237 let typing_efficiency = (1.0 - correction_rate).clamp(0.0, 1.0);
238
239 let shortcut_count = shortcuts.len() as u32;
241 let shortcut_rate = shortcut_count as f64 / window_duration;
242
243 let typing_rate = typing_tap_count as f64 / window_duration;
245
246 let intervals: Vec<i64> = typing_key_presses
248 .windows(2)
249 .map(|pair| (pair[1].timestamp - pair[0].timestamp).num_milliseconds())
250 .collect();
251
252 let pauses: Vec<i64> = intervals
254 .iter()
255 .filter(|&&i| i > PAUSE_THRESHOLD_MS)
256 .copied()
257 .collect();
258 let pause_count = pauses.len() as u32;
259 let mean_pause_ms = if pauses.is_empty() {
260 0.0
261 } else {
262 pauses.iter().sum::<i64>() as f64 / pauses.len() as f64
263 };
264
265 let latency_variability = std_dev(&intervals.iter().map(|&i| i as f64).collect::<Vec<_>>());
267
268 let hold_times = compute_hold_times(&typing_events);
271 let hold_time_mean = if hold_times.is_empty() {
272 0.0
273 } else {
274 hold_times.iter().sum::<f64>() / hold_times.len() as f64
275 };
276
277 let short_interval_count = intervals.iter().filter(|&&i| i < 100).count();
280 let burst_index = if intervals.is_empty() {
281 0.0
282 } else {
283 short_interval_count as f64 / intervals.len() as f64
284 };
285
286 let active_intervals: Vec<i64> = intervals
289 .iter()
290 .filter(|&&i| i <= PAUSE_THRESHOLD_MS * 2) .copied()
292 .collect();
293 let active_time_ms: i64 = active_intervals.iter().sum();
294 let session_continuity = (active_time_ms as f64 / 1000.0) / window_duration;
295
296 let typing_cadence_stability = 1.0 / (1.0 + latency_variability / 100.0);
299
300 let typing_gap_ratio = if intervals.is_empty() {
302 0.0
303 } else {
304 pause_count as f64 / intervals.len() as f64
305 };
306
307 let normalized_speed = (typing_rate / 10.0).min(1.0); let typing_interaction_intensity =
311 (normalized_speed * 0.4 + typing_cadence_stability * 0.3 + (1.0 - typing_gap_ratio) * 0.3)
312 .clamp(0.0, 1.0);
313
314 KeyboardFeatures {
315 typing_rate,
316 pause_count,
317 mean_pause_ms,
318 latency_variability,
319 hold_time_mean,
320 burst_index,
321 session_continuity: session_continuity.min(1.0), typing_tap_count,
323 typing_cadence_stability,
324 typing_gap_ratio,
325 typing_interaction_intensity,
326 keyboard_scroll_rate,
327 navigation_key_count,
328 backspace_count,
329 delete_count,
330 correction_rate,
331 typing_efficiency,
332 enter_count,
333 tab_count,
334 escape_count,
335 modifier_key_count,
336 function_key_count,
337 shortcut_count,
338 shortcut_rate,
339 }
340}
341
342fn compute_hold_times(events: &[&KeyboardEvent]) -> Vec<f64> {
344 let mut hold_times = Vec::new();
345 let mut last_down: Option<&KeyboardEvent> = None;
346
347 for event in events {
348 if event.is_key_down {
349 last_down = Some(event);
350 } else if let Some(down) = last_down {
351 let hold_ms = (event.timestamp - down.timestamp).num_milliseconds() as f64;
352 if (20.0..=2000.0).contains(&hold_ms) {
354 hold_times.push(hold_ms);
355 }
356 last_down = None;
357 }
358 }
359
360 hold_times
361}
362
363fn compute_mouse_features(events: &[MouseEvent], window_duration: f64) -> MouseFeatures {
365 if events.is_empty() || window_duration <= 0.0 {
366 return MouseFeatures::default();
367 }
368
369 let move_events: Vec<&MouseEvent> = events
371 .iter()
372 .filter(|e| e.event_type == MouseEventType::Move)
373 .collect();
374
375 let click_events: Vec<&MouseEvent> = events
376 .iter()
377 .filter(|e| {
378 e.event_type == MouseEventType::LeftClick || e.event_type == MouseEventType::RightClick
379 })
380 .collect();
381
382 let scroll_events: Vec<&MouseEvent> = events
383 .iter()
384 .filter(|e| e.event_type == MouseEventType::Scroll)
385 .collect();
386
387 let mouse_activity_rate = move_events.len() as f64 / window_duration;
389
390 let velocities: Vec<f64> = move_events
392 .iter()
393 .filter_map(|e| e.delta_magnitude)
394 .collect();
395
396 let mean_velocity = if velocities.is_empty() {
397 0.0
398 } else {
399 velocities.iter().sum::<f64>() / velocities.len() as f64
400 };
401
402 let velocity_variability = std_dev(&velocities);
403
404 let acceleration_spikes = velocities
406 .windows(2)
407 .filter(|pair| (pair[1] - pair[0]).abs() > ACCELERATION_SPIKE_THRESHOLD)
408 .count() as u32;
409
410 let click_rate = click_events.len() as f64 / window_duration;
412 let scroll_rate = scroll_events.len() as f64 / window_duration;
413
414 let (idle_ratio, idle_time_ms, _has_long_gap) =
416 estimate_idle_metrics(&move_events, window_duration);
417
418 let micro_count = velocities
420 .iter()
421 .filter(|&&v| v < MICRO_ADJUSTMENT_THRESHOLD)
422 .count();
423 let micro_adjustment_ratio = if velocities.is_empty() {
424 0.0
425 } else {
426 micro_count as f64 / velocities.len() as f64
427 };
428
429 MouseFeatures {
430 mouse_activity_rate,
431 mean_velocity,
432 velocity_variability,
433 acceleration_spikes,
434 click_rate,
435 scroll_rate,
436 idle_ratio,
437 micro_adjustment_ratio,
438 idle_time_ms,
439 }
440}
441
442fn estimate_idle_metrics(move_events: &[&MouseEvent], window_duration: f64) -> (f64, u64, bool) {
446 if move_events.len() < 2 {
447 let total_idle = (window_duration * 1000.0) as u64;
449 return (1.0, total_idle, true);
450 }
451
452 const IDLE_THRESHOLD_MS: i64 = 1000;
454 const LONG_GAP_THRESHOLD_MS: i64 = 2000;
456
457 let mut idle_time_ms: i64 = 0;
458 let mut has_long_gap = false;
459
460 for pair in move_events.windows(2) {
461 let gap = (pair[1].timestamp - pair[0].timestamp).num_milliseconds();
462 if gap > IDLE_THRESHOLD_MS {
463 idle_time_ms += gap - IDLE_THRESHOLD_MS; }
465 if gap > LONG_GAP_THRESHOLD_MS {
466 has_long_gap = true;
467 }
468 }
469
470 let idle_secs = idle_time_ms as f64 / 1000.0;
471 let idle_ratio = (idle_secs / window_duration).min(1.0);
472
473 (idle_ratio, idle_time_ms.max(0) as u64, has_long_gap)
474}
475
476fn compute_behavioral_signals(
478 keyboard: &KeyboardFeatures,
479 mouse: &MouseFeatures,
480) -> BehavioralSignals {
481 let typing_rhythm = 1.0 / (1.0 + keyboard.latency_variability / 100.0);
484 let mouse_rhythm = 1.0 / (1.0 + mouse.velocity_variability / 50.0);
485 let interaction_rhythm = (typing_rhythm + mouse_rhythm) / 2.0;
486
487 let friction = (keyboard.pause_count as f64 * 0.1)
490 + (1.0 - keyboard.burst_index) * 0.2
491 + mouse.micro_adjustment_ratio * 0.2
492 + keyboard.correction_rate.min(1.0) * 0.3;
493
494 let motor_stability = 1.0
497 - (keyboard.latency_variability / 200.0).min(0.5)
498 - (mouse.velocity_variability / 100.0).min(0.5);
499
500 let focus_continuity_proxy = keyboard.session_continuity * 0.5 + (1.0 - mouse.idle_ratio) * 0.5;
503
504 let keyboard_burstiness = keyboard.burst_index;
508 let mouse_burstiness = if mouse.mouse_activity_rate > 0.0 {
510 mouse.idle_ratio * (1.0 - mouse.micro_adjustment_ratio)
512 } else {
513 0.0
514 };
515 let burstiness = (keyboard_burstiness * 0.6 + mouse_burstiness * 0.4).clamp(0.0, 1.0);
516
517 let has_activity = keyboard.typing_tap_count > 0 || mouse.mouse_activity_rate > 0.5;
522 let sustained_typing = keyboard.session_continuity > 0.7;
523 let minimal_idle = mouse.idle_ratio < 0.3;
524 let deep_focus_block = has_activity && sustained_typing && minimal_idle;
525
526 BehavioralSignals {
527 interaction_rhythm: interaction_rhythm.clamp(0.0, 1.0),
528 friction: friction.clamp(0.0, 1.0),
529 motor_stability: motor_stability.clamp(0.0, 1.0),
530 focus_continuity_proxy: focus_continuity_proxy.clamp(0.0, 1.0),
531 burstiness,
532 deep_focus_block,
533 }
534}
535
536fn std_dev(values: &[f64]) -> f64 {
538 if values.len() < 2 {
539 return 0.0;
540 }
541
542 let mean = values.iter().sum::<f64>() / values.len() as f64;
543 let variance = values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
544 variance.sqrt()
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use chrono::{Duration, Utc};
551
552 fn make_keyboard_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
553 KeyboardEvent {
554 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
555 is_key_down: is_down,
556 event_type: KeyboardEventType::TypingTap,
557 }
558 }
559
560 fn make_navigation_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
561 KeyboardEvent {
562 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
563 is_key_down: is_down,
564 event_type: KeyboardEventType::NavigationKey,
565 }
566 }
567
568 #[test]
569 fn test_keyboard_features_empty() {
570 let features = compute_keyboard_features(&[], &[], 10.0);
571 assert_eq!(features.typing_rate, 0.0);
572 }
573
574 #[test]
575 fn test_keyboard_features_basic() {
576 let events = vec![
577 make_keyboard_event(true, 0),
578 make_keyboard_event(false, 50),
579 make_keyboard_event(true, 100),
580 make_keyboard_event(false, 150),
581 make_keyboard_event(true, 200),
582 make_keyboard_event(false, 250),
583 ];
584
585 let features = compute_keyboard_features(&events, &[], 1.0);
586 assert_eq!(features.typing_rate, 3.0); }
588
589 #[test]
590 fn test_std_dev() {
591 let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
592 let sd = std_dev(&values);
593 assert!((sd - 2.0).abs() < 0.1);
594 }
595
596 #[test]
597 fn test_behavioral_signals_bounds() {
598 let keyboard = KeyboardFeatures::default();
599 let mouse = MouseFeatures::default();
600 let signals = compute_behavioral_signals(&keyboard, &mouse);
601
602 assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
604 assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
605 assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
606 assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
607 }
608
609 #[test]
610 fn test_typing_tap_count() {
611 let events = vec![
612 make_keyboard_event(true, 0),
613 make_keyboard_event(false, 50),
614 make_keyboard_event(true, 100),
615 make_keyboard_event(false, 150),
616 make_keyboard_event(true, 200),
617 make_keyboard_event(false, 250),
618 ];
619
620 let features = compute_keyboard_features(&events, &[], 1.0);
621 assert_eq!(features.typing_tap_count, 3); }
623
624 #[test]
625 fn test_typing_cadence_stability_bounds() {
626 let features_empty = compute_keyboard_features(&[], &[], 10.0);
628 assert!(
629 features_empty.typing_cadence_stability >= 0.0
630 && features_empty.typing_cadence_stability <= 1.0
631 );
632
633 let events = vec![
635 make_keyboard_event(true, 0),
636 make_keyboard_event(false, 50),
637 make_keyboard_event(true, 100),
638 make_keyboard_event(false, 150),
639 make_keyboard_event(true, 200),
640 make_keyboard_event(false, 250),
641 ];
642 let features = compute_keyboard_features(&events, &[], 1.0);
643 assert!(
644 features.typing_cadence_stability >= 0.0 && features.typing_cadence_stability <= 1.0
645 );
646 assert!(features.typing_cadence_stability > 0.5);
648 }
649
650 #[test]
651 fn test_typing_gap_ratio_bounds() {
652 let features_empty = compute_keyboard_features(&[], &[], 10.0);
653 assert_eq!(features_empty.typing_gap_ratio, 0.0);
654
655 let events = vec![
657 make_keyboard_event(true, 0),
658 make_keyboard_event(false, 50),
659 make_keyboard_event(true, 100),
660 make_keyboard_event(false, 150),
661 ];
662 let features = compute_keyboard_features(&events, &[], 1.0);
663 assert!(features.typing_gap_ratio >= 0.0 && features.typing_gap_ratio <= 1.0);
664 assert_eq!(features.typing_gap_ratio, 0.0); let events_with_gaps = vec![
668 make_keyboard_event(true, 0),
669 make_keyboard_event(false, 50),
670 make_keyboard_event(true, 600), make_keyboard_event(false, 650),
672 ];
673 let features_gaps = compute_keyboard_features(&events_with_gaps, &[], 1.0);
674 assert!(features_gaps.typing_gap_ratio > 0.0); }
676
677 #[test]
678 fn test_typing_interaction_intensity_bounds() {
679 let features_empty = compute_keyboard_features(&[], &[], 10.0);
680 assert!(
681 features_empty.typing_interaction_intensity >= 0.0
682 && features_empty.typing_interaction_intensity <= 1.0
683 );
684
685 let fast_events = vec![
687 make_keyboard_event(true, 0),
688 make_keyboard_event(false, 30),
689 make_keyboard_event(true, 60),
690 make_keyboard_event(false, 90),
691 make_keyboard_event(true, 120),
692 make_keyboard_event(false, 150),
693 make_keyboard_event(true, 180),
694 make_keyboard_event(false, 210),
695 make_keyboard_event(true, 240),
696 make_keyboard_event(false, 270),
697 ];
698 let features = compute_keyboard_features(&fast_events, &[], 1.0);
699 assert!(
700 features.typing_interaction_intensity >= 0.0
701 && features.typing_interaction_intensity <= 1.0
702 );
703 assert!(features.typing_interaction_intensity > 0.3);
705 }
706
707 #[test]
708 fn test_navigation_key_separation() {
709 let events = vec![
711 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), ];
720
721 let features = compute_keyboard_features(&events, &[], 1.0);
722
723 assert_eq!(features.typing_tap_count, 2);
725 assert_eq!(features.typing_rate, 2.0);
726
727 assert_eq!(features.navigation_key_count, 2);
729 assert_eq!(features.keyboard_scroll_rate, 2.0);
730 }
731
732 #[test]
733 fn test_navigation_keys_dont_inflate_typing_metrics() {
734 let nav_only_events = vec![
736 make_navigation_event(true, 0),
737 make_navigation_event(false, 50),
738 make_navigation_event(true, 100),
739 make_navigation_event(false, 150),
740 make_navigation_event(true, 200),
741 make_navigation_event(false, 250),
742 ];
743
744 let features = compute_keyboard_features(&nav_only_events, &[], 1.0);
745
746 assert_eq!(features.typing_tap_count, 0);
748 assert_eq!(features.typing_rate, 0.0);
749
750 assert_eq!(features.navigation_key_count, 3);
752 assert_eq!(features.keyboard_scroll_rate, 3.0);
753 }
754
755 #[test]
756 fn test_keyboard_scroll_rate_bounds() {
757 let features_empty = compute_keyboard_features(&[], &[], 10.0);
758 assert_eq!(features_empty.keyboard_scroll_rate, 0.0);
759 assert_eq!(features_empty.navigation_key_count, 0);
760
761 let nav_events = vec![
763 make_navigation_event(true, 0),
764 make_navigation_event(false, 30),
765 make_navigation_event(true, 60),
766 make_navigation_event(false, 90),
767 make_navigation_event(true, 120),
768 make_navigation_event(false, 150),
769 ];
770 let features = compute_keyboard_features(&nav_events, &[], 1.0);
771 assert_eq!(features.navigation_key_count, 3);
772 assert!(features.keyboard_scroll_rate > 0.0);
773 }
774
775 #[test]
776 fn test_burstiness_bounds() {
777 let keyboard = KeyboardFeatures::default();
778 let mouse = MouseFeatures::default();
779 let signals = compute_behavioral_signals(&keyboard, &mouse);
780
781 assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
783 }
784
785 #[test]
786 fn test_burstiness_high_burst_index() {
787 let keyboard = KeyboardFeatures {
789 burst_index: 0.9, ..Default::default()
791 };
792
793 let mouse = MouseFeatures::default();
794 let signals = compute_behavioral_signals(&keyboard, &mouse);
795
796 assert!(signals.burstiness > 0.4);
798 assert!(signals.burstiness <= 1.0);
799 }
800
801 #[test]
802 fn test_deep_focus_block_detection() {
803 let keyboard = KeyboardFeatures::default();
805 let mouse = MouseFeatures::default();
806 let signals = compute_behavioral_signals(&keyboard, &mouse);
807 assert!(!signals.deep_focus_block);
808
809 let keyboard_active = KeyboardFeatures {
811 session_continuity: 0.9, typing_tap_count: 50, ..Default::default()
814 };
815
816 let mouse_active = MouseFeatures {
817 idle_ratio: 0.1, mouse_activity_rate: 2.0,
819 ..Default::default()
820 };
821
822 let signals_active = compute_behavioral_signals(&keyboard_active, &mouse_active);
823 assert!(signals_active.deep_focus_block);
824 }
825
826 #[test]
827 fn test_deep_focus_block_requires_low_idle() {
828 let keyboard = KeyboardFeatures {
830 session_continuity: 0.9,
831 typing_tap_count: 50,
832 ..Default::default()
833 };
834
835 let mouse = MouseFeatures {
836 idle_ratio: 0.5, ..Default::default()
838 };
839
840 let signals = compute_behavioral_signals(&keyboard, &mouse);
841 assert!(!signals.deep_focus_block);
842 }
843
844 #[test]
845 fn test_idle_time_ms_computation() {
846 use crate::collector::types::MouseEvent;
847
848 let base_time = chrono::Utc::now();
850 let events = vec![
851 MouseEvent {
852 timestamp: base_time,
853 event_type: MouseEventType::Move,
854 delta_magnitude: Some(10.0),
855 scroll_direction: None,
856 scroll_magnitude: None,
857 },
858 MouseEvent {
859 timestamp: base_time + chrono::Duration::milliseconds(500),
860 event_type: MouseEventType::Move,
861 delta_magnitude: Some(10.0),
862 scroll_direction: None,
863 scroll_magnitude: None,
864 },
865 MouseEvent {
866 timestamp: base_time + chrono::Duration::milliseconds(2000), event_type: MouseEventType::Move,
868 delta_magnitude: Some(10.0),
869 scroll_direction: None,
870 scroll_magnitude: None,
871 },
872 ];
873
874 let features = compute_mouse_features(&events, 2.0);
875
876 assert!(features.idle_time_ms > 0);
878 assert!(features.idle_ratio > 0.0);
879 }
880
881 #[test]
882 fn test_behavioral_signals_new_fields_bounds() {
883 let keyboard = KeyboardFeatures {
885 burst_index: 0.5,
886 session_continuity: 0.5,
887 typing_tap_count: 10,
888 ..Default::default()
889 };
890
891 let mouse = MouseFeatures {
892 idle_ratio: 0.5,
893 mouse_activity_rate: 1.0,
894 ..Default::default()
895 };
896
897 let signals = compute_behavioral_signals(&keyboard, &mouse);
898
899 assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
901 assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
902 assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
903 assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
904 assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
905 }
907
908 fn make_special_key_event(
909 event_type: KeyboardEventType,
910 is_down: bool,
911 offset_ms: i64,
912 ) -> KeyboardEvent {
913 KeyboardEvent {
914 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
915 is_key_down: is_down,
916 event_type,
917 }
918 }
919
920 #[test]
921 fn test_correction_rate_computation() {
922 let events = vec![
923 make_keyboard_event(true, 0),
924 make_keyboard_event(false, 50),
925 make_keyboard_event(true, 100),
926 make_keyboard_event(false, 150),
927 make_keyboard_event(true, 200),
928 make_keyboard_event(false, 250),
929 make_special_key_event(KeyboardEventType::Backspace, true, 300),
930 make_special_key_event(KeyboardEventType::Backspace, false, 350),
931 ];
932
933 let features = compute_keyboard_features(&events, &[], 1.0);
934 assert_eq!(features.typing_tap_count, 3);
935 assert_eq!(features.backspace_count, 1);
936 assert_eq!(features.delete_count, 0);
937 assert!((features.correction_rate - 1.0 / 3.0).abs() < 0.01);
939 assert!((features.typing_efficiency - (1.0 - 1.0 / 3.0)).abs() < 0.01);
940 }
941
942 #[test]
943 fn test_correction_rate_zero_typing() {
944 let events = vec![
946 make_special_key_event(KeyboardEventType::Backspace, true, 0),
947 make_special_key_event(KeyboardEventType::Backspace, false, 50),
948 ];
949
950 let features = compute_keyboard_features(&events, &[], 1.0);
951 assert_eq!(features.typing_tap_count, 0);
952 assert_eq!(features.backspace_count, 1);
953 assert!((features.correction_rate - 1.0).abs() < 0.01);
955 assert_eq!(features.typing_efficiency, 0.0);
956 }
957
958 #[test]
959 fn test_special_key_counts() {
960 let events = vec![
961 make_special_key_event(KeyboardEventType::Enter, true, 0),
962 make_special_key_event(KeyboardEventType::Enter, false, 50),
963 make_special_key_event(KeyboardEventType::Tab, true, 100),
964 make_special_key_event(KeyboardEventType::Tab, false, 150),
965 make_special_key_event(KeyboardEventType::Escape, true, 200),
966 make_special_key_event(KeyboardEventType::Escape, false, 250),
967 make_special_key_event(KeyboardEventType::ModifierKey, true, 300),
968 make_special_key_event(KeyboardEventType::FunctionKey, true, 400),
969 make_special_key_event(KeyboardEventType::FunctionKey, false, 450),
970 ];
971
972 let features = compute_keyboard_features(&events, &[], 1.0);
973 assert_eq!(features.enter_count, 1);
974 assert_eq!(features.tab_count, 1);
975 assert_eq!(features.escape_count, 1);
976 assert_eq!(features.modifier_key_count, 1);
977 assert_eq!(features.function_key_count, 1);
978 assert_eq!(features.typing_tap_count, 0); }
980
981 #[test]
982 fn test_shortcut_metrics() {
983 use crate::collector::types::{ShortcutEvent, ShortcutType};
984
985 let shortcuts = vec![
986 ShortcutEvent {
987 timestamp: Utc::now(),
988 shortcut_type: ShortcutType::Copy,
989 },
990 ShortcutEvent {
991 timestamp: Utc::now() + Duration::milliseconds(500),
992 shortcut_type: ShortcutType::Paste,
993 },
994 ];
995
996 let features = compute_keyboard_features(&[], &shortcuts, 10.0);
997 assert_eq!(features.shortcut_count, 2);
998 assert!((features.shortcut_rate - 0.2).abs() < 0.01); }
1000
1001 #[test]
1002 fn test_friction_includes_correction_rate() {
1003 let keyboard_with_corrections = KeyboardFeatures {
1005 correction_rate: 0.5,
1006 burst_index: 0.5,
1007 ..Default::default()
1008 };
1009
1010 let keyboard_without = KeyboardFeatures {
1011 correction_rate: 0.0,
1012 burst_index: 0.5,
1013 ..Default::default()
1014 };
1015
1016 let mouse = MouseFeatures::default();
1017
1018 let signals_with = compute_behavioral_signals(&keyboard_with_corrections, &mouse);
1019 let signals_without = compute_behavioral_signals(&keyboard_without, &mouse);
1020
1021 assert!(signals_with.friction > signals_without.friction);
1022 }
1023}