//! Extra integration coverage for client keyboard aggregator helpers. //! //! Scope: include keyboard input source and cover reset-state, clipboard //! fallback, send-toggle, and auxiliary paste branches without blowing the //! primary keyboard contract past the 500 LOC cap. //! Targets: `client/src/input/keyboard.rs`. //! Why: keyboard helper branches are numerous enough to merit a paired //! contract file for hygiene-gate modularity. mod keymap { pub use lesavka_client::input::keymap::*; } #[allow(warnings)] mod keyboard_contract_extra { include!(env!("LESAVKA_CLIENT_KEYBOARD_SRC")); use serial_test::serial; use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use temp_env::{with_var, with_vars}; use tempfile::tempdir; fn write_executable(dir: &Path, name: &str, body: &str) { let path = dir.join(name); fs::write(&path, body).expect("write script"); let mut perms = fs::metadata(&path).expect("metadata").permissions(); perms.set_mode(0o755); fs::set_permissions(path, perms).expect("chmod"); } fn with_fake_path_command(name: &str, script_body: &str, f: impl FnOnce()) { let dir = tempdir().expect("tempdir"); write_executable(dir.path(), name, script_body); let prior = std::env::var("PATH").unwrap_or_default(); let merged = if prior.is_empty() { dir.path().display().to_string() } else { format!("{}:{prior}", dir.path().display()) }; with_var("PATH", Some(merged), f); } fn open_any_keyboard_device() -> Option { let entries = std::fs::read_dir("/dev/input").ok()?; for entry in entries.flatten() { let path = entry.path(); let name = path.file_name()?.to_string_lossy(); if !name.starts_with("event") { continue; } let dev = evdev::Device::open(path).ok()?; let _ = dev.set_nonblocking(true); let looks_like_keyboard = dev .supported_keys() .map(|keys| { keys.contains(evdev::KeyCode::KEY_A) && keys.contains(evdev::KeyCode::KEY_ENTER) && keys.contains(evdev::KeyCode::KEY_LEFTCTRL) }) .unwrap_or(false); if looks_like_keyboard { return Some(dev); } } None } fn build_keyboard(name: &str) -> Option { let mut keys = evdev::AttributeSet::::new(); for key in [ evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_B, evdev::KeyCode::KEY_V, evdev::KeyCode::KEY_LEFTCTRL, evdev::KeyCode::KEY_LEFTALT, ] { keys.insert(key); } let mut vdev = evdev::uinput::VirtualDevice::builder() .ok()? .name(name) .with_keys(&keys) .ok()? .build() .ok()?; for _ in 0..40 { if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() { if let Some(Ok(path)) = nodes.next() { let dev = evdev::Device::open(path).ok()?; dev.set_nonblocking(true).ok()?; return Some(dev); } } std::thread::sleep(std::time::Duration::from_millis(10)); } None } fn new_aggregator( dev: evdev::Device, ) -> ( KeyboardAggregator, tokio::sync::broadcast::Receiver, ) { let (tx, rx) = tokio::sync::broadcast::channel(64); (KeyboardAggregator::new(dev, true, tx, None), rx) } #[test] #[serial] fn reset_state_when_idle_still_emits_an_empty_report() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-reset-idle")) else { return; }; let (mut agg, mut rx) = new_aggregator(dev); agg.reset_state(); let pkt = rx .try_recv() .expect("idle reset should still publish empty report"); assert_eq!(pkt.data, vec![0; 8]); } #[test] #[serial] fn pressed_keys_snapshot_returns_the_current_keyset() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-snapshot")) else { return; }; let (mut agg, _) = new_aggregator(dev); agg.pressed_keys.insert(evdev::KeyCode::KEY_A); agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL); let snapshot = agg.pressed_keys_snapshot(); assert!(snapshot.contains(&evdev::KeyCode::KEY_A)); assert!(snapshot.contains(&evdev::KeyCode::KEY_LEFTCTRL)); assert_eq!(snapshot.len(), 2); } #[test] fn update_pressed_keys_tracks_press_and_release_values() { let mut pressed = HashSet::new(); update_pressed_keys(&mut pressed, evdev::KeyCode::KEY_A, 1); assert!(pressed.contains(&evdev::KeyCode::KEY_A)); update_pressed_keys(&mut pressed, evdev::KeyCode::KEY_A, 0); assert!(!pressed.contains(&evdev::KeyCode::KEY_A)); update_pressed_keys(&mut pressed, evdev::KeyCode::KEY_B, 2); assert!(pressed.contains(&evdev::KeyCode::KEY_B)); } #[test] fn debounce_gate_honors_zero_and_nonzero_windows() { let last = AtomicU64::new(123); assert!(debounce_gate(&last, 500, 0)); assert_eq!(last.load(Ordering::Relaxed), 500); last.store(9_900, Ordering::Relaxed); assert!(!debounce_gate(&last, 10_000, 500)); assert_eq!(last.load(Ordering::Relaxed), 10_000); assert!(debounce_gate(&last, 10_700, 500)); assert_eq!(last.load(Ordering::Relaxed), 10_700); } #[test] #[serial] fn paste_rpc_enabled_from_env_honors_flag_and_key_variants() { let home = tempdir().expect("tempdir"); temp_env::with_vars( [ ("HOME", home.path().to_str()), ("LESAVKA_PASTE_KEY_FILE", None::<&str>), ("LESAVKA_PASTE_RPC", Some("0")), ("LESAVKA_PASTE_KEY", Some("shared-key")), ], || { assert!(!paste_rpc_enabled_from_env()); }, ); temp_env::with_vars( [ ("HOME", home.path().to_str()), ("LESAVKA_PASTE_KEY_FILE", None::<&str>), ("LESAVKA_PASTE_RPC", Some("1")), ("LESAVKA_PASTE_KEY", Some(" ")), ], || { assert!(!paste_rpc_enabled_from_env()); }, ); temp_env::with_vars( [ ("HOME", home.path().to_str()), ("LESAVKA_PASTE_KEY_FILE", None::<&str>), ("LESAVKA_PASTE_RPC", Some("1")), ("LESAVKA_PASTE_KEY", Some("shared-key")), ], || { assert!(paste_rpc_enabled_from_env()); }, ); } #[test] #[serial] fn set_send_false_blocks_manual_empty_report() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-nosend")) else { return; }; let (mut agg, mut rx) = new_aggregator(dev); agg.set_send(false); agg.send_empty_report(); assert!(rx.try_recv().is_err()); } #[test] #[serial] fn paste_chord_active_supports_ctrl_v_variant() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-ctrl-v")) else { return; }; let (mut agg, _) = new_aggregator(dev); agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL); agg.pressed_keys.insert(evdev::KeyCode::KEY_V); with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+v"), || { assert!(agg.paste_chord_active()); }); } #[test] #[serial] fn ctrl_v_reaches_remote_when_lesavka_paste_chord_requires_alt() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-ctrl-v-app")) else { return; }; let (mut agg, _) = new_aggregator(dev); agg.paste_enabled = true; agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL); agg.pressed_keys.insert(evdev::KeyCode::KEY_V); with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || { assert!( !agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1), "plain Ctrl+V should relay as an app shortcut, not trigger Lesavka paste" ); }); assert!(!agg.paste_chord_armed); assert!(!agg.paste_chord_consumed); } #[test] #[serial] fn paste_via_rpc_returns_false_without_sender() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-rpc-none")) else { return; }; let (tx, _rx) = tokio::sync::broadcast::channel(8); let agg = KeyboardAggregator::new(dev, false, tx, None); assert!(!agg.paste_via_rpc()); } #[test] #[serial] fn paste_via_rpc_returns_true_for_empty_clipboard_payload() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-rpc-empty")) else { return; }; let (paste_tx, mut paste_rx) = tokio::sync::mpsc::unbounded_channel::(); let (kbd_tx, _rx) = tokio::sync::broadcast::channel(8); let agg = KeyboardAggregator::new(dev, false, kbd_tx, Some(paste_tx)); let wl_paste_empty = "#!/usr/bin/env sh\nexit 0\n"; with_var("LESAVKA_CLIPBOARD_CMD", None::<&str>, || { with_fake_path_command("wl-paste", wl_paste_empty, || { assert!( agg.paste_via_rpc(), "empty clipboard should still consume the chord" ); assert!( paste_rx.try_recv().is_err(), "empty clipboard should not enqueue payload" ); }); }); } #[test] #[cfg(coverage)] #[serial] fn paste_clipboard_skips_unsupported_chars_in_coverage() { let Some(dev) = open_any_keyboard_device() .or_else(|| build_keyboard("lesavka-include-kbd-coverage-unsupported")) else { return; }; let (mut agg, mut rx) = new_aggregator(dev); agg.paste_enabled = true; with_var("LESAVKA_CLIPBOARD_CMD", Some("printf '🚀'"), || { agg.paste_clipboard(); }); assert!( rx.try_recv().is_err(), "unsupported clipboard characters should not emit HID reports" ); } #[test] #[serial] fn set_grab_path_is_non_panicking() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-grab")) else { return; }; let (mut agg, _) = new_aggregator(dev); agg.set_grab(false); agg.set_grab(true); } #[test] #[serial] fn try_handle_paste_event_swallows_incomplete_chord_sequences() { let Some(dev) = open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-incomplete")) else { return; }; let (mut agg, mut rx) = new_aggregator(dev); agg.paste_enabled = true; agg.paste_chord_armed = true; agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL); assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_LEFTCTRL, 1)); let pkt = rx.try_recv().expect("swallow report"); assert_eq!(pkt.data, vec![0; 8]); } #[test] #[cfg(coverage)] #[serial] fn try_handle_paste_event_coverage_path_runs_hid_paste_and_empty_report() { let Some(dev) = open_any_keyboard_device() .or_else(|| build_keyboard("lesavka-include-kbd-coverage-paste")) else { return; }; let (tx, mut rx) = tokio::sync::broadcast::channel(32); let mut agg = KeyboardAggregator::new(dev, false, tx, None); agg.paste_enabled = true; agg.paste_rpc_enabled = false; agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL); agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT); agg.pressed_keys.insert(evdev::KeyCode::KEY_V); with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'a'"), || { with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || { with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("0"), || { assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1)); }); }); }); let mut saw_hid_payload = false; let mut saw_empty = false; while let Ok(pkt) = rx.try_recv() { if pkt.data == vec![0; 8] { saw_empty = true; } if pkt.data.len() == 8 && pkt.data.iter().any(|byte| *byte != 0) { saw_hid_payload = true; } } assert!( saw_hid_payload, "coverage paste path should emit HID reports" ); assert!( saw_empty, "coverage paste path should end with an empty report" ); } #[test] #[cfg(coverage)] #[serial] fn try_handle_paste_event_coverage_path_respects_debounce_fallthrough() { let Some(dev) = open_any_keyboard_device() .or_else(|| build_keyboard("lesavka-include-kbd-coverage-debounce")) else { return; }; let (tx, mut rx) = tokio::sync::broadcast::channel(32); let mut agg = KeyboardAggregator::new(dev, false, tx, None); agg.paste_enabled = true; agg.paste_rpc_enabled = false; agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL); agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT); agg.pressed_keys.insert(evdev::KeyCode::KEY_V); let now_ms = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; LAST_PASTE_MS.store(now_ms, Ordering::Relaxed); with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'a'"), || { with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || { with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("999999"), || { assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1)); }); }); }); let pkt = rx .try_recv() .expect("debounced paste should still emit a swallowed empty report"); assert_eq!(pkt.data, vec![0; 8]); assert!( rx.try_recv().is_err(), "debounced paste should not emit HID reports" ); LAST_PASTE_MS.store(0, Ordering::Relaxed); } #[test] #[cfg(coverage)] #[serial] fn try_handle_paste_event_coverage_path_invokes_rpc_when_enabled() { let Some(dev) = open_any_keyboard_device() .or_else(|| build_keyboard("lesavka-include-kbd-coverage-rpc")) else { return; }; let (paste_tx, mut paste_rx) = tokio::sync::mpsc::unbounded_channel::(); let (tx, _rx) = tokio::sync::broadcast::channel(32); let mut agg = KeyboardAggregator::new(dev, false, tx, Some(paste_tx)); agg.paste_enabled = true; agg.paste_rpc_enabled = true; agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL); agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT); agg.pressed_keys.insert(evdev::KeyCode::KEY_V); with_var( "LESAVKA_CLIPBOARD_CMD", Some("printf 'rpc-coverage'"), || { with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || { with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("0"), || { assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1)); }); }); }, ); let payload = paste_rx.try_recv().expect("rpc payload"); assert_eq!(payload, "rpc-coverage"); } }