From 7c7a1ea9da1b6020c4f0e34cc47b752f20d81ce8 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Apr 2026 07:12:34 -0500 Subject: refactor(transcription): extract four sentinel side-effect helpers Break cj/--transcription-sentinel's seven inline side-effects into named helpers: - cj/--write-transcript-on-success: writes process output to .txt on success - cj/--append-to-log: appends event marker + process output to log - cj/--update-transcription-status: marks tracking-list entry complete/error - cj/--notify-completion: sends success or critical notification Also: switch the tautological (cj/--should-keep-log t) to use the local success-p (equivalent but matches the function signature), and rename the unused audio-file sentinel arg to _audio-file. Sentinel shrinks from 48 lines with 7 inline blocks to 14 lines of straight-line helper calls. 10 tests cover the extracted helpers. --- modules/transcription-config.el | 84 +++++++++-------- tests/test-transcription-sentinel-helpers.el | 136 +++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 40 deletions(-) create mode 100644 tests/test-transcription-sentinel-helpers.el diff --git a/modules/transcription-config.el b/modules/transcription-config.el index e8fe1159..1c864b95 100644 --- a/modules/transcription-config.el +++ b/modules/transcription-config.el @@ -193,53 +193,57 @@ Returns the process object." (format "Started on %s" (file-name-nondirectory audio-file))) process))) -(defun cj/--transcription-sentinel (process event audio-file txt-file log-file) +(defun cj/--write-transcript-on-success (process-buffer success-p txt-file) + "Write PROCESS-BUFFER contents to TXT-FILE when SUCCESS-P is non-nil. +No-op if PROCESS-BUFFER is dead or SUCCESS-P is nil." + (when (and success-p (buffer-live-p process-buffer)) + (with-current-buffer process-buffer + (write-region (point-min) (point-max) txt-file nil 'silent)))) + +(defun cj/--append-to-log (process-buffer log-file event) + "Append an EVENT marker plus PROCESS-BUFFER contents to LOG-FILE. +No-op if PROCESS-BUFFER is dead." + (when (buffer-live-p process-buffer) + (with-temp-buffer + (insert-file-contents log-file) + (goto-char (point-max)) + (insert "\n" (format-time-string "[%Y-%m-%d %H:%M:%S] ") event "\n") + (insert-buffer-substring process-buffer) + (write-region (point-min) (point-max) log-file nil 'silent)))) + +(defun cj/--update-transcription-status (process success-p) + "Mark PROCESS's entry as `complete' or `error' based on SUCCESS-P. +No-op if PROCESS isn't tracked." + (when-let ((entry (assq process cj/transcriptions-list))) + (setf (nth 3 entry) (if success-p 'complete 'error)))) + +(defun cj/--notify-completion (success-p txt-file log-file) + "Send completion notification based on SUCCESS-P. +References TXT-FILE on success (normal urgency), LOG-FILE on failure +\(critical urgency)." + (if success-p + (cj/--notify "Transcription" + (format "Complete. Transcript in %s" (file-name-nondirectory txt-file))) + (cj/--notify "Transcription" + (format "Errored. Logs in %s" (file-name-nondirectory log-file)) + 'critical))) + +(defun cj/--transcription-sentinel (process event _audio-file txt-file log-file) "Sentinel for transcription PROCESS. -EVENT is the process event string. -AUDIO-FILE, TXT-FILE, and LOG-FILE are the associated files." +EVENT is the process event string. TXT-FILE and LOG-FILE are the +associated output files." (let* ((success-p (and (string-match-p "finished" event) (= 0 (process-exit-status process)))) - (process-buffer (process-buffer process)) - (entry (assq process cj/transcriptions-list))) - - ;; Write process output to txt file - (when (and success-p (buffer-live-p process-buffer)) - (with-current-buffer process-buffer - (write-region (point-min) (point-max) txt-file nil 'silent))) - - ;; Append process output to log file - (when (buffer-live-p process-buffer) - (with-temp-buffer - (insert-file-contents log-file) - (goto-char (point-max)) - (insert "\n" (format-time-string "[%Y-%m-%d %H:%M:%S] ") event "\n") - (insert-buffer-substring process-buffer) - (write-region (point-min) (point-max) log-file nil 'silent))) - - ;; Update transcription status - (when entry - (setf (nth 3 entry) (if success-p 'complete 'error))) - - ;; Cleanup log file if successful and configured to do so - (when (and success-p (not (cj/--should-keep-log t))) + (process-buffer (process-buffer process))) + (cj/--write-transcript-on-success process-buffer success-p txt-file) + (cj/--append-to-log process-buffer log-file event) + (cj/--update-transcription-status process success-p) + (when (and success-p (not (cj/--should-keep-log success-p))) (delete-file log-file)) - - ;; Kill process buffer (when (buffer-live-p process-buffer) (kill-buffer process-buffer)) - - ;; Notify user - (if success-p - (cj/--notify "Transcription" - (format "Complete. Transcript in %s" (file-name-nondirectory txt-file))) - (cj/--notify "Transcription" - (format "Errored. Logs in %s" (file-name-nondirectory log-file)) - 'critical)) - - ;; Clean up completed transcriptions after 10 minutes + (cj/--notify-completion success-p txt-file log-file) (run-at-time 600 nil #'cj/--cleanup-completed-transcriptions) - - ;; Update modeline (force-mode-line-update t))) (defun cj/--cleanup-completed-transcriptions () diff --git a/tests/test-transcription-sentinel-helpers.el b/tests/test-transcription-sentinel-helpers.el new file mode 100644 index 00000000..b743acf9 --- /dev/null +++ b/tests/test-transcription-sentinel-helpers.el @@ -0,0 +1,136 @@ +;;; test-transcription-sentinel-helpers.el --- Tests for sentinel helpers -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the four helpers extracted from `cj/--transcription-sentinel': +;; - `cj/--write-transcript-on-success' (writes process output to txt) +;; - `cj/--append-to-log' (appends event + output to log) +;; - `cj/--update-transcription-status' (mutates tracking-list status) +;; - `cj/--notify-completion' (sends completion notification) + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(defvar cj/custom-keymap (make-sparse-keymap)) + +(unless (fboundp 'notifications-notify) + (defun notifications-notify (&rest _args) nil)) + +(require 'transcription-config) + +(defmacro test-sentinel-with-temp-file (var extension &rest body) + "Bind VAR to a fresh temp file path with EXTENSION and run BODY." + (declare (indent 2)) + `(let ((,var (make-temp-file "transcription-sentinel-" nil ,extension))) + (unwind-protect + (progn ,@body) + (when (file-exists-p ,var) (delete-file ,var))))) + +(defmacro test-sentinel-with-process-buffer (var content &rest body) + "Bind VAR to a fresh buffer containing CONTENT; kill it after BODY." + (declare (indent 2)) + `(let ((,var (generate-new-buffer " *test-sentinel-proc*"))) + (unwind-protect + (progn + (with-current-buffer ,var (insert ,content)) + ,@body) + (when (buffer-live-p ,var) (kill-buffer ,var))))) + +(defun test-sentinel-file-contents (path) + (with-temp-buffer (insert-file-contents path) (buffer-string))) + +;;; cj/--write-transcript-on-success + +(ert-deftest test-sentinel-write-transcript-normal-writes-on-success () + "On success with a live buffer, writes buffer contents to TXT-FILE." + (test-sentinel-with-temp-file txt-file ".txt" + (test-sentinel-with-process-buffer buf "hello transcript" + (cj/--write-transcript-on-success buf t txt-file) + (should (equal "hello transcript" (test-sentinel-file-contents txt-file)))))) + +(ert-deftest test-sentinel-write-transcript-boundary-noop-on-failure () + "On failure, TXT-FILE is not touched." + (test-sentinel-with-temp-file txt-file ".txt" + (delete-file txt-file) + (test-sentinel-with-process-buffer buf "ignore me" + (cj/--write-transcript-on-success buf nil txt-file)) + (should-not (file-exists-p txt-file)))) + +(ert-deftest test-sentinel-write-transcript-boundary-noop-on-dead-buffer () + "A dead process buffer is a no-op, even on success." + (test-sentinel-with-temp-file txt-file ".txt" + (delete-file txt-file) + (let ((buf (generate-new-buffer " *dead*"))) + (kill-buffer buf) + (cj/--write-transcript-on-success buf t txt-file)) + (should-not (file-exists-p txt-file)))) + +;;; cj/--append-to-log + +(ert-deftest test-sentinel-append-to-log-normal-appends-event-and-output () + "Appends a timestamped event line and the process-buffer contents." + (test-sentinel-with-temp-file log-file ".log" + (with-temp-file log-file (insert "HEADER\n")) + (test-sentinel-with-process-buffer buf "process stderr here" + (cj/--append-to-log buf log-file "finished\n")) + (let ((contents (test-sentinel-file-contents log-file))) + (should (string-match-p "HEADER" contents)) + (should (string-match-p "finished" contents)) + (should (string-match-p "process stderr here" contents))))) + +(ert-deftest test-sentinel-append-to-log-boundary-noop-on-dead-buffer () + "A dead process buffer is a no-op." + (test-sentinel-with-temp-file log-file ".log" + (with-temp-file log-file (insert "ORIGINAL\n")) + (let ((buf (generate-new-buffer " *dead*"))) + (kill-buffer buf) + (cj/--append-to-log buf log-file "event")) + (should (equal "ORIGINAL\n" (test-sentinel-file-contents log-file))))) + +;;; cj/--update-transcription-status + +(ert-deftest test-sentinel-update-status-normal-success-marks-complete () + "On success, the matching entry's status becomes `complete'." + (let ((cj/transcriptions-list '((proc-a "/a.m4a" nil running) + (proc-b "/b.m4a" nil running)))) + (cj/--update-transcription-status 'proc-a t) + (should (eq 'complete (nth 3 (assq 'proc-a cj/transcriptions-list)))) + (should (eq 'running (nth 3 (assq 'proc-b cj/transcriptions-list)))))) + +(ert-deftest test-sentinel-update-status-normal-failure-marks-error () + "On failure, the matching entry's status becomes `error'." + (let ((cj/transcriptions-list '((proc-a "/a.m4a" nil running)))) + (cj/--update-transcription-status 'proc-a nil) + (should (eq 'error (nth 3 (assq 'proc-a cj/transcriptions-list)))))) + +(ert-deftest test-sentinel-update-status-boundary-unknown-process-noop () + "Updating a process that isn't in the list is a no-op." + (let ((cj/transcriptions-list '((proc-a "/a.m4a" nil running)))) + (cj/--update-transcription-status 'proc-unknown t) + (should (eq 'running (nth 3 (assq 'proc-a cj/transcriptions-list)))))) + +;;; cj/--notify-completion + +(ert-deftest test-sentinel-notify-completion-normal-success-mentions-txt () + "On success, notification body references the TXT-FILE." + (let (captured) + (cl-letf (((symbol-function 'cj/--notify) + (lambda (title body &optional urgency) + (setq captured (list title body urgency))))) + (cj/--notify-completion t "/tmp/out.txt" "/tmp/out.log")) + (should (string-match-p "out\\.txt" (nth 1 captured))) + (should-not (nth 2 captured)))) ; normal urgency = nil + +(ert-deftest test-sentinel-notify-completion-normal-failure-mentions-log-and-critical () + "On failure, notification body references the LOG-FILE at critical urgency." + (let (captured) + (cl-letf (((symbol-function 'cj/--notify) + (lambda (title body &optional urgency) + (setq captured (list title body urgency))))) + (cj/--notify-completion nil "/tmp/out.txt" "/tmp/out.log")) + (should (string-match-p "out\\.log" (nth 1 captured))) + (should (eq 'critical (nth 2 captured))))) + +(provide 'test-transcription-sentinel-helpers) +;;; test-transcription-sentinel-helpers.el ends here -- cgit v1.2.3