summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-14 21:14:50 -0600
committerCraig Jennings <c@cjennings.net>2025-11-14 21:14:50 -0600
commit8126e7334bd86d06775c7cf72042b840b314beb2 (patch)
treefd01a109632a694725dcfc8aca6e04eeade1d804
parent324497c3cdd9d4655e68e17c2a5b4120bbb0f33d (diff)
test: Add integration test for recording monitor capture
-rw-r--r--tests/test-integration-recording-monitor-capture.el222
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