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,
130 pub mouse: MouseFeatures,
131 pub behavioral: BehavioralSignals,
132}
133
134const PAUSE_THRESHOLD_MS: i64 = 500;
136
137const MICRO_ADJUSTMENT_THRESHOLD: f64 = 5.0;
139
140const ACCELERATION_SPIKE_THRESHOLD: f64 = 50.0;
142
143pub fn compute_features(window: &EventWindow) -> WindowFeatures {
145 let keyboard = compute_keyboard_features(
146 &window.keyboard_events,
147 &window.shortcut_events,
148 window.duration_secs(),
149 );
150 let mouse = compute_mouse_features(&window.mouse_events, window.duration_secs());
151 let behavioral = compute_behavioral_signals(&keyboard, &mouse);
152
153 WindowFeatures {
154 keyboard,
155 mouse,
156 behavioral,
157 }
158}
159
160fn compute_keyboard_features(
166 events: &[KeyboardEvent],
167 shortcuts: &[ShortcutEvent],
168 window_duration: f64,
169) -> KeyboardFeatures {
170 if (events.is_empty() && shortcuts.is_empty()) || window_duration <= 0.0 {
171 return KeyboardFeatures::default();
172 }
173
174 let typing_events: Vec<&KeyboardEvent> = events
176 .iter()
177 .filter(|e| e.event_type == KeyboardEventType::TypingTap)
178 .collect();
179
180 let navigation_events: Vec<&KeyboardEvent> = events
181 .iter()
182 .filter(|e| e.event_type == KeyboardEventType::NavigationKey)
183 .collect();
184
185 let navigation_key_presses: Vec<&KeyboardEvent> = navigation_events
187 .iter()
188 .filter(|e| e.is_key_down)
189 .copied()
190 .collect();
191 let navigation_key_count = navigation_key_presses.len() as u32;
192 let keyboard_scroll_rate = navigation_key_count as f64 / window_duration;
193
194 let backspace_count = events
196 .iter()
197 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Backspace)
198 .count() as u32;
199 let delete_count = events
200 .iter()
201 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Delete)
202 .count() as u32;
203 let enter_count = events
204 .iter()
205 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Enter)
206 .count() as u32;
207 let tab_count = events
208 .iter()
209 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Tab)
210 .count() as u32;
211 let escape_count = events
212 .iter()
213 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::Escape)
214 .count() as u32;
215 let modifier_key_count = events
216 .iter()
217 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::ModifierKey)
218 .count() as u32;
219 let function_key_count = events
220 .iter()
221 .filter(|e| e.is_key_down && e.event_type == KeyboardEventType::FunctionKey)
222 .count() as u32;
223
224 let typing_key_presses: Vec<&KeyboardEvent> = typing_events
226 .iter()
227 .filter(|e| e.is_key_down)
228 .copied()
229 .collect();
230 let typing_tap_count = typing_key_presses.len() as u32;
231
232 let correction_rate = (backspace_count + delete_count) as f64 / typing_tap_count.max(1) as f64;
234 let typing_efficiency = (1.0 - correction_rate).clamp(0.0, 1.0);
235
236 let shortcut_count = shortcuts.len() as u32;
238 let shortcut_rate = shortcut_count as f64 / window_duration;
239
240 let typing_rate = typing_tap_count as f64 / window_duration;
242
243 let intervals: Vec<i64> = typing_key_presses
245 .windows(2)
246 .map(|pair| (pair[1].timestamp - pair[0].timestamp).num_milliseconds())
247 .collect();
248
249 let pauses: Vec<i64> = intervals
251 .iter()
252 .filter(|&&i| i > PAUSE_THRESHOLD_MS)
253 .copied()
254 .collect();
255 let pause_count = pauses.len() as u32;
256 let mean_pause_ms = if pauses.is_empty() {
257 0.0
258 } else {
259 pauses.iter().sum::<i64>() as f64 / pauses.len() as f64
260 };
261
262 let latency_variability = std_dev(&intervals.iter().map(|&i| i as f64).collect::<Vec<_>>());
264
265 let hold_times = compute_hold_times(&typing_events);
268 let hold_time_mean = if hold_times.is_empty() {
269 0.0
270 } else {
271 hold_times.iter().sum::<f64>() / hold_times.len() as f64
272 };
273
274 let short_interval_count = intervals.iter().filter(|&&i| i < 100).count();
277 let burst_index = if intervals.is_empty() {
278 0.0
279 } else {
280 short_interval_count as f64 / intervals.len() as f64
281 };
282
283 let active_intervals: Vec<i64> = intervals
286 .iter()
287 .filter(|&&i| i <= PAUSE_THRESHOLD_MS * 2) .copied()
289 .collect();
290 let active_time_ms: i64 = active_intervals.iter().sum();
291 let session_continuity = (active_time_ms as f64 / 1000.0) / window_duration;
292
293 let typing_cadence_stability = 1.0 / (1.0 + latency_variability / 100.0);
296
297 let typing_gap_ratio = if intervals.is_empty() {
299 0.0
300 } else {
301 pause_count as f64 / intervals.len() as f64
302 };
303
304 let normalized_speed = (typing_rate / 10.0).min(1.0); let typing_interaction_intensity =
308 (normalized_speed * 0.4 + typing_cadence_stability * 0.3 + (1.0 - typing_gap_ratio) * 0.3)
309 .clamp(0.0, 1.0);
310
311 KeyboardFeatures {
312 typing_rate,
313 pause_count,
314 mean_pause_ms,
315 latency_variability,
316 hold_time_mean,
317 burst_index,
318 session_continuity: session_continuity.min(1.0), typing_tap_count,
320 typing_cadence_stability,
321 typing_gap_ratio,
322 typing_interaction_intensity,
323 keyboard_scroll_rate,
324 navigation_key_count,
325 backspace_count,
326 delete_count,
327 correction_rate,
328 typing_efficiency,
329 enter_count,
330 tab_count,
331 escape_count,
332 modifier_key_count,
333 function_key_count,
334 shortcut_count,
335 shortcut_rate,
336 }
337}
338
339fn compute_hold_times(events: &[&KeyboardEvent]) -> Vec<f64> {
341 let mut hold_times = Vec::new();
342 let mut last_down: Option<&KeyboardEvent> = None;
343
344 for event in events {
345 if event.is_key_down {
346 last_down = Some(event);
347 } else if let Some(down) = last_down {
348 let hold_ms = (event.timestamp - down.timestamp).num_milliseconds() as f64;
349 if (20.0..=2000.0).contains(&hold_ms) {
351 hold_times.push(hold_ms);
352 }
353 last_down = None;
354 }
355 }
356
357 hold_times
358}
359
360fn compute_mouse_features(events: &[MouseEvent], window_duration: f64) -> MouseFeatures {
362 if events.is_empty() || window_duration <= 0.0 {
363 return MouseFeatures::default();
364 }
365
366 let move_events: Vec<&MouseEvent> = events
368 .iter()
369 .filter(|e| e.event_type == MouseEventType::Move)
370 .collect();
371
372 let click_events: Vec<&MouseEvent> = events
373 .iter()
374 .filter(|e| {
375 e.event_type == MouseEventType::LeftClick || e.event_type == MouseEventType::RightClick
376 })
377 .collect();
378
379 let scroll_events: Vec<&MouseEvent> = events
380 .iter()
381 .filter(|e| e.event_type == MouseEventType::Scroll)
382 .collect();
383
384 let mouse_activity_rate = move_events.len() as f64 / window_duration;
386
387 let velocities: Vec<f64> = move_events
389 .iter()
390 .filter_map(|e| e.delta_magnitude)
391 .collect();
392
393 let mean_velocity = if velocities.is_empty() {
394 0.0
395 } else {
396 velocities.iter().sum::<f64>() / velocities.len() as f64
397 };
398
399 let velocity_variability = std_dev(&velocities);
400
401 let acceleration_spikes = velocities
403 .windows(2)
404 .filter(|pair| (pair[1] - pair[0]).abs() > ACCELERATION_SPIKE_THRESHOLD)
405 .count() as u32;
406
407 let click_rate = click_events.len() as f64 / window_duration;
409 let scroll_rate = scroll_events.len() as f64 / window_duration;
410
411 let (idle_ratio, idle_time_ms, _has_long_gap) =
413 estimate_idle_metrics(&move_events, window_duration);
414
415 let micro_count = velocities
417 .iter()
418 .filter(|&&v| v < MICRO_ADJUSTMENT_THRESHOLD)
419 .count();
420 let micro_adjustment_ratio = if velocities.is_empty() {
421 0.0
422 } else {
423 micro_count as f64 / velocities.len() as f64
424 };
425
426 MouseFeatures {
427 mouse_activity_rate,
428 mean_velocity,
429 velocity_variability,
430 acceleration_spikes,
431 click_rate,
432 scroll_rate,
433 idle_ratio,
434 micro_adjustment_ratio,
435 idle_time_ms,
436 }
437}
438
439fn estimate_idle_metrics(move_events: &[&MouseEvent], window_duration: f64) -> (f64, u64, bool) {
443 if move_events.len() < 2 {
444 let total_idle = (window_duration * 1000.0) as u64;
446 return (1.0, total_idle, true);
447 }
448
449 const IDLE_THRESHOLD_MS: i64 = 1000;
451 const LONG_GAP_THRESHOLD_MS: i64 = 2000;
453
454 let mut idle_time_ms: i64 = 0;
455 let mut has_long_gap = false;
456
457 for pair in move_events.windows(2) {
458 let gap = (pair[1].timestamp - pair[0].timestamp).num_milliseconds();
459 if gap > IDLE_THRESHOLD_MS {
460 idle_time_ms += gap - IDLE_THRESHOLD_MS; }
462 if gap > LONG_GAP_THRESHOLD_MS {
463 has_long_gap = true;
464 }
465 }
466
467 let idle_secs = idle_time_ms as f64 / 1000.0;
468 let idle_ratio = (idle_secs / window_duration).min(1.0);
469
470 (idle_ratio, idle_time_ms.max(0) as u64, has_long_gap)
471}
472
473fn compute_behavioral_signals(
475 keyboard: &KeyboardFeatures,
476 mouse: &MouseFeatures,
477) -> BehavioralSignals {
478 let typing_rhythm = 1.0 / (1.0 + keyboard.latency_variability / 100.0);
481 let mouse_rhythm = 1.0 / (1.0 + mouse.velocity_variability / 50.0);
482 let interaction_rhythm = (typing_rhythm + mouse_rhythm) / 2.0;
483
484 let friction = (keyboard.pause_count as f64 * 0.1)
487 + (1.0 - keyboard.burst_index) * 0.2
488 + mouse.micro_adjustment_ratio * 0.2
489 + keyboard.correction_rate.min(1.0) * 0.3;
490
491 let motor_stability = 1.0
494 - (keyboard.latency_variability / 200.0).min(0.5)
495 - (mouse.velocity_variability / 100.0).min(0.5);
496
497 let focus_continuity_proxy = keyboard.session_continuity * 0.5 + (1.0 - mouse.idle_ratio) * 0.5;
500
501 let keyboard_burstiness = keyboard.burst_index;
505 let mouse_burstiness = if mouse.mouse_activity_rate > 0.0 {
507 mouse.idle_ratio * (1.0 - mouse.micro_adjustment_ratio)
509 } else {
510 0.0
511 };
512 let burstiness = (keyboard_burstiness * 0.6 + mouse_burstiness * 0.4).clamp(0.0, 1.0);
513
514 let has_activity = keyboard.typing_tap_count > 0 || mouse.mouse_activity_rate > 0.5;
519 let sustained_typing = keyboard.session_continuity > 0.7;
520 let minimal_idle = mouse.idle_ratio < 0.3;
521 let deep_focus_block = has_activity && sustained_typing && minimal_idle;
522
523 BehavioralSignals {
524 interaction_rhythm: interaction_rhythm.clamp(0.0, 1.0),
525 friction: friction.clamp(0.0, 1.0),
526 motor_stability: motor_stability.clamp(0.0, 1.0),
527 focus_continuity_proxy: focus_continuity_proxy.clamp(0.0, 1.0),
528 burstiness,
529 deep_focus_block,
530 }
531}
532
533fn std_dev(values: &[f64]) -> f64 {
535 if values.len() < 2 {
536 return 0.0;
537 }
538
539 let mean = values.iter().sum::<f64>() / values.len() as f64;
540 let variance = values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
541 variance.sqrt()
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use chrono::{Duration, Utc};
548
549 fn make_keyboard_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
550 KeyboardEvent {
551 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
552 is_key_down: is_down,
553 event_type: KeyboardEventType::TypingTap,
554 }
555 }
556
557 fn make_navigation_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
558 KeyboardEvent {
559 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
560 is_key_down: is_down,
561 event_type: KeyboardEventType::NavigationKey,
562 }
563 }
564
565 #[test]
566 fn test_keyboard_features_empty() {
567 let features = compute_keyboard_features(&[], &[], 10.0);
568 assert_eq!(features.typing_rate, 0.0);
569 }
570
571 #[test]
572 fn test_keyboard_features_basic() {
573 let events = vec![
574 make_keyboard_event(true, 0),
575 make_keyboard_event(false, 50),
576 make_keyboard_event(true, 100),
577 make_keyboard_event(false, 150),
578 make_keyboard_event(true, 200),
579 make_keyboard_event(false, 250),
580 ];
581
582 let features = compute_keyboard_features(&events, &[], 1.0);
583 assert_eq!(features.typing_rate, 3.0); }
585
586 #[test]
587 fn test_std_dev() {
588 let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
589 let sd = std_dev(&values);
590 assert!((sd - 2.0).abs() < 0.1);
591 }
592
593 #[test]
594 fn test_behavioral_signals_bounds() {
595 let keyboard = KeyboardFeatures::default();
596 let mouse = MouseFeatures::default();
597 let signals = compute_behavioral_signals(&keyboard, &mouse);
598
599 assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
601 assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
602 assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
603 assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
604 }
605
606 #[test]
607 fn test_typing_tap_count() {
608 let events = vec![
609 make_keyboard_event(true, 0),
610 make_keyboard_event(false, 50),
611 make_keyboard_event(true, 100),
612 make_keyboard_event(false, 150),
613 make_keyboard_event(true, 200),
614 make_keyboard_event(false, 250),
615 ];
616
617 let features = compute_keyboard_features(&events, &[], 1.0);
618 assert_eq!(features.typing_tap_count, 3); }
620
621 #[test]
622 fn test_typing_cadence_stability_bounds() {
623 let features_empty = compute_keyboard_features(&[], &[], 10.0);
625 assert!(
626 features_empty.typing_cadence_stability >= 0.0
627 && features_empty.typing_cadence_stability <= 1.0
628 );
629
630 let events = vec![
632 make_keyboard_event(true, 0),
633 make_keyboard_event(false, 50),
634 make_keyboard_event(true, 100),
635 make_keyboard_event(false, 150),
636 make_keyboard_event(true, 200),
637 make_keyboard_event(false, 250),
638 ];
639 let features = compute_keyboard_features(&events, &[], 1.0);
640 assert!(
641 features.typing_cadence_stability >= 0.0 && features.typing_cadence_stability <= 1.0
642 );
643 assert!(features.typing_cadence_stability > 0.5);
645 }
646
647 #[test]
648 fn test_typing_gap_ratio_bounds() {
649 let features_empty = compute_keyboard_features(&[], &[], 10.0);
650 assert_eq!(features_empty.typing_gap_ratio, 0.0);
651
652 let events = vec![
654 make_keyboard_event(true, 0),
655 make_keyboard_event(false, 50),
656 make_keyboard_event(true, 100),
657 make_keyboard_event(false, 150),
658 ];
659 let features = compute_keyboard_features(&events, &[], 1.0);
660 assert!(features.typing_gap_ratio >= 0.0 && features.typing_gap_ratio <= 1.0);
661 assert_eq!(features.typing_gap_ratio, 0.0); let events_with_gaps = vec![
665 make_keyboard_event(true, 0),
666 make_keyboard_event(false, 50),
667 make_keyboard_event(true, 600), make_keyboard_event(false, 650),
669 ];
670 let features_gaps = compute_keyboard_features(&events_with_gaps, &[], 1.0);
671 assert!(features_gaps.typing_gap_ratio > 0.0); }
673
674 #[test]
675 fn test_typing_interaction_intensity_bounds() {
676 let features_empty = compute_keyboard_features(&[], &[], 10.0);
677 assert!(
678 features_empty.typing_interaction_intensity >= 0.0
679 && features_empty.typing_interaction_intensity <= 1.0
680 );
681
682 let fast_events = vec![
684 make_keyboard_event(true, 0),
685 make_keyboard_event(false, 30),
686 make_keyboard_event(true, 60),
687 make_keyboard_event(false, 90),
688 make_keyboard_event(true, 120),
689 make_keyboard_event(false, 150),
690 make_keyboard_event(true, 180),
691 make_keyboard_event(false, 210),
692 make_keyboard_event(true, 240),
693 make_keyboard_event(false, 270),
694 ];
695 let features = compute_keyboard_features(&fast_events, &[], 1.0);
696 assert!(
697 features.typing_interaction_intensity >= 0.0
698 && features.typing_interaction_intensity <= 1.0
699 );
700 assert!(features.typing_interaction_intensity > 0.3);
702 }
703
704 #[test]
705 fn test_navigation_key_separation() {
706 let events = vec![
708 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), ];
717
718 let features = compute_keyboard_features(&events, &[], 1.0);
719
720 assert_eq!(features.typing_tap_count, 2);
722 assert_eq!(features.typing_rate, 2.0);
723
724 assert_eq!(features.navigation_key_count, 2);
726 assert_eq!(features.keyboard_scroll_rate, 2.0);
727 }
728
729 #[test]
730 fn test_navigation_keys_dont_inflate_typing_metrics() {
731 let nav_only_events = vec![
733 make_navigation_event(true, 0),
734 make_navigation_event(false, 50),
735 make_navigation_event(true, 100),
736 make_navigation_event(false, 150),
737 make_navigation_event(true, 200),
738 make_navigation_event(false, 250),
739 ];
740
741 let features = compute_keyboard_features(&nav_only_events, &[], 1.0);
742
743 assert_eq!(features.typing_tap_count, 0);
745 assert_eq!(features.typing_rate, 0.0);
746
747 assert_eq!(features.navigation_key_count, 3);
749 assert_eq!(features.keyboard_scroll_rate, 3.0);
750 }
751
752 #[test]
753 fn test_keyboard_scroll_rate_bounds() {
754 let features_empty = compute_keyboard_features(&[], &[], 10.0);
755 assert_eq!(features_empty.keyboard_scroll_rate, 0.0);
756 assert_eq!(features_empty.navigation_key_count, 0);
757
758 let nav_events = vec![
760 make_navigation_event(true, 0),
761 make_navigation_event(false, 30),
762 make_navigation_event(true, 60),
763 make_navigation_event(false, 90),
764 make_navigation_event(true, 120),
765 make_navigation_event(false, 150),
766 ];
767 let features = compute_keyboard_features(&nav_events, &[], 1.0);
768 assert_eq!(features.navigation_key_count, 3);
769 assert!(features.keyboard_scroll_rate > 0.0);
770 }
771
772 #[test]
773 fn test_burstiness_bounds() {
774 let keyboard = KeyboardFeatures::default();
775 let mouse = MouseFeatures::default();
776 let signals = compute_behavioral_signals(&keyboard, &mouse);
777
778 assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
780 }
781
782 #[test]
783 fn test_burstiness_high_burst_index() {
784 let keyboard = KeyboardFeatures {
786 burst_index: 0.9, ..Default::default()
788 };
789
790 let mouse = MouseFeatures::default();
791 let signals = compute_behavioral_signals(&keyboard, &mouse);
792
793 assert!(signals.burstiness > 0.4);
795 assert!(signals.burstiness <= 1.0);
796 }
797
798 #[test]
799 fn test_deep_focus_block_detection() {
800 let keyboard = KeyboardFeatures::default();
802 let mouse = MouseFeatures::default();
803 let signals = compute_behavioral_signals(&keyboard, &mouse);
804 assert!(!signals.deep_focus_block);
805
806 let keyboard_active = KeyboardFeatures {
808 session_continuity: 0.9, typing_tap_count: 50, ..Default::default()
811 };
812
813 let mouse_active = MouseFeatures {
814 idle_ratio: 0.1, mouse_activity_rate: 2.0,
816 ..Default::default()
817 };
818
819 let signals_active = compute_behavioral_signals(&keyboard_active, &mouse_active);
820 assert!(signals_active.deep_focus_block);
821 }
822
823 #[test]
824 fn test_deep_focus_block_requires_low_idle() {
825 let keyboard = KeyboardFeatures {
827 session_continuity: 0.9,
828 typing_tap_count: 50,
829 ..Default::default()
830 };
831
832 let mouse = MouseFeatures {
833 idle_ratio: 0.5, ..Default::default()
835 };
836
837 let signals = compute_behavioral_signals(&keyboard, &mouse);
838 assert!(!signals.deep_focus_block);
839 }
840
841 #[test]
842 fn test_idle_time_ms_computation() {
843 use crate::collector::types::MouseEvent;
844
845 let base_time = chrono::Utc::now();
847 let events = vec![
848 MouseEvent {
849 timestamp: base_time,
850 event_type: MouseEventType::Move,
851 delta_magnitude: Some(10.0),
852 scroll_direction: None,
853 scroll_magnitude: None,
854 },
855 MouseEvent {
856 timestamp: base_time + chrono::Duration::milliseconds(500),
857 event_type: MouseEventType::Move,
858 delta_magnitude: Some(10.0),
859 scroll_direction: None,
860 scroll_magnitude: None,
861 },
862 MouseEvent {
863 timestamp: base_time + chrono::Duration::milliseconds(2000), event_type: MouseEventType::Move,
865 delta_magnitude: Some(10.0),
866 scroll_direction: None,
867 scroll_magnitude: None,
868 },
869 ];
870
871 let features = compute_mouse_features(&events, 2.0);
872
873 assert!(features.idle_time_ms > 0);
875 assert!(features.idle_ratio > 0.0);
876 }
877
878 #[test]
879 fn test_behavioral_signals_new_fields_bounds() {
880 let keyboard = KeyboardFeatures {
882 burst_index: 0.5,
883 session_continuity: 0.5,
884 typing_tap_count: 10,
885 ..Default::default()
886 };
887
888 let mouse = MouseFeatures {
889 idle_ratio: 0.5,
890 mouse_activity_rate: 1.0,
891 ..Default::default()
892 };
893
894 let signals = compute_behavioral_signals(&keyboard, &mouse);
895
896 assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
898 assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
899 assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
900 assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
901 assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
902 }
904
905 fn make_special_key_event(
906 event_type: KeyboardEventType,
907 is_down: bool,
908 offset_ms: i64,
909 ) -> KeyboardEvent {
910 KeyboardEvent {
911 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
912 is_key_down: is_down,
913 event_type,
914 }
915 }
916
917 #[test]
918 fn test_correction_rate_computation() {
919 let events = vec![
920 make_keyboard_event(true, 0),
921 make_keyboard_event(false, 50),
922 make_keyboard_event(true, 100),
923 make_keyboard_event(false, 150),
924 make_keyboard_event(true, 200),
925 make_keyboard_event(false, 250),
926 make_special_key_event(KeyboardEventType::Backspace, true, 300),
927 make_special_key_event(KeyboardEventType::Backspace, false, 350),
928 ];
929
930 let features = compute_keyboard_features(&events, &[], 1.0);
931 assert_eq!(features.typing_tap_count, 3);
932 assert_eq!(features.backspace_count, 1);
933 assert_eq!(features.delete_count, 0);
934 assert!((features.correction_rate - 1.0 / 3.0).abs() < 0.01);
936 assert!((features.typing_efficiency - (1.0 - 1.0 / 3.0)).abs() < 0.01);
937 }
938
939 #[test]
940 fn test_correction_rate_zero_typing() {
941 let events = vec![
943 make_special_key_event(KeyboardEventType::Backspace, true, 0),
944 make_special_key_event(KeyboardEventType::Backspace, false, 50),
945 ];
946
947 let features = compute_keyboard_features(&events, &[], 1.0);
948 assert_eq!(features.typing_tap_count, 0);
949 assert_eq!(features.backspace_count, 1);
950 assert!((features.correction_rate - 1.0).abs() < 0.01);
952 assert_eq!(features.typing_efficiency, 0.0);
953 }
954
955 #[test]
956 fn test_special_key_counts() {
957 let events = vec![
958 make_special_key_event(KeyboardEventType::Enter, true, 0),
959 make_special_key_event(KeyboardEventType::Enter, false, 50),
960 make_special_key_event(KeyboardEventType::Tab, true, 100),
961 make_special_key_event(KeyboardEventType::Tab, false, 150),
962 make_special_key_event(KeyboardEventType::Escape, true, 200),
963 make_special_key_event(KeyboardEventType::Escape, false, 250),
964 make_special_key_event(KeyboardEventType::ModifierKey, true, 300),
965 make_special_key_event(KeyboardEventType::FunctionKey, true, 400),
966 make_special_key_event(KeyboardEventType::FunctionKey, false, 450),
967 ];
968
969 let features = compute_keyboard_features(&events, &[], 1.0);
970 assert_eq!(features.enter_count, 1);
971 assert_eq!(features.tab_count, 1);
972 assert_eq!(features.escape_count, 1);
973 assert_eq!(features.modifier_key_count, 1);
974 assert_eq!(features.function_key_count, 1);
975 assert_eq!(features.typing_tap_count, 0); }
977
978 #[test]
979 fn test_shortcut_metrics() {
980 use crate::collector::types::{ShortcutEvent, ShortcutType};
981
982 let shortcuts = vec![
983 ShortcutEvent {
984 timestamp: Utc::now(),
985 shortcut_type: ShortcutType::Copy,
986 },
987 ShortcutEvent {
988 timestamp: Utc::now() + Duration::milliseconds(500),
989 shortcut_type: ShortcutType::Paste,
990 },
991 ];
992
993 let features = compute_keyboard_features(&[], &shortcuts, 10.0);
994 assert_eq!(features.shortcut_count, 2);
995 assert!((features.shortcut_rate - 0.2).abs() < 0.01); }
997
998 #[test]
999 fn test_friction_includes_correction_rate() {
1000 let keyboard_with_corrections = KeyboardFeatures {
1002 correction_rate: 0.5,
1003 burst_index: 0.5,
1004 ..Default::default()
1005 };
1006
1007 let keyboard_without = KeyboardFeatures {
1008 correction_rate: 0.0,
1009 burst_index: 0.5,
1010 ..Default::default()
1011 };
1012
1013 let mouse = MouseFeatures::default();
1014
1015 let signals_with = compute_behavioral_signals(&keyboard_with_corrections, &mouse);
1016 let signals_without = compute_behavioral_signals(&keyboard_without, &mouse);
1017
1018 assert!(signals_with.friction > signals_without.friction);
1019 }
1020}