summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-03 15:26:11 -0600
committerCraig Jennings <c@cjennings.net>2025-11-03 15:26:11 -0600
commit0a69c5854378afcafc567d965f206cf6a0a984be (patch)
tree923425a2aaf106c51cf5a3100cdf42f98e534da8 /tests
parent9f8aec55033d46ca4f1cd78fbd315444a0c00bc6 (diff)
test: Add comprehensive test suite for video-audio-recording module
Added 83 test cases across 9 test files with 100% pass rate, covering device detection, parsing, grouping, and complete workflow integration. ## What Was Done ### Refactoring for Testability - Extracted `cj/recording--parse-pactl-output` from `cj/recording-parse-sources` - Separated parsing logic from shell command execution - Enables testing with fixture data instead of live system calls ### Test Fixtures Created - `pactl-output-normal.txt` - All device types (built-in, USB, Bluetooth) - `pactl-output-empty.txt` - Empty output - `pactl-output-single.txt` - Single device - `pactl-output-monitors-only.txt` - Only monitor devices - `pactl-output-inputs-only.txt` - Only input devices - `pactl-output-malformed.txt` - Invalid/malformed output ### Unit Tests (8 files, 78 test cases) 1. **test-video-audio-recording-friendly-state.el** (10 tests) - State name conversion: SUSPENDED→Ready, RUNNING→Active 2. **test-video-audio-recording-parse-pactl-output.el** (14 tests) - Parse raw pactl output into structured data - Handle empty, malformed, and mixed valid/invalid input 3. **test-video-audio-recording-parse-sources.el** (6 tests) - Shell command wrapper testing with mocked output 4. **test-video-audio-recording-detect-mic-device.el** (13 tests) - Documents bugs: Returns ID numbers instead of device names - Doesn't filter monitors (legacy function, not actively used) 5. **test-video-audio-recording-detect-system-device.el** (13 tests) - Works correctly: Returns full device names - Tests monitor detection with various device types 6. **test-video-audio-recording-group-devices-by-hardware.el** (12 tests) - CRITICAL: Bluetooth MAC address normalization (colons vs underscores) - Device pairing logic (mic + monitor from same hardware) - Friendly name assignment - Filters incomplete devices 7. **test-video-audio-recording-check-ffmpeg.el** (3 tests) - ffmpeg availability detection 8. **test-video-audio-recording-get-devices.el** (7 tests) - Auto-detection fallback logic - Error handling for incomplete detection ### Integration Tests (1 file, 5 test cases) 9. **test-integration-recording-device-workflow.el** (5 tests) - Complete workflow: parse → group → friendly names - Bluetooth MAC normalization end-to-end - Incomplete device filtering across components - Malformed data graceful handling ## Key Testing Insights ### Bugs Documented - `cj/recording-detect-mic-device` has bugs (returns IDs, doesn't filter monitors) - These functions appear to be legacy code not used by main workflow - Tests document current behavior to catch regressions if fixed ### Critical Features Validated - **Bluetooth MAC normalization**: Input uses colons (00:1B:66:C0:91:6D), output uses underscores (00_1B_66_C0_91_6D), grouping normalizes correctly - **Device pairing**: Only devices with BOTH mic and monitor are included - **Friendly names**: USB/PCI/Bluetooth patterns correctly identified ### Test Coverage - Normal cases: Valid inputs, typical workflows - Boundary cases: Empty, single device, incomplete pairs - Error cases: Malformed input, missing devices, partial detection ## Test Execution All tests pass: 9/9 files, 83/83 test cases (100% pass rate) ```bash make test-file FILE=test-video-audio-recording-*.el # All pass individually # Integration test also passes make test-file FILE=test-integration-recording-device-workflow.el ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/fixtures/pactl-output-empty.txt0
-rw-r--r--tests/fixtures/pactl-output-inputs-only.txt3
-rw-r--r--tests/fixtures/pactl-output-malformed.txt4
-rw-r--r--tests/fixtures/pactl-output-monitors-only.txt3
-rw-r--r--tests/fixtures/pactl-output-normal.txt6
-rw-r--r--tests/fixtures/pactl-output-single.txt1
-rw-r--r--tests/test-integration-recording-device-workflow.el232
-rw-r--r--tests/test-video-audio-recording-check-ffmpeg.el46
-rw-r--r--tests/test-video-audio-recording-detect-mic-device.el152
-rw-r--r--tests/test-video-audio-recording-detect-system-device.el151
-rw-r--r--tests/test-video-audio-recording-friendly-state.el65
-rw-r--r--tests/test-video-audio-recording-get-devices.el142
-rw-r--r--tests/test-video-audio-recording-group-devices-by-hardware.el194
-rw-r--r--tests/test-video-audio-recording-parse-pactl-output.el157
-rw-r--r--tests/test-video-audio-recording-parse-sources.el98
15 files changed, 1254 insertions, 0 deletions
diff --git a/tests/fixtures/pactl-output-empty.txt b/tests/fixtures/pactl-output-empty.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/fixtures/pactl-output-empty.txt
diff --git a/tests/fixtures/pactl-output-inputs-only.txt b/tests/fixtures/pactl-output-inputs-only.txt
new file mode 100644
index 00000000..1840b37c
--- /dev/null
+++ b/tests/fixtures/pactl-output-inputs-only.txt
@@ -0,0 +1,3 @@
+50 alsa_input.pci-0000_00_1f.3.analog-stereo PipeWire s32le 2ch 48000Hz SUSPENDED
+79 bluez_input.00:1B:66:C0:91:6D PipeWire float32le 1ch 48000Hz SUSPENDED
+100 alsa_input.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.mono-fallback PipeWire s16le 1ch 16000Hz SUSPENDED
diff --git a/tests/fixtures/pactl-output-malformed.txt b/tests/fixtures/pactl-output-malformed.txt
new file mode 100644
index 00000000..a37b8dd6
--- /dev/null
+++ b/tests/fixtures/pactl-output-malformed.txt
@@ -0,0 +1,4 @@
+This is not valid pactl output
+Some random text
+50 incomplete-line-missing-fields
+Another bad line with only two tabs
diff --git a/tests/fixtures/pactl-output-monitors-only.txt b/tests/fixtures/pactl-output-monitors-only.txt
new file mode 100644
index 00000000..be29ebe8
--- /dev/null
+++ b/tests/fixtures/pactl-output-monitors-only.txt
@@ -0,0 +1,3 @@
+49 alsa_output.pci-0000_00_1f.3.analog-stereo.monitor PipeWire s32le 2ch 48000Hz SUSPENDED
+81 bluez_output.00_1B_66_C0_91_6D.1.monitor PipeWire s24le 2ch 48000Hz RUNNING
+99 alsa_output.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.analog-stereo.monitor PipeWire s16le 2ch 48000Hz SUSPENDED
diff --git a/tests/fixtures/pactl-output-normal.txt b/tests/fixtures/pactl-output-normal.txt
new file mode 100644
index 00000000..6d8d955b
--- /dev/null
+++ b/tests/fixtures/pactl-output-normal.txt
@@ -0,0 +1,6 @@
+49 alsa_output.pci-0000_00_1f.3.analog-stereo.monitor PipeWire s32le 2ch 48000Hz SUSPENDED
+50 alsa_input.pci-0000_00_1f.3.analog-stereo PipeWire s32le 2ch 48000Hz SUSPENDED
+79 bluez_input.00:1B:66:C0:91:6D PipeWire float32le 1ch 48000Hz SUSPENDED
+81 bluez_output.00_1B_66_C0_91_6D.1.monitor PipeWire s24le 2ch 48000Hz SUSPENDED
+99 alsa_output.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.analog-stereo.monitor PipeWire s16le 2ch 48000Hz SUSPENDED
+100 alsa_input.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.mono-fallback PipeWire s16le 1ch 16000Hz SUSPENDED
diff --git a/tests/fixtures/pactl-output-single.txt b/tests/fixtures/pactl-output-single.txt
new file mode 100644
index 00000000..d1d1c254
--- /dev/null
+++ b/tests/fixtures/pactl-output-single.txt
@@ -0,0 +1 @@
+50 alsa_input.pci-0000_00_1f.3.analog-stereo PipeWire s32le 2ch 48000Hz SUSPENDED
diff --git a/tests/test-integration-recording-device-workflow.el b/tests/test-integration-recording-device-workflow.el
new file mode 100644
index 00000000..ba92d700
--- /dev/null
+++ b/tests/test-integration-recording-device-workflow.el
@@ -0,0 +1,232 @@
+;;; test-integration-recording-device-workflow.el --- Integration tests for recording device workflow -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Integration tests covering the complete device detection and grouping workflow.
+;;
+;; This tests the full pipeline from raw pactl output through parsing, grouping,
+;; and friendly name assignment. The workflow enables users to select audio devices
+;; for recording calls/meetings.
+;;
+;; Components integrated:
+;; - cj/recording--parse-pactl-output (parse raw pactl output into structured data)
+;; - cj/recording-parse-sources (shell command wrapper)
+;; - cj/recording-group-devices-by-hardware (group inputs/monitors by device)
+;; - cj/recording-friendly-state (convert technical state names)
+;; - Bluetooth MAC address normalization (colons → underscores)
+;; - Device name pattern matching (USB, PCI, Bluetooth)
+;; - Friendly name assignment (user-facing device names)
+;;
+;; Critical integration points:
+;; - Parse output must produce data that group-devices can process
+;; - Bluetooth MAC normalization must work across parse→group boundary
+;; - Incomplete devices (only mic OR only monitor) must be filtered
+;; - Friendly names must correctly identify device types
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Test Fixtures Helper
+
+(defun test-load-fixture (filename)
+ "Load fixture file FILENAME from tests/fixtures directory."
+ (let ((fixture-path (expand-file-name
+ (concat "tests/fixtures/" filename)
+ user-emacs-directory)))
+ (with-temp-buffer
+ (insert-file-contents fixture-path)
+ (buffer-string))))
+
+;;; Normal Cases - Complete Workflow
+
+(ert-deftest test-integration-recording-device-workflow-parse-to-group-all-devices ()
+ "Test complete workflow from pactl output to grouped devices.
+
+When pactl output contains all three device types (built-in, USB, Bluetooth),
+the workflow should parse, group, and assign friendly names to all devices.
+
+Components integrated:
+- cj/recording--parse-pactl-output (parsing)
+- cj/recording-group-devices-by-hardware (grouping + MAC normalization)
+- Device pattern matching (USB/PCI/Bluetooth detection)
+- Friendly name assignment
+
+Validates:
+- All three device types are detected
+- Bluetooth MAC addresses normalized (colons → underscores)
+- Each device has both mic and monitor
+- Friendly names correctly assigned
+- Complete data flow: raw output → parsed list → grouped pairs"
+ (let ((output (test-load-fixture "pactl-output-normal.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ ;; Test parse step
+ (let ((parsed (cj/recording-parse-sources)))
+ (should (= 6 (length parsed)))
+
+ ;; Test group step (receives parsed data)
+ (let ((grouped (cj/recording-group-devices-by-hardware)))
+ (should (= 3 (length grouped)))
+
+ ;; Validate built-in device
+ (let ((built-in (assoc "Built-in Laptop Audio" grouped)))
+ (should built-in)
+ (should (string-prefix-p "alsa_input.pci" (cadr built-in)))
+ (should (string-prefix-p "alsa_output.pci" (cddr built-in))))
+
+ ;; Validate USB device
+ (let ((usb (assoc "Jabra SPEAK 510 USB" grouped)))
+ (should usb)
+ (should (string-match-p "Jabra" (cadr usb)))
+ (should (string-match-p "Jabra" (cddr usb))))
+
+ ;; Validate Bluetooth device (CRITICAL: MAC normalization)
+ (let ((bluetooth (assoc "Bluetooth Headset" grouped)))
+ (should bluetooth)
+ ;; Input has colons
+ (should (string-match-p "00:1B:66:C0:91:6D" (cadr bluetooth)))
+ ;; Output has underscores
+ (should (string-match-p "00_1B_66_C0_91_6D" (cddr bluetooth)))
+ ;; But they're grouped together!
+ (should (equal "bluez_input.00:1B:66:C0:91:6D" (cadr bluetooth)))
+ (should (equal "bluez_output.00_1B_66_C0_91_6D.1.monitor" (cddr bluetooth)))))))))
+
+(ert-deftest test-integration-recording-device-workflow-friendly-states-in-list ()
+ "Test that friendly state names appear in device list output.
+
+When listing devices, technical state names (SUSPENDED, RUNNING) should be
+converted to friendly names (Ready, Active) for better UX.
+
+Components integrated:
+- cj/recording-parse-sources (parsing with state)
+- cj/recording-friendly-state (state name conversion)
+
+Validates:
+- SUSPENDED → Ready
+- RUNNING → Active
+- State conversion works across the parse workflow"
+ (let ((output (concat
+ "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "81\tbluez_output.00_1B_66_C0_91_6D.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((parsed (cj/recording-parse-sources)))
+ ;; Verify states are parsed correctly
+ (should (equal "SUSPENDED" (nth 2 (nth 0 parsed))))
+ (should (equal "RUNNING" (nth 2 (nth 1 parsed))))
+
+ ;; Verify friendly conversion works
+ (should (equal "Ready" (cj/recording-friendly-state (nth 2 (nth 0 parsed)))))
+ (should (equal "Active" (cj/recording-friendly-state (nth 2 (nth 1 parsed)))))))))
+
+;;; Boundary Cases - Incomplete Devices
+
+(ert-deftest test-integration-recording-device-workflow-incomplete-devices-filtered ()
+ "Test that devices with only mic OR only monitor are filtered out.
+
+For call recording, we need BOTH mic and monitor from the same device.
+Incomplete devices should not appear in the grouped output.
+
+Components integrated:
+- cj/recording-parse-sources (parsing all devices)
+- cj/recording-group-devices-by-hardware (filtering incomplete pairs)
+
+Validates:
+- Device with only mic is filtered
+- Device with only monitor is filtered
+- Only complete devices (both mic and monitor) are returned
+- Filtering happens at group stage, not parse stage"
+ (let ((output (concat
+ ;; Complete device
+ "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ ;; Incomplete: USB mic with no monitor
+ "100\talsa_input.usb-device.mono-fallback\tPipeWire\ts16le 1ch 16000Hz\tSUSPENDED\n"
+ ;; Incomplete: Bluetooth monitor with no mic
+ "81\tbluez_output.AA_BB_CC_DD_EE_FF.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ ;; Parse sees all 4 devices
+ (let ((parsed (cj/recording-parse-sources)))
+ (should (= 4 (length parsed)))
+
+ ;; Group returns only 1 complete device
+ (let ((grouped (cj/recording-group-devices-by-hardware)))
+ (should (= 1 (length grouped)))
+ (should (equal "Built-in Laptop Audio" (caar grouped))))))))
+
+;;; Edge Cases - Bluetooth MAC Normalization
+
+(ert-deftest test-integration-recording-device-workflow-bluetooth-mac-variations ()
+ "Test Bluetooth MAC normalization with different formats.
+
+Bluetooth devices use colons in input names but underscores in output names.
+The grouping must normalize these to match devices correctly.
+
+Components integrated:
+- cj/recording-parse-sources (preserves original MAC format)
+- cj/recording-group-devices-by-hardware (normalizes MAC for matching)
+- Base name extraction (regex patterns)
+- MAC address transformation (underscores → colons)
+
+Validates:
+- Input with colons (bluez_input.AA:BB:CC:DD:EE:FF) parsed correctly
+- Output with underscores (bluez_output.AA_BB_CC_DD_EE_FF) parsed correctly
+- Normalization happens during grouping
+- Devices paired despite format difference
+- Original device names preserved (not mutated)"
+ (let ((output (concat
+ "79\tbluez_input.11:22:33:44:55:66\tPipeWire\tfloat32le 1ch 48000Hz\tSUSPENDED\n"
+ "81\tbluez_output.11_22_33_44_55_66.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((parsed (cj/recording-parse-sources)))
+ ;; Original formats preserved in parse
+ (should (string-match-p "11:22:33" (caar parsed)))
+ (should (string-match-p "11_22_33" (caadr parsed)))
+
+ ;; But grouping matches them
+ (let ((grouped (cj/recording-group-devices-by-hardware)))
+ (should (= 1 (length grouped)))
+ (should (equal "Bluetooth Headset" (caar grouped)))
+ ;; Original names preserved
+ (should (equal "bluez_input.11:22:33:44:55:66" (cadar grouped)))
+ (should (equal "bluez_output.11_22_33_44_55_66.1.monitor" (cddar grouped))))))))
+
+;;; Error Cases - Malformed Data
+
+(ert-deftest test-integration-recording-device-workflow-malformed-output-handled ()
+ "Test that malformed pactl output is handled gracefully.
+
+When pactl output is malformed or unparseable, the workflow should not crash.
+It should return empty results at appropriate stages.
+
+Components integrated:
+- cj/recording--parse-pactl-output (malformed line handling)
+- cj/recording-group-devices-by-hardware (empty input handling)
+
+Validates:
+- Malformed lines are silently skipped during parse
+- Empty parse results don't crash grouping
+- Workflow degrades gracefully
+- No exceptions thrown"
+ (let ((output (test-load-fixture "pactl-output-malformed.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((parsed (cj/recording-parse-sources)))
+ ;; Malformed output produces empty parse
+ (should (null parsed))
+
+ ;; Empty parse produces empty grouping (no crash)
+ (let ((grouped (cj/recording-group-devices-by-hardware)))
+ (should (null grouped)))))))
+
+(provide 'test-integration-recording-device-workflow)
+;;; test-integration-recording-device-workflow.el ends here
diff --git a/tests/test-video-audio-recording-check-ffmpeg.el b/tests/test-video-audio-recording-check-ffmpeg.el
new file mode 100644
index 00000000..5c264b64
--- /dev/null
+++ b/tests/test-video-audio-recording-check-ffmpeg.el
@@ -0,0 +1,46 @@
+;;; test-video-audio-recording-check-ffmpeg.el --- Tests for cj/recording-check-ffmpeg -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording-check-ffmpeg function.
+;; Tests detection of ffmpeg availability.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-check-ffmpeg-normal-ffmpeg-found-returns-t ()
+ "Test that function returns t when ffmpeg is found."
+ (cl-letf (((symbol-function 'executable-find)
+ (lambda (cmd)
+ (when (equal cmd "ffmpeg") "/usr/bin/ffmpeg"))))
+ (let ((result (cj/recording-check-ffmpeg)))
+ (should (eq t result)))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-check-ffmpeg-error-ffmpeg-not-found-signals-error ()
+ "Test that function signals user-error when ffmpeg is not found."
+ (cl-letf (((symbol-function 'executable-find)
+ (lambda (_cmd) nil)))
+ (should-error (cj/recording-check-ffmpeg) :type 'user-error)))
+
+(ert-deftest test-video-audio-recording-check-ffmpeg-error-message-mentions-pacman ()
+ "Test that error message includes installation command."
+ (cl-letf (((symbol-function 'executable-find)
+ (lambda (_cmd) nil)))
+ (condition-case err
+ (cj/recording-check-ffmpeg)
+ (user-error
+ (should (string-match-p "pacman -S ffmpeg" (error-message-string err)))))))
+
+(provide 'test-video-audio-recording-check-ffmpeg)
+;;; test-video-audio-recording-check-ffmpeg.el ends here
diff --git a/tests/test-video-audio-recording-detect-mic-device.el b/tests/test-video-audio-recording-detect-mic-device.el
new file mode 100644
index 00000000..e95889e3
--- /dev/null
+++ b/tests/test-video-audio-recording-detect-mic-device.el
@@ -0,0 +1,152 @@
+;;; test-video-audio-recording-detect-mic-device.el --- Tests for cj/recording-detect-mic-device -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording-detect-mic-device function.
+;; Tests auto-detection of microphone input device from pactl output.
+;; Mocks shell-command-to-string to test regex matching logic.
+;;
+;; IMPORTANT: These tests document actual behavior, which appears to have a bug.
+;; The function currently returns the pactl ID number (e.g., "50") instead of
+;; the device name (e.g., "alsa_input.pci-0000_00_1f.3.analog-stereo").
+;; This is because the regex captures group 1 is \\([^\t\n]+\\) which stops
+;; at the first tab, capturing only the ID.
+;;
+;; This function may not be actively used (parse-sources is preferred).
+;; Tests document current behavior to catch regressions if function is fixed.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-detect-mic-device-normal-built-in-analog-stereo-found ()
+ "Test detection of built-in analog stereo microphone.
+Note: Returns first match which is the monitor (ID 49), not the input."
+ (let ((output "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (stringp result))
+ ;; BUG: Returns first match "49" (monitor), not input "50"
+ (should (equal "49" result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-normal-usb-analog-stereo-found ()
+ "Test detection of USB analog stereo microphone.
+Note: Returns ID '100', not device name."
+ (let ((output "100\talsa_input.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.analog-stereo\tPipeWire\ts16le 2ch 16000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (stringp result))
+ ;; Current behavior: returns ID "100"
+ (should (equal "100" result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-normal-first-match-returned ()
+ "Test that first matching device is returned when multiple exist.
+Note: Returns first ID, not device name."
+ (let ((output (concat "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "100\talsa_input.usb-device.analog-stereo\tPipeWire\ts16le 2ch 16000Hz\tSUSPENDED\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ ;; Current behavior: returns first ID "50"
+ (should (equal "50" result))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording-detect-mic-device-boundary-empty-output-returns-nil ()
+ "Test that empty output returns nil."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "")))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (null result)))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-boundary-only-monitors-returns-nil ()
+ "Test that output with only monitor devices still matches (documents bug).
+Current regex doesn't exclude monitors, so this returns ID '49'."
+ (let ((output "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ ;; BUG: Should return nil for monitors, but regex doesn't exclude them
+ (should (equal "49" result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-boundary-mono-fallback-no-match ()
+ "Test that mono-fallback device doesn't match (not stereo)."
+ (let ((output "100\talsa_input.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.mono-fallback\tPipeWire\ts16le 1ch 16000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-boundary-bluetooth-no-match ()
+ "Test that Bluetooth devices without 'analog stereo' don't match."
+ (let ((output "79\tbluez_input.00:1B:66:C0:91:6D\tPipeWire\tfloat32le 1ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-boundary-whitespace-only-returns-nil ()
+ "Test that whitespace-only output returns nil."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) " \n\t\n ")))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (null result)))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-boundary-case-insensitive-analog ()
+ "Test that 'ANALOG' (uppercase) matches (case-insensitive regex).
+Documents that regex is actually case-insensitive."
+ (let ((output "50\talsa_input.pci-0000_00_1f.3.ANALOG-STEREO\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ ;; Regex is case-insensitive, matches uppercase
+ (should (equal "50" result))))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-detect-mic-device-error-malformed-output-returns-nil ()
+ "Test that malformed output returns nil."
+ (let ((output "This is not valid pactl output\nRandom text here\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-error-partial-match-analog-only ()
+ "Test that 'analog' without 'stereo' doesn't match."
+ (let ((output "50\talsa_input.pci-0000_00_1f.3.analog-mono\tPipeWire\ts32le 1ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-error-partial-match-stereo-only ()
+ "Test that 'stereo' without 'analog' doesn't match."
+ (let ((output "50\talsa_input.pci-0000_00_1f.3.digital-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-detect-mic-device-error-monitor-with-analog-stereo-matches-bug ()
+ "Test that monitor device with 'analog stereo' incorrectly matches (documents bug).
+Should return nil for monitors, but current regex doesn't filter them."
+ (let ((output "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-mic-device)))
+ ;; BUG: Returns ID "49" even though this is a monitor (output device)
+ (should (equal "49" result))))))
+
+(provide 'test-video-audio-recording-detect-mic-device)
+;;; test-video-audio-recording-detect-mic-device.el ends here
diff --git a/tests/test-video-audio-recording-detect-system-device.el b/tests/test-video-audio-recording-detect-system-device.el
new file mode 100644
index 00000000..bea20e8a
--- /dev/null
+++ b/tests/test-video-audio-recording-detect-system-device.el
@@ -0,0 +1,151 @@
+;;; test-video-audio-recording-detect-system-device.el --- Tests for cj/recording-detect-system-device -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording-detect-system-device function.
+;; Tests auto-detection of system audio monitor device from pactl output.
+;; Mocks shell-command-to-string to test regex matching logic.
+;;
+;; NOTE: This function works correctly - returns the full device name ending in .monitor.
+;; The regex \\([^\t\n]+\\.monitor\\) matches any non-tab/newline chars ending with .monitor,
+;; which correctly captures the device name field from pactl output.
+;;
+;; This function may not be actively used (parse-sources is preferred).
+;; Tests document current behavior to catch regressions.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-detect-system-device-normal-built-in-monitor-found ()
+ "Test detection of built-in system audio monitor.
+Returns full device name."
+ (let ((output "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (stringp result))
+ (should (equal "alsa_output.pci-0000_00_1f.3.analog-stereo.monitor" result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-normal-usb-monitor-found ()
+ "Test detection of USB system audio monitor."
+ (let ((output "99\talsa_output.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.analog-stereo.monitor\tPipeWire\ts16le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (stringp result))
+ (should (equal "alsa_output.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.analog-stereo.monitor" result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-normal-bluetooth-monitor-found ()
+ "Test detection of Bluetooth monitor device."
+ (let ((output "81\tbluez_output.00_1B_66_C0_91_6D.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (stringp result))
+ (should (equal "bluez_output.00_1B_66_C0_91_6D.1.monitor" result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-normal-first-match-returned ()
+ "Test that first matching monitor is returned when multiple exist."
+ (let ((output (concat "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "81\tbluez_output.00_1B_66_C0_91_6D.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n"
+ "99\talsa_output.usb-device.monitor\tPipeWire\ts16le 2ch 48000Hz\tSUSPENDED\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ ;; Returns first monitor device name
+ (should (equal "alsa_output.pci-0000_00_1f.3.analog-stereo.monitor" result))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording-detect-system-device-boundary-empty-output-returns-nil ()
+ "Test that empty output returns nil."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "")))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (null result)))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-boundary-only-inputs-returns-nil ()
+ "Test that output with only input devices (no monitors) returns nil."
+ (let ((output (concat "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "79\tbluez_input.00:1B:66:C0:91:6D\tPipeWire\tfloat32le 1ch 48000Hz\tSUSPENDED\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-boundary-whitespace-only-returns-nil ()
+ "Test that whitespace-only output returns nil."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) " \n\t\n ")))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (null result)))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-boundary-monitor-different-states ()
+ "Test that monitors in different states are all matched."
+ (let ((output "81\tbluez_output.00_1B_66_C0_91_6D.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ ;; Should match regardless of state (RUNNING, SUSPENDED, IDLE)
+ (should (equal "bluez_output.00_1B_66_C0_91_6D.1.monitor" result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-boundary-case-insensitive-monitor ()
+ "Test that regex is case-insensitive for '.monitor' suffix.
+Documents that .MONITOR (uppercase) also matches."
+ (let ((output "49\talsa_output.pci-0000_00_1f.3.analog-stereo.MONITOR\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ ;; Case-insensitive: .MONITOR matches
+ (should (equal "alsa_output.pci-0000_00_1f.3.analog-stereo.MONITOR" result))))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-detect-system-device-error-malformed-output-returns-nil ()
+ "Test that malformed output returns nil."
+ (let ((output "This is not valid pactl output\nRandom text here\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-error-partial-monitor-matches ()
+ "Test that device with .monitor in middle partially matches (documents quirk).
+The regex matches up to first .monitor occurrence, even if not at end of device name."
+ (let ((output "50\talsa_input.monitor-device.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ ;; QUIRK: Matches partial string "alsa_input.monitor"
+ (should (equal "alsa_input.monitor" result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-error-incomplete-line ()
+ "Test that incomplete lines with .monitor are still matched."
+ (let ((output "49\tincomplete-line.monitor\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ ;; Should match device name ending in .monitor
+ (should (equal "incomplete-line.monitor" result))))))
+
+(ert-deftest test-video-audio-recording-detect-system-device-error-mixed-valid-invalid ()
+ "Test that mix of valid and invalid lines returns first valid monitor."
+ (let ((output (concat "invalid line without tabs\n"
+ "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "another invalid line\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-detect-system-device)))
+ (should (equal "alsa_output.pci-0000_00_1f.3.analog-stereo.monitor" result))))))
+
+(provide 'test-video-audio-recording-detect-system-device)
+;;; test-video-audio-recording-detect-system-device.el ends here
diff --git a/tests/test-video-audio-recording-friendly-state.el b/tests/test-video-audio-recording-friendly-state.el
new file mode 100644
index 00000000..91b47998
--- /dev/null
+++ b/tests/test-video-audio-recording-friendly-state.el
@@ -0,0 +1,65 @@
+;;; test-video-audio-recording-friendly-state.el --- Tests for cj/recording-friendly-state -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording-friendly-state function.
+;; Tests conversion of technical pactl state names to user-friendly labels.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-friendly-state-normal-suspended-returns-ready ()
+ "Test that SUSPENDED state converts to Ready."
+ (should (string= "Ready" (cj/recording-friendly-state "SUSPENDED"))))
+
+(ert-deftest test-video-audio-recording-friendly-state-normal-running-returns-active ()
+ "Test that RUNNING state converts to Active."
+ (should (string= "Active" (cj/recording-friendly-state "RUNNING"))))
+
+(ert-deftest test-video-audio-recording-friendly-state-normal-idle-returns-ready ()
+ "Test that IDLE state converts to Ready."
+ (should (string= "Ready" (cj/recording-friendly-state "IDLE"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording-friendly-state-boundary-empty-string-returns-empty ()
+ "Test that empty string passes through unchanged."
+ (should (string= "" (cj/recording-friendly-state ""))))
+
+(ert-deftest test-video-audio-recording-friendly-state-boundary-lowercase-suspended-returns-unchanged ()
+ "Test that lowercase 'suspended' is not converted (case-sensitive)."
+ (should (string= "suspended" (cj/recording-friendly-state "suspended"))))
+
+(ert-deftest test-video-audio-recording-friendly-state-boundary-mixed-case-returns-unchanged ()
+ "Test that mixed case 'Running' passes through unchanged."
+ (should (string= "Running" (cj/recording-friendly-state "Running"))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-friendly-state-error-unknown-state-returns-unchanged ()
+ "Test that unknown state passes through unchanged."
+ (should (string= "UNKNOWN" (cj/recording-friendly-state "UNKNOWN"))))
+
+(ert-deftest test-video-audio-recording-friendly-state-error-random-string-returns-unchanged ()
+ "Test that random string passes through unchanged."
+ (should (string= "foobar" (cj/recording-friendly-state "foobar"))))
+
+(ert-deftest test-video-audio-recording-friendly-state-error-numeric-string-returns-unchanged ()
+ "Test that numeric string passes through unchanged."
+ (should (string= "12345" (cj/recording-friendly-state "12345"))))
+
+(ert-deftest test-video-audio-recording-friendly-state-error-special-chars-returns-unchanged ()
+ "Test that string with special characters passes through unchanged."
+ (should (string= "!@#$%" (cj/recording-friendly-state "!@#$%"))))
+
+(provide 'test-video-audio-recording-friendly-state)
+;;; test-video-audio-recording-friendly-state.el ends here
diff --git a/tests/test-video-audio-recording-get-devices.el b/tests/test-video-audio-recording-get-devices.el
new file mode 100644
index 00000000..b1b8470b
--- /dev/null
+++ b/tests/test-video-audio-recording-get-devices.el
@@ -0,0 +1,142 @@
+;;; test-video-audio-recording-get-devices.el --- Tests for cj/recording-get-devices -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording-get-devices function.
+;; Tests device auto-detection fallback logic.
+;;
+;; Note: This function has interactive prompts, but we test the core logic paths
+;; without mocking y-or-n-p. We focus on testing:
+;; - Already-set devices (no auto-detection needed)
+;; - Successful auto-detection
+;; - Failed auto-detection → error
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Setup and Teardown
+
+(defun test-get-devices-setup ()
+ "Reset device variables before each test."
+ (setq cj/recording-mic-device nil)
+ (setq cj/recording-system-device nil))
+
+(defun test-get-devices-teardown ()
+ "Clean up device variables after each test."
+ (setq cj/recording-mic-device nil)
+ (setq cj/recording-system-device nil))
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-get-devices-normal-already-set-returns-devices ()
+ "Test that already-set devices are returned without auto-detection."
+ (test-get-devices-setup)
+ (unwind-protect
+ (progn
+ (setq cj/recording-mic-device "test-mic")
+ (setq cj/recording-system-device "test-monitor")
+ (let ((result (cj/recording-get-devices)))
+ (should (consp result))
+ (should (equal "test-mic" (car result)))
+ (should (equal "test-monitor" (cdr result)))))
+ (test-get-devices-teardown)))
+
+(ert-deftest test-video-audio-recording-get-devices-normal-auto-detect-success ()
+ "Test that auto-detection succeeds and returns devices."
+ (test-get-devices-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/recording-detect-mic-device)
+ (lambda () "auto-detected-mic"))
+ ((symbol-function 'cj/recording-detect-system-device)
+ (lambda () "auto-detected-monitor")))
+ (let ((result (cj/recording-get-devices)))
+ (should (consp result))
+ (should (equal "auto-detected-mic" (car result)))
+ (should (equal "auto-detected-monitor" (cdr result)))
+ ;; Verify variables were set
+ (should (equal "auto-detected-mic" cj/recording-mic-device))
+ (should (equal "auto-detected-monitor" cj/recording-system-device))))
+ (test-get-devices-teardown)))
+
+(ert-deftest test-video-audio-recording-get-devices-normal-partial-auto-detect ()
+ "Test when only one device is already set, only the other is auto-detected."
+ (test-get-devices-setup)
+ (unwind-protect
+ (progn
+ (setq cj/recording-mic-device "preset-mic")
+ (cl-letf (((symbol-function 'cj/recording-detect-system-device)
+ (lambda () "auto-detected-monitor")))
+ (let ((result (cj/recording-get-devices)))
+ (should (consp result))
+ (should (equal "preset-mic" (car result)))
+ (should (equal "auto-detected-monitor" (cdr result))))))
+ (test-get-devices-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-get-devices-error-auto-detect-fails-signals-error ()
+ "Test that failed auto-detection signals user-error.
+When auto-detection fails and user doesn't manually select, function errors."
+ (test-get-devices-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/recording-detect-mic-device)
+ (lambda () nil))
+ ((symbol-function 'cj/recording-detect-system-device)
+ (lambda () nil))
+ ;; Mock y-or-n-p to say no to manual selection
+ ((symbol-function 'y-or-n-p)
+ (lambda (_prompt) nil)))
+ (should-error (cj/recording-get-devices) :type 'user-error))
+ (test-get-devices-teardown)))
+
+(ert-deftest test-video-audio-recording-get-devices-error-only-mic-detected-signals-error ()
+ "Test that detecting only mic (no monitor) signals error."
+ (test-get-devices-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/recording-detect-mic-device)
+ (lambda () "detected-mic"))
+ ((symbol-function 'cj/recording-detect-system-device)
+ (lambda () nil))
+ ((symbol-function 'y-or-n-p)
+ (lambda (_prompt) nil)))
+ (should-error (cj/recording-get-devices) :type 'user-error))
+ (test-get-devices-teardown)))
+
+(ert-deftest test-video-audio-recording-get-devices-error-only-monitor-detected-signals-error ()
+ "Test that detecting only monitor (no mic) signals error."
+ (test-get-devices-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/recording-detect-mic-device)
+ (lambda () nil))
+ ((symbol-function 'cj/recording-detect-system-device)
+ (lambda () "detected-monitor"))
+ ((symbol-function 'y-or-n-p)
+ (lambda (_prompt) nil)))
+ (should-error (cj/recording-get-devices) :type 'user-error))
+ (test-get-devices-teardown)))
+
+(ert-deftest test-video-audio-recording-get-devices-error-message-mentions-select-devices ()
+ "Test that error message guides user to manual selection command."
+ (test-get-devices-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/recording-detect-mic-device)
+ (lambda () nil))
+ ((symbol-function 'cj/recording-detect-system-device)
+ (lambda () nil))
+ ((symbol-function 'y-or-n-p)
+ (lambda (_prompt) nil)))
+ (condition-case err
+ (cj/recording-get-devices)
+ (user-error
+ (should (string-match-p "cj/recording-select-devices" (error-message-string err))))))
+ (test-get-devices-teardown)))
+
+(provide 'test-video-audio-recording-get-devices)
+;;; test-video-audio-recording-get-devices.el ends here
diff --git a/tests/test-video-audio-recording-group-devices-by-hardware.el b/tests/test-video-audio-recording-group-devices-by-hardware.el
new file mode 100644
index 00000000..0abe5f6c
--- /dev/null
+++ b/tests/test-video-audio-recording-group-devices-by-hardware.el
@@ -0,0 +1,194 @@
+;;; test-video-audio-recording-group-devices-by-hardware.el --- Tests for cj/recording-group-devices-by-hardware -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording-group-devices-by-hardware function.
+;; Tests grouping of audio sources by physical hardware device.
+;; Critical test: Bluetooth MAC address normalization (colons vs underscores).
+;;
+;; This function is used by the quick setup command to automatically pair
+;; microphone and monitor devices from the same hardware.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Test Fixtures Helper
+
+(defun test-load-fixture (filename)
+ "Load fixture file FILENAME from tests/fixtures directory."
+ (let ((fixture-path (expand-file-name
+ (concat "tests/fixtures/" filename)
+ user-emacs-directory)))
+ (with-temp-buffer
+ (insert-file-contents fixture-path)
+ (buffer-string))))
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-normal-all-types-grouped ()
+ "Test grouping of all three device types (built-in, USB, Bluetooth).
+This is the key test validating the complete grouping logic."
+ (let ((output (test-load-fixture "pactl-output-normal.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (listp result))
+ (should (= 3 (length result)))
+ ;; Check that we have all three device types
+ (let ((names (mapcar #'car result)))
+ (should (member "Built-in Laptop Audio" names))
+ (should (member "Bluetooth Headset" names))
+ (should (member "Jabra SPEAK 510 USB" names)))
+ ;; Verify each device has both mic and monitor
+ (dolist (device result)
+ (should (stringp (car device))) ; friendly name
+ (should (stringp (cadr device))) ; mic device
+ (should (stringp (cddr device))) ; monitor device
+ (should-not (string-suffix-p ".monitor" (cadr device))) ; mic not monitor
+ (should (string-suffix-p ".monitor" (cddr device)))))))) ; monitor has suffix
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-normal-built-in-paired ()
+ "Test that built-in laptop audio devices are correctly paired."
+ (let ((output (test-load-fixture "pactl-output-normal.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let* ((result (cj/recording-group-devices-by-hardware))
+ (built-in (assoc "Built-in Laptop Audio" result)))
+ (should built-in)
+ (should (string-match-p "pci-0000_00_1f" (cadr built-in)))
+ (should (string-match-p "pci-0000_00_1f" (cddr built-in)))
+ (should (equal "alsa_input.pci-0000_00_1f.3.analog-stereo" (cadr built-in)))
+ (should (equal "alsa_output.pci-0000_00_1f.3.analog-stereo.monitor" (cddr built-in)))))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-normal-usb-paired ()
+ "Test that USB devices (Jabra) are correctly paired."
+ (let ((output (test-load-fixture "pactl-output-normal.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let* ((result (cj/recording-group-devices-by-hardware))
+ (jabra (assoc "Jabra SPEAK 510 USB" result)))
+ (should jabra)
+ (should (string-match-p "Jabra" (cadr jabra)))
+ (should (string-match-p "Jabra" (cddr jabra)))))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-normal-bluetooth-paired ()
+ "Test that Bluetooth devices are correctly paired.
+CRITICAL: Tests MAC address normalization (colons in input, underscores in output)."
+ (let ((output (test-load-fixture "pactl-output-normal.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let* ((result (cj/recording-group-devices-by-hardware))
+ (bluetooth (assoc "Bluetooth Headset" result)))
+ (should bluetooth)
+ ;; Input has colons: bluez_input.00:1B:66:C0:91:6D
+ (should (equal "bluez_input.00:1B:66:C0:91:6D" (cadr bluetooth)))
+ ;; Output has underscores: bluez_output.00_1B_66_C0_91_6D.1.monitor
+ ;; But they should still be grouped together (MAC address normalized)
+ (should (equal "bluez_output.00_1B_66_C0_91_6D.1.monitor" (cddr bluetooth)))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-boundary-empty-returns-empty ()
+ "Test that empty pactl output returns empty list."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "")))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (listp result))
+ (should (null result)))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-boundary-only-inputs-returns-empty ()
+ "Test that only input devices (no monitors) returns empty list.
+Devices must have BOTH mic and monitor to be included."
+ (let ((output (test-load-fixture "pactl-output-inputs-only.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (listp result))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-boundary-only-monitors-returns-empty ()
+ "Test that only monitor devices (no inputs) returns empty list."
+ (let ((output (test-load-fixture "pactl-output-monitors-only.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (listp result))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-boundary-single-complete-device ()
+ "Test that single device with both mic and monitor is returned."
+ (let ((output "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (= 1 (length result)))
+ (should (equal "Built-in Laptop Audio" (caar result)))))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-boundary-mixed-complete-incomplete ()
+ "Test that only devices with BOTH mic and monitor are included.
+Incomplete devices (only mic or only monitor) are filtered out."
+ (let ((output (concat
+ ;; Complete device (built-in)
+ "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "49\talsa_output.pci-0000_00_1f.3.analog-stereo.monitor\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ ;; Incomplete: USB mic with no monitor
+ "100\talsa_input.usb-device.mono-fallback\tPipeWire\ts16le 1ch 16000Hz\tSUSPENDED\n"
+ ;; Incomplete: Bluetooth monitor with no mic
+ "81\tbluez_output.AA_BB_CC_DD_EE_FF.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ ;; Only the complete built-in device should be returned
+ (should (= 1 (length result)))
+ (should (equal "Built-in Laptop Audio" (caar result)))))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-error-malformed-output-returns-empty ()
+ "Test that malformed pactl output returns empty list."
+ (let ((output (test-load-fixture "pactl-output-malformed.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (listp result))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-error-unknown-device-type ()
+ "Test that unknown device types get generic 'USB Audio Device' name."
+ (let ((output (concat
+ "100\talsa_input.usb-unknown_device-00.analog-stereo\tPipeWire\ts16le 2ch 16000Hz\tSUSPENDED\n"
+ "99\talsa_output.usb-unknown_device-00.analog-stereo.monitor\tPipeWire\ts16le 2ch 48000Hz\tSUSPENDED\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (= 1 (length result)))
+ ;; Should get generic USB name (not matching Jabra pattern)
+ (should (equal "USB Audio Device" (caar result)))))))
+
+(ert-deftest test-video-audio-recording-group-devices-by-hardware-error-bluetooth-mac-case-variations ()
+ "Test that Bluetooth MAC addresses work with different formatting.
+Tests the normalization logic handles various MAC address formats."
+ (let ((output (concat
+ ;; Input with colons (typical)
+ "79\tbluez_input.AA:BB:CC:DD:EE:FF\tPipeWire\tfloat32le 1ch 48000Hz\tSUSPENDED\n"
+ ;; Output with underscores (typical)
+ "81\tbluez_output.AA_BB_CC_DD_EE_FF.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording-group-devices-by-hardware)))
+ (should (= 1 (length result)))
+ (should (equal "Bluetooth Headset" (caar result)))
+ ;; Verify both devices paired despite different MAC formats
+ (let ((device (car result)))
+ (should (string-match-p "AA:BB:CC" (cadr device)))
+ (should (string-match-p "AA_BB_CC" (cddr device))))))))
+
+(provide 'test-video-audio-recording-group-devices-by-hardware)
+;;; test-video-audio-recording-group-devices-by-hardware.el ends here
diff --git a/tests/test-video-audio-recording-parse-pactl-output.el b/tests/test-video-audio-recording-parse-pactl-output.el
new file mode 100644
index 00000000..db49a897
--- /dev/null
+++ b/tests/test-video-audio-recording-parse-pactl-output.el
@@ -0,0 +1,157 @@
+;;; test-video-audio-recording-parse-pactl-output.el --- Tests for cj/recording--parse-pactl-output -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording--parse-pactl-output function.
+;; Tests parsing of pactl sources output into structured data.
+;; Uses fixture files with sample pactl output for reproducible testing.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Test Fixtures Helper
+
+(defun test-load-fixture (filename)
+ "Load fixture file FILENAME from tests/fixtures directory."
+ (let ((fixture-path (expand-file-name
+ (concat "tests/fixtures/" filename)
+ user-emacs-directory)))
+ (with-temp-buffer
+ (insert-file-contents fixture-path)
+ (buffer-string))))
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-normal-all-devices-returns-list ()
+ "Test parsing normal pactl output with all device types."
+ (let* ((output (test-load-fixture "pactl-output-normal.txt"))
+ (result (cj/recording--parse-pactl-output output)))
+ (should (listp result))
+ (should (= 6 (length result)))
+ ;; Check first device (built-in monitor)
+ (should (equal '("alsa_output.pci-0000_00_1f.3.analog-stereo.monitor"
+ "PipeWire"
+ "SUSPENDED")
+ (nth 0 result)))
+ ;; Check Bluetooth input
+ (should (equal '("bluez_input.00:1B:66:C0:91:6D"
+ "PipeWire"
+ "SUSPENDED")
+ (nth 2 result)))
+ ;; Check USB device
+ (should (equal '("alsa_input.usb-0b0e_Jabra_SPEAK_510_USB_1C48F9C067D5020A00-00.mono-fallback"
+ "PipeWire"
+ "SUSPENDED")
+ (nth 5 result)))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-normal-single-device-returns-list ()
+ "Test parsing output with single device."
+ (let* ((output (test-load-fixture "pactl-output-single.txt"))
+ (result (cj/recording--parse-pactl-output output)))
+ (should (listp result))
+ (should (= 1 (length result)))
+ (should (equal '("alsa_input.pci-0000_00_1f.3.analog-stereo"
+ "PipeWire"
+ "SUSPENDED")
+ (car result)))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-normal-monitors-only-returns-list ()
+ "Test parsing output with only monitor devices."
+ (let* ((output (test-load-fixture "pactl-output-monitors-only.txt"))
+ (result (cj/recording--parse-pactl-output output)))
+ (should (listp result))
+ (should (= 3 (length result)))
+ ;; All should end with .monitor
+ (dolist (device result)
+ (should (string-suffix-p ".monitor" (car device))))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-normal-inputs-only-returns-list ()
+ "Test parsing output with only input devices."
+ (let* ((output (test-load-fixture "pactl-output-inputs-only.txt"))
+ (result (cj/recording--parse-pactl-output output)))
+ (should (listp result))
+ (should (= 3 (length result)))
+ ;; None should end with .monitor
+ (dolist (device result)
+ (should-not (string-suffix-p ".monitor" (car device))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-boundary-empty-string-returns-empty-list ()
+ "Test parsing empty string returns empty list."
+ (let ((result (cj/recording--parse-pactl-output "")))
+ (should (listp result))
+ (should (null result))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-boundary-empty-file-returns-empty-list ()
+ "Test parsing empty file returns empty list."
+ (let* ((output (test-load-fixture "pactl-output-empty.txt"))
+ (result (cj/recording--parse-pactl-output output)))
+ (should (listp result))
+ (should (null result))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-boundary-whitespace-only-returns-empty-list ()
+ "Test parsing whitespace-only string returns empty list."
+ (let ((result (cj/recording--parse-pactl-output " \n\t\n ")))
+ (should (listp result))
+ (should (null result))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-boundary-single-newline-returns-empty-list ()
+ "Test parsing single newline returns empty list."
+ (let ((result (cj/recording--parse-pactl-output "\n")))
+ (should (listp result))
+ (should (null result))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-boundary-device-with-running-state-parsed ()
+ "Test that RUNNING state (not just SUSPENDED) is parsed correctly."
+ (let* ((output "81\tbluez_output.00_1B_66_C0_91_6D.1.monitor\tPipeWire\ts24le 2ch 48000Hz\tRUNNING\n")
+ (result (cj/recording--parse-pactl-output output)))
+ (should (= 1 (length result)))
+ (should (equal "RUNNING" (nth 2 (car result))))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-boundary-device-with-idle-state-parsed ()
+ "Test that IDLE state is parsed correctly."
+ (let* ((output "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tIDLE\n")
+ (result (cj/recording--parse-pactl-output output)))
+ (should (= 1 (length result)))
+ (should (equal "IDLE" (nth 2 (car result))))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-error-malformed-lines-ignored ()
+ "Test that malformed lines are silently ignored."
+ (let* ((output (test-load-fixture "pactl-output-malformed.txt"))
+ (result (cj/recording--parse-pactl-output output)))
+ (should (listp result))
+ (should (null result)))) ; All lines malformed, so empty list
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-error-mixed-valid-invalid-returns-valid ()
+ "Test that mix of valid and invalid lines returns only valid ones."
+ (let* ((output (concat "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "This is invalid\n"
+ "79\tbluez_input.00:1B:66:C0:91:6D\tPipeWire\tfloat32le 1ch 48000Hz\tSUSPENDED\n"
+ "Also invalid\n"))
+ (result (cj/recording--parse-pactl-output output)))
+ (should (= 2 (length result)))
+ (should (equal "alsa_input.pci-0000_00_1f.3.analog-stereo" (car (nth 0 result))))
+ (should (equal "bluez_input.00:1B:66:C0:91:6D" (car (nth 1 result))))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-error-missing-fields-ignored ()
+ "Test that lines with missing fields are ignored."
+ (let* ((output "50\tincomplete-line\tPipeWire\n") ; Missing state and format
+ (result (cj/recording--parse-pactl-output output)))
+ (should (null result))))
+
+(ert-deftest test-video-audio-recording-parse-pactl-output-error-nil-input-returns-error ()
+ "Test that nil input signals an error."
+ (should-error (cj/recording--parse-pactl-output nil)))
+
+(provide 'test-video-audio-recording-parse-pactl-output)
+;;; test-video-audio-recording-parse-pactl-output.el ends here
diff --git a/tests/test-video-audio-recording-parse-sources.el b/tests/test-video-audio-recording-parse-sources.el
new file mode 100644
index 00000000..d6d445b5
--- /dev/null
+++ b/tests/test-video-audio-recording-parse-sources.el
@@ -0,0 +1,98 @@
+;;; test-video-audio-recording-parse-sources.el --- Tests for cj/recording-parse-sources -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording-parse-sources function.
+;; Tests the wrapper that calls pactl and delegates to internal parser.
+;; Mocks shell-command-to-string to avoid system dependencies.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'video-audio-recording)
+
+;;; Test Fixtures Helper
+
+(defun test-load-fixture (filename)
+ "Load fixture file FILENAME from tests/fixtures directory."
+ (let ((fixture-path (expand-file-name
+ (concat "tests/fixtures/" filename)
+ user-emacs-directory)))
+ (with-temp-buffer
+ (insert-file-contents fixture-path)
+ (buffer-string))))
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording-parse-sources-normal-calls-pactl-and-parses ()
+ "Test that parse-sources calls shell command and returns parsed list."
+ (let ((fixture-output (test-load-fixture "pactl-output-normal.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) fixture-output)))
+ (let ((result (cj/recording-parse-sources)))
+ (should (listp result))
+ (should (= 6 (length result)))
+ ;; Verify it returns structured data
+ (should (equal "alsa_output.pci-0000_00_1f.3.analog-stereo.monitor"
+ (car (nth 0 result))))
+ (should (equal "PipeWire" (nth 1 (nth 0 result))))
+ (should (equal "SUSPENDED" (nth 2 (nth 0 result))))))))
+
+(ert-deftest test-video-audio-recording-parse-sources-normal-single-device-returns-list ()
+ "Test parse-sources with single device."
+ (let ((fixture-output (test-load-fixture "pactl-output-single.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) fixture-output)))
+ (let ((result (cj/recording-parse-sources)))
+ (should (listp result))
+ (should (= 1 (length result)))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording-parse-sources-boundary-empty-output-returns-empty-list ()
+ "Test that empty pactl output returns empty list."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "")))
+ (let ((result (cj/recording-parse-sources)))
+ (should (listp result))
+ (should (null result)))))
+
+(ert-deftest test-video-audio-recording-parse-sources-boundary-whitespace-output-returns-empty-list ()
+ "Test that whitespace-only output returns empty list."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) " \n\t\n ")))
+ (let ((result (cj/recording-parse-sources)))
+ (should (listp result))
+ (should (null result)))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording-parse-sources-error-malformed-output-returns-empty-list ()
+ "Test that malformed output is handled gracefully."
+ (let ((fixture-output (test-load-fixture "pactl-output-malformed.txt")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) fixture-output)))
+ (let ((result (cj/recording-parse-sources)))
+ (should (listp result))
+ (should (null result))))))
+
+(ert-deftest test-video-audio-recording-parse-sources-error-mixed-valid-invalid-returns-valid-only ()
+ "Test that mix of valid and invalid lines returns only valid entries."
+ (let ((mixed-output (concat
+ "50\talsa_input.pci-0000_00_1f.3.analog-stereo\tPipeWire\ts32le 2ch 48000Hz\tSUSPENDED\n"
+ "invalid line\n"
+ "79\tbluez_input.00:1B:66:C0:91:6D\tPipeWire\tfloat32le 1ch 48000Hz\tRUNNING\n")))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) mixed-output)))
+ (let ((result (cj/recording-parse-sources)))
+ (should (= 2 (length result)))
+ (should (equal "alsa_input.pci-0000_00_1f.3.analog-stereo" (car (nth 0 result))))
+ (should (equal "bluez_input.00:1B:66:C0:91:6D" (car (nth 1 result))))))))
+
+(provide 'test-video-audio-recording-parse-sources)
+;;; test-video-audio-recording-parse-sources.el ends here