Skip to content

Conversation

@google-labs-jules
Copy link
Contributor

This change adds comprehensive unit tests for the enigo and kdotool injectors, improving code coverage and reliability. The enigo_injector was refactored to allow for dependency injection, and the kdotool_injector tests use a mock script to ensure isolation.

Fixes #266


PR created automatically by Jules for task 15939947936558664243 started by @Coldaine

Adds comprehensive unit tests for the `enigo_injector` and `kdotool_injector` modules.

For the `enigo_injector`, the code was refactored to extract the core keyboard manipulation logic into synchronous, generic functions. This allows for the use of a mock `Enigo` object, enabling isolated unit tests without side effects.

For the `kdotool_injector`, the tests mock the `kdotool` command-line tool by creating a temporary script and manipulating the `PATH` environment variable. This ensures that the tests are reliable and do not depend on the actual `kdotool` binary being installed.
@google-labs-jules
Copy link
Contributor Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!


For security, I will only act on instructions from the user who triggered this task.

New to Jules? Learn more at jules.google/docs.

@qodo-code-review
Copy link

qodo-code-review bot commented Dec 3, 2025

CI Feedback 🧐

(Feedback updated until commit b4dd47e)

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Lint & Format

Failed stage: Check formatting [❌]

Failure summary:

The action failed because cargo fmt --all -- --check detected formatting inconsistencies in the
code. The command found multiple files with formatting differences that don't match the project's
rustfmt configuration. The specific issues include:

- Incorrect indentation and line breaks in multiple files
- Files with formatting issues:
-
crates/coldvox-stt/src/plugins/parakeet.rs (lines 287, 361)
- crates/coldvox-stt/src/processor.rs
(lines 174, 220)
- crates/coldvox-telemetry/src/stt_metrics.rs (lines 99, 227, 250, 309, 347)
-
crates/coldvox-text-injection/src/enigo_injector.rs (lines 35, 60, 87, 96, 109, 226, 249)

The command exited with code 1, indicating that the code needs to be reformatted using cargo fmt
before the PR can pass this check.

Relevant error logs:
1:  Runner name: 'laptop-extra'
2:  Runner group name: 'Default'
...

136:  CARGO_TERM_COLOR: always
137:  targets: 
138:  components: rustfmt, clippy
139:  ##[endgroup]
140:  ##[group]Run : set $CARGO_HOME
141:  �[36;1m: set $CARGO_HOME�[0m
142:  �[36;1mecho CARGO_HOME=${CARGO_HOME:-"$HOME/.cargo"} >> $GITHUB_ENV�[0m
143:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
144:  env:
145:  RUSTFLAGS: -D warnings
146:  CARGO_TERM_COLOR: always
147:  ##[endgroup]
148:  ##[group]Run : install rustup if needed
149:  �[36;1m: install rustup if needed�[0m
150:  �[36;1mif ! command -v rustup &>/dev/null; then�[0m
151:  �[36;1m  curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail https://sh.rustup.rs | sh -s -- --default-toolchain none -y�[0m
152:  �[36;1m  echo "$CARGO_HOME/bin" >> $GITHUB_PATH�[0m
...

223:  �[36;1m  if rustc +stable --version --verbose | grep -q '^release: 1\.6[89]\.'; then�[0m
224:  �[36;1m    touch "/home/coldaine/actions-runner/_work/_temp"/.implicit_cargo_registries_crates_io_protocol || true�[0m
225:  �[36;1m    echo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse >> $GITHUB_ENV�[0m
226:  �[36;1m  elif rustc +stable --version --verbose | grep -q '^release: 1\.6[67]\.'; then�[0m
227:  �[36;1m    touch "/home/coldaine/actions-runner/_work/_temp"/.implicit_cargo_registries_crates_io_protocol || true�[0m
228:  �[36;1m    echo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=git >> $GITHUB_ENV�[0m
229:  �[36;1m  fi�[0m
230:  �[36;1mfi�[0m
231:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
232:  env:
233:  RUSTFLAGS: -D warnings
234:  CARGO_TERM_COLOR: always
235:  CARGO_HOME: /home/coldaine/.cargo
236:  CARGO_INCREMENTAL: 0
237:  ##[endgroup]
238:  ##[group]Run : work around spurious network errors in curl 8.0
239:  �[36;1m: work around spurious network errors in curl 8.0�[0m
240:  �[36;1m# https://rust-lang.zulipchat.com/#narrow/stream/246057-t-cargo/topic/timeout.20investigation�[0m
...

327:  - /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-telemetry/Cargo.toml
328:  - /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/Cargo.toml
329:  - /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-vad-silero/Cargo.toml
330:  - /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-vad/Cargo.toml
331:  ##[endgroup]
332:  ... Restoring cache ...
333:  No cache found.
334:  ##[group]Run cargo fmt --all -- --check
335:  �[36;1mcargo fmt --all -- --check�[0m
336:  shell: /usr/bin/bash -e {0}
337:  env:
338:  RUSTFLAGS: -D warnings
339:  CARGO_TERM_COLOR: always
340:  CARGO_HOME: /home/coldaine/.cargo
341:  CARGO_INCREMENTAL: 0
342:  CACHE_ON_FAILURE: false
343:  ##[endgroup]
...

358:  confidence_scores: true,
359:  speaker_diarization: false, // Can be added later via pyannote
360:  -            auto_punctuation: true, // Both variants support punctuation
361:  +            auto_punctuation: true,     // Both variants support punctuation
362:  custom_vocabulary: false,
363:  }
364:  }
365:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-stt/src/plugins/parakeet.rs:287:
366:  };
367:  // Initialize the model
368:  -            let model = Parakeet::from_pretrained(self.variant.model_identifier(), Some(parakeet_config))
369:  -                .map_err(|err| {
370:  +            let model =
371:  +                Parakeet::from_pretrained(self.variant.model_identifier(), Some(parakeet_config))
372:  +                    .map_err(|err| {
373:  error!(
374:  target: "coldvox::stt::parakeet",
375:  error = %err,
376:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-stt/src/processor.rs:174:
377:  /// Handle speech end event
378:  async fn handle_speech_end(&mut self, _timestamp_ms: u64, _duration_ms: Option<u64>) {
379:  debug!(target: "stt", "Starting handle_speech_end()");
380:  -        let _guard = coldvox_telemetry::TimingGuard::new(
381:  -            &self.metrics,
382:  -            |m, d| m.record_end_to_end_latency(d)
383:  -        );
384:  +        let _guard = coldvox_telemetry::TimingGuard::new(&self.metrics, |m, d| {
385:  +            m.record_end_to_end_latency(d)
386:  +        });
387:  if let UtteranceState::SpeechActive { audio_buffer, .. } = &self.state {
388:  if !audio_buffer.is_empty() {
389:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-stt/src/processor.rs:220:
390:  TranscriptionEvent::Error { .. } => self.metrics.record_error(),
391:  }
392:  -        if tokio::time::timeout(
393:  -            std::time::Duration::from_secs(5),
394:  -            self.event_tx.send(event),
395:  -        )
396:  -        .await
397:  -        .is_err()
398:  +        if tokio::time::timeout(std::time::Duration::from_secs(5), self.event_tx.send(event))
399:  +            .await
400:  +            .is_err()
401:  {
402:  self.metrics.record_error();
403:  debug!(target: "stt", "Event channel closed or send timed out");
...

432:  +        "  Preprocessing:   {:.1}ms",
433:  +        latency.preprocessing_us as f64 / 1000.0
434:  +    );
435:  +    println!(
436:  +        "  Result Delivery: {:.1}ms",
437:  +        latency.result_delivery_us as f64 / 1000.0
438:  +    );
439:  // Accuracy metrics
440:  let avg_confidence = metrics.get_average_confidence();
441:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-telemetry/src/stt_metrics.rs:99:
442:  /// Performance alert types
443:  #[derive(Debug, Clone, PartialEq)]
444:  pub enum PerformanceAlert {
445:  -    HighLatency { measured_us: u64, threshold_us: u64 },
446:  -    LowConfidence { measured: f64, threshold: f64 },
447:  -    HighErrorRate { measured_per_1k: u64, threshold_per_1k: u64 },
448:  -    HighMemoryUsage { measured_bytes: u64, threshold_bytes: u64 },
449:  -    ProcessingStalled { last_activity: Duration },
450:  +    HighLatency {
451:  +        measured_us: u64,
452:  +        threshold_us: u64,
453:  +    },
454:  +    LowConfidence {
455:  +        measured: f64,
456:  +        threshold: f64,
457:  +    },
458:  +    HighErrorRate {
459:  +        measured_per_1k: u64,
460:  +        threshold_per_1k: u64,
461:  +    },
462:  +    HighMemoryUsage {
463:  +        measured_bytes: u64,
464:  +        threshold_bytes: u64,
465:  +    },
466:  +    ProcessingStalled {
467:  +        last_activity: Duration,
468:  +    },
469:  }
470:  impl SttPerformanceMetrics {
471:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-telemetry/src/stt_metrics.rs:227:
472:  }
473:  let total_ops = inner.operational.request_count;
474:  -        if total_ops > 1000 { // Only check if significant number of operations
475:  -             let error_rate_per_1k = (inner.operational.error_count * 1000) / total_ops;
476:  -             if error_rate_per_1k > thresholds.max_error_rate_per_1k {
477:  +        if total_ops > 1000 {
478:  +            // Only check if significant number of operations
479:  +            let error_rate_per_1k = (inner.operational.error_count * 1000) / total_ops;
480:  +            if error_rate_per_1k > thresholds.max_error_rate_per_1k {
481:  alerts.push(PerformanceAlert::HighErrorRate {
482:  measured_per_1k: error_rate_per_1k,
483:  threshold_per_1k: thresholds.max_error_rate_per_1k,
484:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-telemetry/src/stt_metrics.rs:250:
...

522:  inner.resources.clone(),
523:  -            inner.operational.clone()
524:  +            inner.operational.clone(),
525:  )
526:  }
527:  }
528:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-telemetry/src/stt_metrics.rs:309:
529:  }
530:  }
531:  -
532:  #[cfg(test)]
533:  mod tests {
534:  use super::*;
535:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-telemetry/src/stt_metrics.rs:347:
536:  metrics.record_transcription_success();
537:  metrics.record_transcription_failure();
538:  let rate = metrics.get_success_rate();
...

553:  +            measured_us: 200_000,
554:  +            threshold_us: 100_000
555:  +        }));
556:  +        assert!(alerts.contains(&PerformanceAlert::LowConfidence {
557:  +            measured: 0.5,
558:  +            threshold: 0.8
559:  +        }));
560:  }
561:  #[test]
562:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/src/enigo_injector.rs:35:
563:  // Type each character with a small delay
564:  for c in text.chars() {
565:  match c {
566:  -                ' ' => enigo
567:  -                    .key(Key::Space, Direction::Click)
568:  -                    .map_err(|e| InjectionError::MethodFailed(format!("Failed to type space: {}", e)))?,
569:  -                '\n' => enigo
570:  -                    .key(Key::Return, Direction::Click)
571:  -                    .map_err(|e| InjectionError::MethodFailed(format!("Failed to type enter: {}", e)))?,
572:  -                '\t' => enigo
573:  -                    .key(Key::Tab, Direction::Click)
574:  -                    .map_err(|e| InjectionError::MethodFailed(format!("Failed to type tab: {}", e)))?,
575:  +                ' ' => enigo.key(Key::Space, Direction::Click).map_err(|e| {
576:  +                    InjectionError::MethodFailed(format!("Failed to type space: {}", e))
577:  +                })?,
578:  +                '\n' => enigo.key(Key::Return, Direction::Click).map_err(|e| {
579:  +                    InjectionError::MethodFailed(format!("Failed to type enter: {}", e))
580:  +                })?,
581:  +                '\t' => enigo.key(Key::Tab, Direction::Click).map_err(|e| {
582:  +                    InjectionError::MethodFailed(format!("Failed to type tab: {}", e))
583:  +                })?,
584:  _ => {
585:  // Use text method for all other characters
586:  -                    enigo
587:  -                        .text(&c.to_string())
588:  -                        .map_err(|e| InjectionError::MethodFailed(format!("Failed to type text: {}", e)))?;
589:  +                    enigo.text(&c.to_string()).map_err(|e| {
590:  +                        InjectionError::MethodFailed(format!("Failed to type text: {}", e))
591:  +                    })?;
592:  }
593:  }
594:  }
595:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/src/enigo_injector.rs:60:
596:  let text_clone = text.to_string();
597:  let result = tokio::task::spawn_blocking(move || {
598:  -            let mut enigo = Enigo::new(&Settings::default())
599:  -                .map_err(|e| InjectionError::MethodFailed(format!("Failed to create Enigo: {}", e)))?;
600:  +            let mut enigo = Enigo::new(&Settings::default()).map_err(|e| {
601:  +                InjectionError::MethodFailed(format!("Failed to create Enigo: {}", e))
602:  +            })?;
603:  Self::type_text_logic(&mut enigo, &text_clone)
604:  })
605:  .await;
606:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/src/enigo_injector.rs:87:
607:  enigo
608:  .key(Key::Unicode('v'), Direction::Click)
609:  .map_err(|e| InjectionError::MethodFailed(format!("Failed to type 'v': {}", e)))?;
610:  -            enigo
611:  -                .key(Key::Meta, Direction::Release)
612:  -                .map_err(|e| InjectionError::MethodFailed(format!("Failed to release Cmd: {}", e)))?;
613:  +            enigo.key(Key::Meta, Direction::Release).map_err(|e| {
614:  +                InjectionError::MethodFailed(format!("Failed to release Cmd: {}", e))
615:  +            })?;
616:  }
617:  #[cfg(not(target_os = "macos"))]
618:  {
619:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/src/enigo_injector.rs:96:
620:  +            enigo.key(Key::Control, Direction::Press).map_err(|e| {
621:  +                InjectionError::MethodFailed(format!("Failed to press Ctrl: {}", e))
622:  +            })?;
623:  enigo
624:  -                .key(Key::Control, Direction::Press)
625:  -                .map_err(|e| InjectionError::MethodFailed(format!("Failed to press Ctrl: {}", e)))?;
626:  -            enigo
627:  .key(Key::Unicode('v'), Direction::Click)
628:  .map_err(|e| InjectionError::MethodFailed(format!("Failed to type 'v': {}", e)))?;
629:  -            enigo
630:  -                .key(Key::Control, Direction::Release)
631:  -                .map_err(|e| InjectionError::MethodFailed(format!("Failed to release Ctrl: {}", e)))?;
632:  +            enigo.key(Key::Control, Direction::Release).map_err(|e| {
633:  +                InjectionError::MethodFailed(format!("Failed to release Ctrl: {}", e))
634:  +            })?;
635:  }
636:  Ok(())
637:  }
638:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/src/enigo_injector.rs:109:
639:  /// Trigger paste action using enigo (Ctrl+V)
640:  async fn trigger_paste(&self) -> Result<(), InjectionError> {
641:  let result = tokio::task::spawn_blocking(|| {
642:  -            let mut enigo = Enigo::new(&Settings::default())
643:  -                .map_err(|e| InjectionError::MethodFailed(format!("Failed to create Enigo: {}", e)))?;
644:  +            let mut enigo = Enigo::new(&Settings::default()).map_err(|e| {
645:  +                InjectionError::MethodFailed(format!("Failed to create Enigo: {}", e))
646:  +            })?;
647:  Self::trigger_paste_logic(&mut enigo)
648:  })
649:  .await;
650:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/src/enigo_injector.rs:226:
651:  if self.should_fail() {
652:  return Err(enigo::Error::InvalidText);
653:  }
...

658:  Ok(())
659:  }
660:  }
661:  Diff in /home/coldaine/actions-runner/_work/ColdVox/ColdVox/crates/coldvox-text-injection/src/enigo_injector.rs:249:
662:  assert!(result.is_ok());
663:  assert_eq!(
664:  *mock_enigo.actions.borrow(),
665:  -            vec![
666:  -                "key(Space,Click)",
667:  -                "key(Return,Click)",
668:  -                "key(Tab,Click)"
669:  -            ]
670:  +            vec!["key(Space,Click)", "key(Return,Click)", "key(Tab,Click)"]
671:  );
672:  }
673:  ##[error]Process completed with exit code 1.
674:  Post job cleanup.

@Coldaine
Copy link
Owner

Superseded by #312 (rebased and integrated)

@Coldaine Coldaine closed this Dec 24, 2025
@google-labs-jules
Copy link
Contributor Author

Superseded by #312 (rebased and integrated)

Understood. Acknowledging that this work is now obsolete and stopping work on this task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add unit tests for enigo and kdotool injectors

2 participants