summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/fixtures/grammar-correct.txt5
-rw-r--r--tests/fixtures/grammar-errors-basic.txt7
-rw-r--r--tests/fixtures/grammar-errors-punctuation.txt5
-rw-r--r--tests/test-flycheck-languagetool-setup.el71
-rw-r--r--tests/test-integration-grammar-checking.el190
-rw-r--r--tests/test-integration-transcription.el145
-rw-r--r--tests/test-transcription-audio-file.el83
-rw-r--r--tests/test-transcription-config--transcription-script-path.el106
-rw-r--r--tests/test-transcription-counter.el98
-rw-r--r--tests/test-transcription-duration.el58
-rw-r--r--tests/test-transcription-log-cleanup.el44
-rw-r--r--tests/test-transcription-paths.el80
12 files changed, 892 insertions, 0 deletions
diff --git a/tests/fixtures/grammar-correct.txt b/tests/fixtures/grammar-correct.txt
new file mode 100644
index 00000000..bea335e8
--- /dev/null
+++ b/tests/fixtures/grammar-correct.txt
@@ -0,0 +1,5 @@
+This is a well-written sentence with no grammar errors.
+
+The quick brown fox jumps over the lazy dog.
+
+Everything here follows standard English grammar rules.
diff --git a/tests/fixtures/grammar-errors-basic.txt b/tests/fixtures/grammar-errors-basic.txt
new file mode 100644
index 00000000..c2f72c12
--- /dev/null
+++ b/tests/fixtures/grammar-errors-basic.txt
@@ -0,0 +1,7 @@
+This are a test of basic grammar errors.
+
+I could of done better with this sentence.
+
+Their going to the store to buy there groceries.
+
+The dog wagged it's tail happily.
diff --git a/tests/fixtures/grammar-errors-punctuation.txt b/tests/fixtures/grammar-errors-punctuation.txt
new file mode 100644
index 00000000..37de646a
--- /dev/null
+++ b/tests/fixtures/grammar-errors-punctuation.txt
@@ -0,0 +1,5 @@
+This sentence is missing punctuation at the end
+
+Multiple spaces between words should be detected.
+
+A sentence with,incorrect comma,placement and usage.
diff --git a/tests/test-flycheck-languagetool-setup.el b/tests/test-flycheck-languagetool-setup.el
new file mode 100644
index 00000000..a719e822
--- /dev/null
+++ b/tests/test-flycheck-languagetool-setup.el
@@ -0,0 +1,71 @@
+;;; test-flycheck-languagetool-setup.el --- Unit tests for LanguageTool setup -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests verifying LanguageTool installation and wrapper script setup.
+;; Focus: Testing OUR code (wrapper script, file setup), not flycheck internals.
+;;
+;; We trust that flycheck works correctly (it's an external framework).
+;; These tests verify:
+;; - LanguageTool is installed and accessible
+;; - Our wrapper script exists, is executable, and has correct structure
+;; - Python 3 dependency is available
+;;
+;; Categories: Normal (installation checks), Boundary (script structure), Error (missing dependencies)
+
+;;; Code:
+
+(require 'ert)
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-flycheck-languagetool-setup-normal-wrapper-exists ()
+ "Test that languagetool-flycheck wrapper script exists."
+ (let ((wrapper-path (expand-file-name "~/.emacs.d/scripts/languagetool-flycheck")))
+ (should (file-exists-p wrapper-path))))
+
+(ert-deftest test-flycheck-languagetool-setup-normal-wrapper-executable ()
+ "Test that languagetool-flycheck wrapper script is executable."
+ (let ((wrapper-path (expand-file-name "~/.emacs.d/scripts/languagetool-flycheck")))
+ (should (file-executable-p wrapper-path))))
+
+(ert-deftest test-flycheck-languagetool-setup-normal-languagetool-installed ()
+ "Test that languagetool command is available in PATH."
+ (should (executable-find "languagetool")))
+
+(ert-deftest test-flycheck-languagetool-setup-normal-python3-available ()
+ "Test that python3 is available for wrapper script."
+ (should (executable-find "python3")))
+
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-flycheck-languagetool-setup-boundary-wrapper-script-format ()
+ "Test that wrapper script has correct shebang and structure."
+ (let ((wrapper-path (expand-file-name "~/.emacs.d/scripts/languagetool-flycheck")))
+ (with-temp-buffer
+ (insert-file-contents wrapper-path)
+ (goto-char (point-min))
+ ;; Check shebang
+ (should (looking-at "#!/usr/bin/env python3"))
+ ;; Check it contains required imports
+ (should (search-forward "import json" nil t))
+ (should (search-forward "import subprocess" nil t)))))
+
+;; ----------------------------- Error Cases -----------------------------------
+
+(ert-deftest test-flycheck-languagetool-setup-error-missing-file-argument ()
+ "Test that wrapper script requires file argument.
+When called without arguments, wrapper should exit with error."
+ (let* ((wrapper (expand-file-name "~/.emacs.d/scripts/languagetool-flycheck"))
+ (exit-code nil))
+ (with-temp-buffer
+ (setq exit-code (call-process wrapper nil t nil))
+ ;; Should exit with non-zero status when no file provided
+ (should-not (= 0 exit-code))
+ ;; Should print usage message to stderr (captured in buffer)
+ (goto-char (point-min))
+ (should (or (search-forward "Usage:" nil t)
+ (search-forward "FILE" nil t))))))
+
+(provide 'test-flycheck-languagetool-setup)
+;;; test-flycheck-languagetool-setup.el ends here
diff --git a/tests/test-integration-grammar-checking.el b/tests/test-integration-grammar-checking.el
new file mode 100644
index 00000000..8948c17a
--- /dev/null
+++ b/tests/test-integration-grammar-checking.el
@@ -0,0 +1,190 @@
+;;; test-integration-grammar-checking.el --- Integration tests for grammar checking -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Integration tests for the LanguageTool wrapper script with real grammar checking.
+;; Tests the integration: test fixture → wrapper script → LanguageTool → formatted output
+;;
+;; Components integrated:
+;; - scripts/languagetool-flycheck (our wrapper script)
+;; - languagetool command (external grammar checker)
+;; - Test fixtures with known grammar errors
+;; - Output formatting (JSON → flycheck format)
+;;
+;; Focus: Testing OUR integration code (wrapper), not flycheck framework.
+;; We trust that flycheck works; we test that our wrapper produces correct output.
+;;
+;; Categories: Normal workflow, Boundary cases, Error handling
+
+;;; Code:
+
+(require 'ert)
+
+;; ----------------------------- Test Helpers ----------------------------------
+
+(defun test-integration-grammar--fixture-path (filename)
+ "Return absolute path to test fixture FILENAME."
+ (expand-file-name (concat "tests/fixtures/" filename)
+ user-emacs-directory))
+
+(defun test-integration-grammar--wrapper-output (file-path)
+ "Run languagetool-flycheck wrapper directly on FILE-PATH.
+Returns output as string."
+ (let ((wrapper (expand-file-name "~/.emacs.d/scripts/languagetool-flycheck")))
+ (with-temp-buffer
+ (call-process wrapper nil t nil file-path)
+ (buffer-string))))
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-integration-grammar-checking-normal-wrapper-detects-errors ()
+ "Test that wrapper script detects grammar errors in fixture.
+
+Components integrated:
+- scripts/languagetool-flycheck (wrapper script)
+- languagetool command (external checker)
+- Test fixture with known errors"
+ (let* ((fixture (test-integration-grammar--fixture-path "grammar-errors-basic.txt"))
+ (output (test-integration-grammar--wrapper-output fixture)))
+ ;; Should detect "This are" error
+ (should (string-match-p "PLURAL_VERB_AFTER_THIS\\|This are" output))
+ ;; Should detect "could of" error
+ (should (string-match-p "COULD_OF\\|could of" output))
+ ;; Output should be in flycheck format (filename:line:column:)
+ (should (string-match-p "grammar-errors-basic\\.txt:[0-9]+:[0-9]+:" output))))
+
+(ert-deftest test-integration-grammar-checking-normal-wrapper-format ()
+ "Test that wrapper outputs flycheck-compatible format.
+
+Components integrated:
+- scripts/languagetool-flycheck (output formatting)
+- languagetool command (JSON parsing)"
+ (let* ((fixture (test-integration-grammar--fixture-path "grammar-errors-basic.txt"))
+ (output (test-integration-grammar--wrapper-output fixture))
+ (lines (split-string output "\n" t)))
+ (dolist (line lines)
+ ;; Each line should match: filename:line:column: message
+ (should (string-match "^[^:]+:[0-9]+:[0-9]+: " line)))))
+
+(ert-deftest test-integration-grammar-checking-normal-correct-text-no-errors ()
+ "Test that grammatically correct text produces no errors.
+
+Components integrated:
+- scripts/languagetool-flycheck (wrapper script)
+- languagetool command (validation)
+- Test fixture with correct grammar"
+ (let* ((fixture (test-integration-grammar--fixture-path "grammar-correct.txt"))
+ (output (test-integration-grammar--wrapper-output fixture)))
+ ;; Correct grammar should produce no output (or only whitespace)
+ (should (or (string-empty-p (string-trim output))
+ (= 0 (length (string-trim output)))))))
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-integration-grammar-checking-boundary-empty-file ()
+ "Test that empty file produces no errors.
+
+Components integrated:
+- scripts/languagetool-flycheck (empty input handling)
+- languagetool command"
+ (let ((temp-file (make-temp-file "grammar-test-" nil ".txt")))
+ (unwind-protect
+ (let ((output (test-integration-grammar--wrapper-output temp-file)))
+ (should (or (string-empty-p (string-trim output))
+ (= 0 (length (string-trim output))))))
+ (delete-file temp-file))))
+
+(ert-deftest test-integration-grammar-checking-boundary-single-word ()
+ "Test that single word file produces no errors.
+
+Components integrated:
+- scripts/languagetool-flycheck (minimal input)
+- languagetool command"
+ (let ((temp-file (make-temp-file "grammar-test-" nil ".txt")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "Hello"))
+ (let ((output (test-integration-grammar--wrapper-output temp-file)))
+ ;; Single word might produce no errors or might flag as incomplete sentence
+ ;; Just verify it doesn't crash
+ (should (stringp output))))
+ (delete-file temp-file))))
+
+(ert-deftest test-integration-grammar-checking-boundary-multiple-paragraphs ()
+ "Test that file with multiple paragraphs is checked completely.
+
+Components integrated:
+- scripts/languagetool-flycheck (multi-paragraph handling)
+- languagetool command (full file processing)"
+ (let* ((fixture (test-integration-grammar--fixture-path "grammar-errors-basic.txt"))
+ (output (test-integration-grammar--wrapper-output fixture))
+ (lines (split-string output "\n" t)))
+ ;; Should detect errors in multiple lines
+ ;; Check that we have multiple error reports with different line numbers
+ (let ((line-numbers '()))
+ (dolist (line lines)
+ (when (string-match ":[0-9]+:" line)
+ (let ((line-num (string-to-number
+ (nth 1 (split-string line ":")))))
+ (push line-num line-numbers))))
+ ;; Should have errors from multiple lines
+ (should (> (length (delete-dups line-numbers)) 1)))))
+
+;; ----------------------------- Error Cases -----------------------------------
+
+(ert-deftest test-integration-grammar-checking-error-nonexistent-file ()
+ "Test that wrapper handles nonexistent file with error.
+
+Components integrated:
+- scripts/languagetool-flycheck (error handling)
+- File system (missing file)
+- Python exception handling"
+ (let* ((nonexistent "/tmp/this-file-does-not-exist-12345.txt")
+ (wrapper (expand-file-name "~/.emacs.d/scripts/languagetool-flycheck"))
+ (exit-code nil)
+ (output nil))
+ (with-temp-buffer
+ (setq exit-code (call-process wrapper nil t nil nonexistent))
+ (setq output (buffer-string)))
+ ;; LanguageTool/Python should handle the error
+ ;; Check that we get output (error message or error in flycheck format)
+ (should (stringp output))
+ ;; Output should contain some indication of the error (filename or error marker)
+ (should (or (string-match-p nonexistent output)
+ (string-match-p "error" output)
+ (string-match-p "Error" output)
+ ;; Or it might report no errors for a nonexistent file
+ (string-empty-p (string-trim output))))))
+
+(ert-deftest test-integration-grammar-checking-error-no-file-argument ()
+ "Test that wrapper requires file argument.
+
+Components integrated:
+- scripts/languagetool-flycheck (argument validation)"
+ (let* ((wrapper (expand-file-name "~/.emacs.d/scripts/languagetool-flycheck"))
+ (exit-code nil))
+ (with-temp-buffer
+ (setq exit-code (call-process wrapper nil t nil))
+ ;; Should exit with non-zero status when no file provided
+ (should-not (= 0 exit-code)))))
+
+;; ----------------------------- Integration with Real Files -------------------
+
+(ert-deftest test-integration-grammar-checking-integration-comprehensive-errors ()
+ "Test that wrapper catches multiple types of grammar errors in one file.
+
+Components integrated:
+- scripts/languagetool-flycheck (our wrapper)
+- languagetool command (comprehensive checking)
+- Test fixture with various error types"
+ (let* ((fixture (test-integration-grammar--fixture-path "grammar-errors-basic.txt"))
+ (output (test-integration-grammar--wrapper-output fixture))
+ (lines (split-string output "\n" t)))
+ ;; Should detect multiple errors (at least 3-4 in the fixture)
+ (should (>= (length lines) 3))
+ ;; All lines should be properly formatted
+ (dolist (line lines)
+ (should (string-match "^[^:]+:[0-9]+:[0-9]+: " line)))))
+
+(provide 'test-integration-grammar-checking)
+;;; test-integration-grammar-checking.el ends here
diff --git a/tests/test-integration-transcription.el b/tests/test-integration-transcription.el
new file mode 100644
index 00000000..96b617bc
--- /dev/null
+++ b/tests/test-integration-transcription.el
@@ -0,0 +1,145 @@
+;;; test-integration-transcription.el --- Integration tests for transcription -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; End-to-end integration tests for transcription workflow
+;; Tests complete workflow with temporary files and mocked processes
+;; Categories: Normal workflow, Error handling, Cleanup
+
+;;; Code:
+
+(require 'ert)
+(require 'transcription-config)
+
+;; ----------------------------- Test Helpers ----------------------------------
+
+(defun test-transcription--make-mock-audio-file ()
+ "Create a temporary mock audio file for testing.
+Returns the absolute path to the file."
+ (let ((file (make-temp-file "test-audio-" nil ".m4a")))
+ (with-temp-file file
+ (insert "Mock audio data"))
+ file))
+
+(defun test-transcription--cleanup-output-files (audio-file)
+ "Delete transcript and log files associated with AUDIO-FILE."
+ (let* ((outputs (cj/--transcription-output-files audio-file))
+ (txt-file (car outputs))
+ (log-file (cdr outputs)))
+ (when (file-exists-p txt-file)
+ (delete-file txt-file))
+ (when (file-exists-p log-file)
+ (delete-file log-file))))
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-integration-transcription-output-files-created ()
+ "Test that .txt and .log files are created for audio file."
+ (let* ((audio-file (test-transcription--make-mock-audio-file))
+ (outputs (cj/--transcription-output-files audio-file))
+ (txt-file (car outputs))
+ (log-file (cdr outputs)))
+ (unwind-protect
+ (progn
+ ;; Verify output file paths are correct
+ (should (string-suffix-p ".txt" txt-file))
+ (should (string-suffix-p ".log" log-file))
+ (should (string= (file-name-sans-extension txt-file)
+ (file-name-sans-extension audio-file)))
+ (should (string= (file-name-sans-extension log-file)
+ (file-name-sans-extension audio-file))))
+ ;; Cleanup
+ (delete-file audio-file)
+ (test-transcription--cleanup-output-files audio-file))))
+
+(ert-deftest test-integration-transcription-validates-file-exists ()
+ "Test that transcription fails for non-existent file."
+ (should-error
+ (cj/--start-transcription-process "/nonexistent/audio.m4a")
+ :type 'user-error))
+
+(ert-deftest test-integration-transcription-validates-audio-extension ()
+ "Test that transcription fails for non-audio file."
+ (let ((non-audio (make-temp-file "test-" nil ".txt")))
+ (unwind-protect
+ (should-error
+ (cj/--start-transcription-process non-audio)
+ :type 'user-error)
+ (delete-file non-audio))))
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-integration-transcription-audio-file-detection ()
+ "Test various audio file extensions are accepted."
+ (dolist (ext '("m4a" "mp3" "wav" "flac" "ogg" "opus"))
+ (let ((audio-file (make-temp-file "test-audio-" nil (concat "." ext))))
+ (unwind-protect
+ (progn
+ (should (cj/--audio-file-p audio-file))
+ ;; Would start transcription if script existed
+ )
+ (delete-file audio-file)))))
+
+(ert-deftest test-integration-transcription-filename-with-spaces ()
+ "Test transcription with audio file containing spaces."
+ (let ((audio-file (make-temp-file "test audio file" nil ".m4a")))
+ (unwind-protect
+ (let* ((outputs (cj/--transcription-output-files audio-file))
+ (txt-file (car outputs))
+ (log-file (cdr outputs)))
+ (should (file-name-absolute-p txt-file))
+ (should (file-name-absolute-p log-file)))
+ (delete-file audio-file))))
+
+(ert-deftest test-integration-transcription-filename-with-special-chars ()
+ "Test transcription with special characters in filename."
+ (let ((audio-file (make-temp-file "test_(final)" nil ".m4a")))
+ (unwind-protect
+ (let* ((outputs (cj/--transcription-output-files audio-file))
+ (txt-file (car outputs)))
+ ;; make-temp-file adds random suffix, so just check it ends with .txt
+ ;; and contains the special chars
+ (should (string-suffix-p ".txt" txt-file))
+ (should (string-match-p "test_(final)" txt-file)))
+ (delete-file audio-file))))
+
+;; ----------------------------- Cleanup Tests ---------------------------------
+
+(ert-deftest test-integration-transcription-cleanup-completed ()
+ "Test that completed transcriptions are removed from tracking."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running)
+ (proc2 "file2.m4a" nil complete)
+ (proc3 "file3.m4a" nil error))))
+ (cj/--cleanup-completed-transcriptions)
+ (should (= 1 (length cj/transcriptions-list)))
+ (should (eq 'running (nth 3 (car cj/transcriptions-list))))))
+
+(ert-deftest test-integration-transcription-cleanup-all-complete ()
+ "Test cleanup when all transcriptions are complete."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil complete)
+ (proc2 "file2.m4a" nil error))))
+ (cj/--cleanup-completed-transcriptions)
+ (should (null cj/transcriptions-list))))
+
+(ert-deftest test-integration-transcription-cleanup-preserves-running ()
+ "Test that running transcriptions are not cleaned up."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running)
+ (proc2 "file2.m4a" nil running))))
+ (cj/--cleanup-completed-transcriptions)
+ (should (= 2 (length cj/transcriptions-list)))))
+
+;; ----------------------------- Backend Tests ---------------------------------
+
+(ert-deftest test-integration-transcription-script-path-exists ()
+ "Test that transcription scripts exist in expected location."
+ (dolist (backend '(local-whisper openai-api))
+ (let ((cj/transcribe-backend backend))
+ (let ((script (cj/--transcription-script-path)))
+ (should (file-name-absolute-p script))
+ ;; Note: Script may not exist in test environment, just check path format
+ (should (string-match-p "scripts/" script))))))
+
+(provide 'test-integration-transcription)
+;;; test-integration-transcription.el ends here
diff --git a/tests/test-transcription-audio-file.el b/tests/test-transcription-audio-file.el
new file mode 100644
index 00000000..f40d9ca6
--- /dev/null
+++ b/tests/test-transcription-audio-file.el
@@ -0,0 +1,83 @@
+;;; test-transcription-audio-file.el --- Tests for audio file detection -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--audio-file-p function
+;; Categories: Normal cases, Boundary cases, Error cases
+
+;;; Code:
+
+(require 'ert)
+(require 'transcription-config)
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-cj/--audio-file-p-m4a ()
+ "Test that .m4a files are recognized as audio."
+ (should (cj/--audio-file-p "meeting.m4a")))
+
+(ert-deftest test-cj/--audio-file-p-mp3 ()
+ "Test that .mp3 files are recognized as audio."
+ (should (cj/--audio-file-p "podcast.mp3")))
+
+(ert-deftest test-cj/--audio-file-p-wav ()
+ "Test that .wav files are recognized as audio."
+ (should (cj/--audio-file-p "recording.wav")))
+
+(ert-deftest test-cj/--audio-file-p-flac ()
+ "Test that .flac files are recognized as audio."
+ (should (cj/--audio-file-p "music.flac")))
+
+(ert-deftest test-cj/--audio-file-p-with-path ()
+ "Test audio file recognition with full path."
+ (should (cj/--audio-file-p "/home/user/recordings/meeting.m4a")))
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-cj/--audio-file-p-uppercase-extension ()
+ "Test that uppercase extensions are recognized."
+ (should (cj/--audio-file-p "MEETING.M4A")))
+
+(ert-deftest test-cj/--audio-file-p-mixed-case ()
+ "Test that mixed case extensions are recognized."
+ (should (cj/--audio-file-p "podcast.Mp3")))
+
+(ert-deftest test-cj/--audio-file-p-no-extension ()
+ "Test that files without extension are not recognized."
+ (should-not (cj/--audio-file-p "meeting")))
+
+(ert-deftest test-cj/--audio-file-p-empty-string ()
+ "Test that empty string is not recognized as audio."
+ (should-not (cj/--audio-file-p "")))
+
+(ert-deftest test-cj/--audio-file-p-dotfile ()
+ "Test that dotfiles without proper extension are not recognized."
+ (should-not (cj/--audio-file-p ".hidden")))
+
+(ert-deftest test-cj/--audio-file-p-multiple-dots ()
+ "Test file with multiple dots but audio extension."
+ (should (cj/--audio-file-p "meeting.2025-11-04.final.m4a")))
+
+;; ------------------------------ Error Cases ----------------------------------
+
+(ert-deftest test-cj/--audio-file-p-not-audio ()
+ "Test that non-audio files are not recognized."
+ (should-not (cj/--audio-file-p "document.pdf")))
+
+(ert-deftest test-cj/--audio-file-p-text-file ()
+ "Test that text files are not recognized as audio."
+ (should-not (cj/--audio-file-p "notes.txt")))
+
+(ert-deftest test-cj/--audio-file-p-org-file ()
+ "Test that org files are not recognized as audio."
+ (should-not (cj/--audio-file-p "tasks.org")))
+
+(ert-deftest test-cj/--audio-file-p-video-file ()
+ "Test that video files are not recognized as audio."
+ (should-not (cj/--audio-file-p "video.mp4")))
+
+(ert-deftest test-cj/--audio-file-p-nil ()
+ "Test that nil input returns nil."
+ (should-not (cj/--audio-file-p nil)))
+
+(provide 'test-transcription-audio-file)
+;;; test-transcription-audio-file.el ends here
diff --git a/tests/test-transcription-config--transcription-script-path.el b/tests/test-transcription-config--transcription-script-path.el
new file mode 100644
index 00000000..a56cb05c
--- /dev/null
+++ b/tests/test-transcription-config--transcription-script-path.el
@@ -0,0 +1,106 @@
+;;; test-transcription-config--transcription-script-path.el --- Tests for cj/--transcription-script-path -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--transcription-script-path function from transcription-config.el
+;;
+;; This function returns the absolute path to the transcription script based on
+;; the current value of cj/transcribe-backend.
+
+;;; Code:
+
+(require 'ert)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub notification function
+(unless (fboundp 'notifications-notify)
+ (defun notifications-notify (&rest _args)
+ "Stub notification function for testing."
+ nil))
+
+;; Now load the actual production module
+(require 'transcription-config)
+
+;;; Setup and Teardown
+
+(defun test-transcription-script-path-setup ()
+ "Set up test environment."
+ ;; Save original backend setting
+ (setq test-transcription-original-backend cj/transcribe-backend))
+
+(defun test-transcription-script-path-teardown ()
+ "Clean up test environment."
+ ;; Restore original backend setting
+ (setq cj/transcribe-backend test-transcription-original-backend))
+
+;;; Normal Cases
+
+(ert-deftest test-transcription-config--transcription-script-path-normal-openai-api-returns-oai-transcribe ()
+ "Should return oai-transcribe script path for openai-api backend."
+ (test-transcription-script-path-setup)
+ (unwind-protect
+ (progn
+ (setq cj/transcribe-backend 'openai-api)
+ (let ((result (cj/--transcription-script-path)))
+ (should (stringp result))
+ (should (string-suffix-p "scripts/oai-transcribe" result))
+ (should (string-prefix-p (expand-file-name user-emacs-directory) result))))
+ (test-transcription-script-path-teardown)))
+
+(ert-deftest test-transcription-config--transcription-script-path-normal-assemblyai-returns-assemblyai-transcribe ()
+ "Should return assemblyai-transcribe script path for assemblyai backend."
+ (test-transcription-script-path-setup)
+ (unwind-protect
+ (progn
+ (setq cj/transcribe-backend 'assemblyai)
+ (let ((result (cj/--transcription-script-path)))
+ (should (stringp result))
+ (should (string-suffix-p "scripts/assemblyai-transcribe" result))
+ (should (string-prefix-p (expand-file-name user-emacs-directory) result))))
+ (test-transcription-script-path-teardown)))
+
+(ert-deftest test-transcription-config--transcription-script-path-normal-local-whisper-returns-local-whisper ()
+ "Should return local-whisper script path for local-whisper backend."
+ (test-transcription-script-path-setup)
+ (unwind-protect
+ (progn
+ (setq cj/transcribe-backend 'local-whisper)
+ (let ((result (cj/--transcription-script-path)))
+ (should (stringp result))
+ (should (string-suffix-p "scripts/local-whisper" result))
+ (should (string-prefix-p (expand-file-name user-emacs-directory) result))))
+ (test-transcription-script-path-teardown)))
+
+(ert-deftest test-transcription-config--transcription-script-path-normal-returns-absolute-path ()
+ "Should return absolute path starting with user-emacs-directory."
+ (test-transcription-script-path-setup)
+ (unwind-protect
+ (progn
+ (setq cj/transcribe-backend 'openai-api)
+ (let ((result (cj/--transcription-script-path)))
+ (should (file-name-absolute-p result))
+ (should (string-prefix-p "/" result))))
+ (test-transcription-script-path-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-transcription-config--transcription-script-path-boundary-path-format-consistent ()
+ "Should return paths in consistent format across backends."
+ (test-transcription-script-path-setup)
+ (unwind-protect
+ (let (paths)
+ (dolist (backend '(openai-api assemblyai local-whisper))
+ (setq cj/transcribe-backend backend)
+ (push (cj/--transcription-script-path) paths))
+ ;; All paths should have same structure: <emacs-dir>/scripts/<name>
+ (should (= (length paths) 3))
+ (should (seq-every-p (lambda (p) (string-match-p "/scripts/[^/]+$" p)) paths)))
+ (test-transcription-script-path-teardown)))
+
+(provide 'test-transcription-config--transcription-script-path)
+;;; test-transcription-config--transcription-script-path.el ends here
diff --git a/tests/test-transcription-counter.el b/tests/test-transcription-counter.el
new file mode 100644
index 00000000..fae353ba
--- /dev/null
+++ b/tests/test-transcription-counter.el
@@ -0,0 +1,98 @@
+;;; test-transcription-counter.el --- Tests for active transcription counting -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--count-active-transcriptions and modeline integration
+;; Categories: Normal cases, Boundary cases
+
+;;; Code:
+
+(require 'ert)
+(require 'transcription-config)
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-cj/--count-active-transcriptions-empty ()
+ "Test count when no transcriptions are active."
+ (let ((cj/transcriptions-list '()))
+ (should (= 0 (cj/--count-active-transcriptions)))))
+
+(ert-deftest test-cj/--count-active-transcriptions-one-running ()
+ "Test count with one running transcription."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running))))
+ (should (= 1 (cj/--count-active-transcriptions)))))
+
+(ert-deftest test-cj/--count-active-transcriptions-multiple-running ()
+ "Test count with multiple running transcriptions."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running)
+ (proc2 "file2.m4a" nil running)
+ (proc3 "file3.m4a" nil running))))
+ (should (= 3 (cj/--count-active-transcriptions)))))
+
+(ert-deftest test-cj/--count-active-transcriptions-mixed-status ()
+ "Test count excludes completed/errored transcriptions."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running)
+ (proc2 "file2.m4a" nil complete)
+ (proc3 "file3.m4a" nil running)
+ (proc4 "file4.m4a" nil error))))
+ (should (= 2 (cj/--count-active-transcriptions)))))
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-cj/--count-active-transcriptions-only-complete ()
+ "Test count when all transcriptions are complete."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil complete)
+ (proc2 "file2.m4a" nil complete))))
+ (should (= 0 (cj/--count-active-transcriptions)))))
+
+(ert-deftest test-cj/--count-active-transcriptions-only-error ()
+ "Test count when all transcriptions errored."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil error)
+ (proc2 "file2.m4a" nil error))))
+ (should (= 0 (cj/--count-active-transcriptions)))))
+
+;; ----------------------------- Modeline Tests --------------------------------
+
+(ert-deftest test-cj/--transcription-modeline-string-none-active ()
+ "Test modeline string when no transcriptions active."
+ (let ((cj/transcriptions-list '()))
+ (should-not (cj/--transcription-modeline-string))))
+
+(ert-deftest test-cj/--transcription-modeline-string-one-active ()
+ "Test modeline string with one active transcription."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running))))
+ (let ((result (cj/--transcription-modeline-string)))
+ (should result)
+ (should (string-match-p "⏺1" result)))))
+
+(ert-deftest test-cj/--transcription-modeline-string-multiple-active ()
+ "Test modeline string with multiple active transcriptions."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running)
+ (proc2 "file2.m4a" nil running)
+ (proc3 "file3.m4a" nil running))))
+ (let ((result (cj/--transcription-modeline-string)))
+ (should result)
+ (should (string-match-p "⏺3" result)))))
+
+(ert-deftest test-cj/--transcription-modeline-string-has-help-echo ()
+ "Test that modeline string has help-echo property."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running))))
+ (let ((result (cj/--transcription-modeline-string)))
+ (should (get-text-property 0 'help-echo result)))))
+
+(ert-deftest test-cj/--transcription-modeline-string-has-face ()
+ "Test that modeline string has warning face."
+ (let ((cj/transcriptions-list
+ '((proc1 "file1.m4a" nil running))))
+ (let ((result (cj/--transcription-modeline-string)))
+ (should (eq 'warning (get-text-property 0 'face result))))))
+
+(provide 'test-transcription-counter)
+;;; test-transcription-counter.el ends here
diff --git a/tests/test-transcription-duration.el b/tests/test-transcription-duration.el
new file mode 100644
index 00000000..370c439b
--- /dev/null
+++ b/tests/test-transcription-duration.el
@@ -0,0 +1,58 @@
+;;; test-transcription-duration.el --- Tests for duration calculation -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--transcription-duration function
+;; Categories: Normal cases, Boundary cases
+
+;;; Code:
+
+(require 'ert)
+(require 'transcription-config)
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-cj/--transcription-duration-zero-seconds ()
+ "Test duration calculation for current time (should be 00:00)."
+ (let ((now (current-time)))
+ (should (string= (cj/--transcription-duration now) "00:00"))))
+
+(ert-deftest test-cj/--transcription-duration-30-seconds ()
+ "Test duration calculation for 30 seconds ago."
+ (let ((start-time (time-subtract (current-time) (seconds-to-time 30))))
+ (should (string= (cj/--transcription-duration start-time) "00:30"))))
+
+(ert-deftest test-cj/--transcription-duration-1-minute ()
+ "Test duration calculation for 1 minute ago."
+ (let ((start-time (time-subtract (current-time) (seconds-to-time 60))))
+ (should (string= (cj/--transcription-duration start-time) "01:00"))))
+
+(ert-deftest test-cj/--transcription-duration-2-minutes-30-seconds ()
+ "Test duration calculation for 2:30 ago."
+ (let ((start-time (time-subtract (current-time) (seconds-to-time 150))))
+ (should (string= (cj/--transcription-duration start-time) "02:30"))))
+
+(ert-deftest test-cj/--transcription-duration-10-minutes ()
+ "Test duration calculation for 10 minutes ago."
+ (let ((start-time (time-subtract (current-time) (seconds-to-time 600))))
+ (should (string= (cj/--transcription-duration start-time) "10:00"))))
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-cj/--transcription-duration-59-seconds ()
+ "Test duration just before 1 minute."
+ (let ((start-time (time-subtract (current-time) (seconds-to-time 59))))
+ (should (string= (cj/--transcription-duration start-time) "00:59"))))
+
+(ert-deftest test-cj/--transcription-duration-1-hour ()
+ "Test duration for 1 hour (60 minutes)."
+ (let ((start-time (time-subtract (current-time) (seconds-to-time 3600))))
+ (should (string= (cj/--transcription-duration start-time) "60:00"))))
+
+(ert-deftest test-cj/--transcription-duration-format ()
+ "Test that duration is always in MM:SS format with zero-padding."
+ (let ((start-time (time-subtract (current-time) (seconds-to-time 65))))
+ (let ((result (cj/--transcription-duration start-time)))
+ (should (string-match-p "^[0-9][0-9]:[0-9][0-9]$" result)))))
+
+(provide 'test-transcription-duration)
+;;; test-transcription-duration.el ends here
diff --git a/tests/test-transcription-log-cleanup.el b/tests/test-transcription-log-cleanup.el
new file mode 100644
index 00000000..82c902d8
--- /dev/null
+++ b/tests/test-transcription-log-cleanup.el
@@ -0,0 +1,44 @@
+;;; test-transcription-log-cleanup.el --- Tests for log cleanup logic -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--should-keep-log function
+;; Categories: Normal cases, Boundary cases
+
+;;; Code:
+
+(require 'ert)
+(require 'transcription-config)
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-cj/--should-keep-log-success-keep-disabled ()
+ "Test that logs are deleted on success when keep-log is nil."
+ (let ((cj/transcription-keep-log-when-done nil))
+ (should-not (cj/--should-keep-log t))))
+
+(ert-deftest test-cj/--should-keep-log-success-keep-enabled ()
+ "Test that logs are kept on success when keep-log is t."
+ (let ((cj/transcription-keep-log-when-done t))
+ (should (cj/--should-keep-log t))))
+
+(ert-deftest test-cj/--should-keep-log-error-keep-disabled ()
+ "Test that logs are always kept on error, even if keep-log is nil."
+ (let ((cj/transcription-keep-log-when-done nil))
+ (should (cj/--should-keep-log nil))))
+
+(ert-deftest test-cj/--should-keep-log-error-keep-enabled ()
+ "Test that logs are kept on error when keep-log is t."
+ (let ((cj/transcription-keep-log-when-done t))
+ (should (cj/--should-keep-log nil))))
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-cj/--should-keep-log-default-behavior ()
+ "Test default behavior (should not keep on success)."
+ ;; Default is nil based on defcustom
+ (let ((cj/transcription-keep-log-when-done nil))
+ (should-not (cj/--should-keep-log t))
+ (should (cj/--should-keep-log nil))))
+
+(provide 'test-transcription-log-cleanup)
+;;; test-transcription-log-cleanup.el ends here
diff --git a/tests/test-transcription-paths.el b/tests/test-transcription-paths.el
new file mode 100644
index 00000000..5ee80e67
--- /dev/null
+++ b/tests/test-transcription-paths.el
@@ -0,0 +1,80 @@
+;;; test-transcription-paths.el --- Tests for transcription file path logic -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--transcription-output-files and cj/--transcription-script-path
+;; Categories: Normal cases, Boundary cases, Error cases
+
+;;; Code:
+
+(require 'ert)
+(require 'transcription-config)
+
+;; ----------------------------- Normal Cases ----------------------------------
+
+(ert-deftest test-cj/--transcription-output-files-simple ()
+ "Test output file paths for simple filename."
+ (let ((result (cj/--transcription-output-files "meeting.m4a")))
+ (should (string= (car result) "meeting.txt"))
+ (should (string= (cdr result) "meeting.log"))))
+
+(ert-deftest test-cj/--transcription-output-files-with-path ()
+ "Test output file paths with full path."
+ (let ((result (cj/--transcription-output-files "/home/user/audio/podcast.mp3")))
+ (should (string= (car result) "/home/user/audio/podcast.txt"))
+ (should (string= (cdr result) "/home/user/audio/podcast.log"))))
+
+(ert-deftest test-cj/--transcription-output-files-different-extensions ()
+ "Test output files for various audio extensions."
+ (dolist (ext '("m4a" "mp3" "wav" "flac" "ogg"))
+ (let* ((input (format "audio.%s" ext))
+ (result (cj/--transcription-output-files input)))
+ (should (string= (car result) "audio.txt"))
+ (should (string= (cdr result) "audio.log")))))
+
+;; ----------------------------- Boundary Cases --------------------------------
+
+(ert-deftest test-cj/--transcription-output-files-multiple-dots ()
+ "Test output files for filename with multiple dots."
+ (let ((result (cj/--transcription-output-files "meeting.2025-11-04.final.m4a")))
+ (should (string= (car result) "meeting.2025-11-04.final.txt"))
+ (should (string= (cdr result) "meeting.2025-11-04.final.log"))))
+
+(ert-deftest test-cj/--transcription-output-files-no-extension ()
+ "Test output files for filename without extension."
+ (let ((result (cj/--transcription-output-files "meeting")))
+ (should (string= (car result) "meeting.txt"))
+ (should (string= (cdr result) "meeting.log"))))
+
+(ert-deftest test-cj/--transcription-output-files-spaces-in-name ()
+ "Test output files for filename with spaces."
+ (let ((result (cj/--transcription-output-files "team meeting 2025.m4a")))
+ (should (string= (car result) "team meeting 2025.txt"))
+ (should (string= (cdr result) "team meeting 2025.log"))))
+
+(ert-deftest test-cj/--transcription-output-files-special-chars ()
+ "Test output files for filename with special characters."
+ (let ((result (cj/--transcription-output-files "meeting_(final).m4a")))
+ (should (string= (car result) "meeting_(final).txt"))
+ (should (string= (cdr result) "meeting_(final).log"))))
+
+;; ----------------------------- Script Path Tests -----------------------------
+
+(ert-deftest test-cj/--transcription-script-path-local-whisper ()
+ "Test script path for local-whisper backend."
+ (let ((cj/transcribe-backend 'local-whisper))
+ (should (string-suffix-p "scripts/local-whisper"
+ (cj/--transcription-script-path)))))
+
+(ert-deftest test-cj/--transcription-script-path-openai-api ()
+ "Test script path for openai-api backend."
+ (let ((cj/transcribe-backend 'openai-api))
+ (should (string-suffix-p "scripts/oai-transcribe"
+ (cj/--transcription-script-path)))))
+
+(ert-deftest test-cj/--transcription-script-path-absolute ()
+ "Test that script path is absolute."
+ (let ((path (cj/--transcription-script-path)))
+ (should (file-name-absolute-p path))))
+
+(provide 'test-transcription-paths)
+;;; test-transcription-paths.el ends here