diff options
| author | Craig Jennings <c@cjennings.net> | 2025-11-14 21:14:50 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-11-14 21:14:50 -0600 |
| commit | 8126e7334bd86d06775c7cf72042b840b314beb2 (patch) | |
| tree | fd01a109632a694725dcfc8aca6e04eeade1d804 | |
| parent | 324497c3cdd9d4655e68e17c2a5b4120bbb0f33d (diff) | |
test: Add integration test for recording monitor capture
| -rw-r--r-- | tests/test-integration-recording-monitor-capture.el | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/tests/test-integration-recording-monitor-capture.el b/tests/test-integration-recording-monitor-capture.el new file mode 100644 index 00000000..7d7c5dfb --- /dev/null +++ b/tests/test-integration-recording-monitor-capture.el @@ -0,0 +1,222 @@ +;;; test-integration-recording-monitor-capture.el --- Integration test for monitor audio capture -*- lexical-binding: t; -*- + +;; Author: Craig Jennings <c@cjennings.net> +;; Created: 2025-11-14 + +;;; Commentary: +;; +;; Integration test that verifies phone call audio (speaker output) is captured +;; during recording. This tests the actual PulseAudio device selection end-to-end. +;; +;; This test: +;; 1. Plays known speech through speakers (simulating phone call audio) +;; 2. Records it using the configured monitor device +;; 3. Transcribes the recording +;; 4. Verifies the expected text appears in the transcription +;; +;; Requirements: +;; - Audio system must be working (PulseAudio/PipeWire) +;; - Recording devices must be configured (C-; r c) +;; - paplay must be available +;; - Transcription backend must be configured +;; +;; This is a MANUAL integration test - not suitable for CI since it requires +;; working audio hardware. +;; +;; USAGE: +;; M-x ert RET test-integration-recording-monitor-capture RET +;; +;; Or from command line: +;; make test-file FILE=tests/test-integration-recording-monitor-capture.el + +;;; Code: + +(require 'ert) +(require 'video-audio-recording) +(require 'transcription-config) + +(defvar test-recording-monitor--test-audio-file + (expand-file-name "tests/fixtures/audio/speaker-output-test.wav" + user-emacs-directory) + "Test audio file with known speech for speaker output testing.") + +(defvar test-recording-monitor--expected-phrases + '("quick brown fox" "lazy dog" "speaker output test") + "Phrases expected in transcription. +We use partial matches since transcription may not be 100% accurate.") + +(defun test-recording-monitor--wait-for-transcription (audio-file timeout) + "Wait for transcription of AUDIO-FILE to complete, up to TIMEOUT seconds. +Returns the path to the .txt file if successful, nil if timeout." + (let* ((txt-file (concat (file-name-sans-extension audio-file) ".txt")) + (deadline (time-add (current-time) (seconds-to-time timeout)))) + ;; Wait for .txt file to be created and have content + (while (and (time-less-p (current-time) deadline) + (or (not (file-exists-p txt-file)) + (= 0 (file-attribute-size (file-attributes txt-file))))) + (sleep-for 1) + (message "Waiting for transcription... (%d seconds remaining)" + (ceiling (float-time (time-subtract deadline (current-time)))))) + (if (and (file-exists-p txt-file) + (> (file-attribute-size (file-attributes txt-file)) 0)) + txt-file + nil))) + +(defun test-recording-monitor--cleanup (recording-file) + "Clean up RECORDING-FILE and associated transcription files." + (when (and recording-file (file-exists-p recording-file)) + (let* ((base (file-name-sans-extension recording-file)) + (txt-file (concat base ".txt")) + (log-file (concat base ".log"))) + (when (file-exists-p recording-file) + (delete-file recording-file)) + (when (file-exists-p txt-file) + (delete-file txt-file)) + (when (file-exists-p log-file) + (delete-file log-file))))) + +(ert-deftest test-integration-recording-monitor-capture () + "Integration test: verify speaker output (monitor) is captured during recording. + +This test simulates a phone call scenario: +- Test audio plays through speakers (simulates remote person speaking) +- Recording should capture this via the monitor device +- Transcription should contain the expected speech + +This verifies that the system audio monitor device selection is working correctly. + +Components integrated: +- video-audio-recording.el (device selection, ffmpeg command generation) +- PulseAudio/PipeWire (actual audio routing) +- ffmpeg (recording process) +- transcription-config.el (speech-to-text verification) + +Test category: Integration test (requires working audio hardware)" + :tags '(:integration :manual :audio) + + ;; Skip if prerequisites not met + (skip-unless (executable-find "paplay")) + (skip-unless (executable-find "ffmpeg")) + (skip-unless (file-exists-p test-recording-monitor--test-audio-file)) + (skip-unless (and cj/recording-mic-device cj/recording-system-device)) + + (let ((recording-file nil) + (playback-process nil)) + (unwind-protect + (progn + (message "\n========================================") + (message "Starting monitor capture integration test") + (message "========================================") + (message "Test audio: %s" test-recording-monitor--test-audio-file) + (message "Mic device: %s" cj/recording-mic-device) + (message "Monitor device: %s" cj/recording-system-device) + (message "Backend: %s" cj/transcribe-backend) + + ;; Step 1: Start recording + (message "\n[1/6] Starting recording...") + (let ((temp-dir (make-temp-file "recording-test-" t))) + (cj/ffmpeg-record-audio temp-dir) + ;; Give ffmpeg a moment to initialize + (sleep-for 1) + (should (process-live-p cj/audio-recording-ffmpeg-process)) + (message "✓ Recording started (process: %s)" + (process-name cj/audio-recording-ffmpeg-process)) + + ;; Determine the recording filename (ffmpeg uses timestamp) + ;; We'll find it after stopping + (setq recording-file + (expand-file-name + (concat (format-time-string "%Y-%m-%d-%H-%M-%S") ".m4a") + temp-dir))) + + ;; Step 2: Play test audio through speakers + (message "\n[2/6] Playing test audio through speakers...") + (setq playback-process + (start-process "test-audio-playback" + "*test-audio-playback*" + "paplay" + test-recording-monitor--test-audio-file)) + (message "✓ Playback started (process: %s)" + (process-name playback-process)) + + ;; Step 3: Wait for playback to complete + (message "\n[3/6] Waiting for playback to complete...") + (let ((max-wait 10) + (elapsed 0)) + (while (and (process-live-p playback-process) + (< elapsed max-wait)) + (sleep-for 0.5) + (setq elapsed (+ elapsed 0.5))) + (if (process-live-p playback-process) + (progn + (kill-process playback-process) + (error "Playback did not complete within %d seconds" max-wait)) + (message "✓ Playback completed (%.1f seconds)" elapsed))) + + ;; Give audio a moment to be captured + (sleep-for 1) + + ;; Step 4: Stop recording + (message "\n[4/6] Stopping recording...") + (cj/audio-recording-stop) + (sleep-for 1) + (should-not (process-live-p cj/audio-recording-ffmpeg-process)) + + ;; Find the actual recording file (may differ by timestamp) + (let* ((dir (file-name-directory recording-file)) + (files (directory-files dir t "\\.m4a$"))) + (should (= 1 (length files))) + (setq recording-file (car files))) + + (should (file-exists-p recording-file)) + (message "✓ Recording stopped") + (message " Recording file: %s" recording-file) + (message " File size: %d bytes" + (file-attribute-size (file-attributes recording-file))) + + ;; Step 5: Transcribe recording + (message "\n[5/6] Transcribing recording (this may take 30-60 seconds)...") + (cj/transcribe-audio recording-file) + + ;; Wait for transcription to complete + (let ((txt-file (test-recording-monitor--wait-for-transcription + recording-file 120))) + (should txt-file) + (message "✓ Transcription completed") + (message " Transcript file: %s" txt-file) + + ;; Step 6: Verify transcription contains expected phrases + (message "\n[6/6] Verifying transcription content...") + (let ((transcript (with-temp-buffer + (insert-file-contents txt-file) + (downcase (buffer-string))))) + (message "Transcript length: %d characters" (length transcript)) + (message "Transcript preview: %s" + (substring transcript 0 (min 100 (length transcript)))) + + ;; Verify at least 2 of the expected phrases appear + (let ((matches 0)) + (dolist (phrase test-recording-monitor--expected-phrases) + (when (string-match-p phrase transcript) + (setq matches (1+ matches)) + (message "✓ Found expected phrase: '%s'" phrase))) + + (message "\nMatched %d of %d expected phrases" + matches (length test-recording-monitor--expected-phrases)) + + (should (>= matches 2)) + (message "\n✓✓✓ TEST PASSED ✓✓✓") + (message "Monitor device is correctly capturing speaker audio!"))))) + + ;; Cleanup + (message "\nCleaning up...") + (when (process-live-p playback-process) + (kill-process playback-process)) + (when (and cj/audio-recording-ffmpeg-process + (process-live-p cj/audio-recording-ffmpeg-process)) + (cj/audio-recording-stop)) + (test-recording-monitor--cleanup recording-file) + (message "✓ Cleanup complete")))) + +(provide 'test-integration-recording-monitor-capture) +;;; test-integration-recording-monitor-capture.el ends here |
