aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-11 04:50:45 -0500
committerCraig Jennings <c@cjennings.net>2026-05-11 04:50:45 -0500
commit21ec114def7df9aa61e43e8f2cee484ded772e72 (patch)
treebcce37275249879f5fb32d879e19126efdacf2d4
parenta11f554fd533f2139cf6b9e592388a5385d4462b (diff)
downloadchime-21ec114def7df9aa61e43e8f2cee484ded772e72.tar.gz
chime-21ec114def7df9aa61e43e8f2cee484ded772e72.zip
test: close coverage gaps to 99.88% line coverage
Five new test files cover branches the per-function suites missed: the day-wide notification pipeline, the jump-to-event navigation path (including the org-show-entry fallback for Org < 9.6), chime--stop's process-interrupt branch, chime--start's debug log, and the two async error branches in chime--fetch-and-process. The edge-coverage file mops up scattered one-line fallbacks: the day-wide-notification "today" path, the tooltip placeholder pass-through, timestamp-parse's no-context error message, log-silently's mid-line newline insert, the validation :error count, and record-async-failure's chime-debug hook. Line coverage on chime.el goes from 97.1% to 99.88%, 823 of 824 coverable lines. The one remaining line is a pcase _ fallback the preceding regex can't reach.
-rw-r--r--tests/test-chime-day-wide-notifications.el80
-rw-r--r--tests/test-chime-edge-coverage.el174
-rw-r--r--tests/test-chime-fetch-and-process.el83
-rw-r--r--tests/test-chime-jump-to-event.el125
-rw-r--r--tests/test-chime-lifecycle.el87
5 files changed, 549 insertions, 0 deletions
diff --git a/tests/test-chime-day-wide-notifications.el b/tests/test-chime-day-wide-notifications.el
new file mode 100644
index 0000000..260abfc
--- /dev/null
+++ b/tests/test-chime-day-wide-notifications.el
@@ -0,0 +1,80 @@
+;;; test-chime-day-wide-notifications.el --- Tests for chime--day-wide-notifications -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;;; Commentary:
+
+;; Direct unit tests for `chime--day-wide-notifications', which combines the
+;; day-wide filter, the notification-text builder, dedup, and severity
+;; wrapping into a single pipeline.
+;;
+;; Mock-clock note: `with-test-time' replaces `current-time' with a lambda
+;; that returns the captured base time. The base must be computed BEFORE
+;; entering the macro, because `test-time-now' itself calls `current-time'
+;; — passing `(test-time-now)' inside the macro causes infinite recursion.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'testutil-time (expand-file-name "testutil-time.el"))
+
+(ert-deftest test-chime-day-wide-notifications-normal-wraps-with-medium-severity ()
+ "Normal: each generated text is wrapped as (TEXT . \\='medium)."
+ (let* ((base (test-time-now))
+ (today (test-time-today-at 0 0))
+ (ts (test-timestamp-string today t))
+ (event (chime--make-event (list (cons ts nil))
+ "Birthday"
+ '((0 . medium))))
+ (chime-day-wide-advance-notice nil)
+ (chime-show-any-overdue-with-day-wide-alerts t))
+ (with-test-time base
+ (let ((result (chime--day-wide-notifications (list event))))
+ (should (= 1 (length result)))
+ (should (eq 'medium (cdr (car result))))
+ (should (stringp (car (car result))))
+ (should (string-match-p "Birthday" (car (car result))))))))
+
+(ert-deftest test-chime-day-wide-notifications-boundary-empty-events ()
+ "Boundary: empty events list yields empty notification list."
+ (should (null (chime--day-wide-notifications '()))))
+
+(ert-deftest test-chime-day-wide-notifications-boundary-deduplicates-identical-text ()
+ "Boundary: two events producing identical notification text collapse to one."
+ (let* ((base (test-time-now))
+ (today (test-time-today-at 0 0))
+ (ts (test-timestamp-string today t))
+ (event1 (chime--make-event (list (cons ts nil))
+ "Birthday"
+ '((0 . medium))))
+ (event2 (chime--make-event (list (cons ts nil))
+ "Birthday"
+ '((0 . medium))))
+ (chime-day-wide-advance-notice nil)
+ (chime-show-any-overdue-with-day-wide-alerts t))
+ (with-test-time base
+ (let ((result (chime--day-wide-notifications (list event1 event2))))
+ (should (= 1 (length result)))))))
+
+(ert-deftest test-chime-day-wide-notifications-boundary-filters-non-day-wide-events ()
+ "Boundary: events that don't pass the day-wide filter contribute nothing."
+ (let* ((base (test-time-now))
+ (future (test-time-tomorrow-at 9 0))
+ (ts (test-timestamp-string future))
+ (event (chime--make-event (list (cons ts future))
+ "Future Timed"
+ '((10 . medium))))
+ (chime-day-wide-advance-notice nil)
+ (chime-show-any-overdue-with-day-wide-alerts t))
+ (with-test-time base
+ (should (null (chime--day-wide-notifications (list event)))))))
+
+(provide 'test-chime-day-wide-notifications)
+;;; test-chime-day-wide-notifications.el ends here
diff --git a/tests/test-chime-edge-coverage.el b/tests/test-chime-edge-coverage.el
new file mode 100644
index 0000000..1325e77
--- /dev/null
+++ b/tests/test-chime-edge-coverage.el
@@ -0,0 +1,174 @@
+;;; test-chime-edge-coverage.el --- Cover edge branches surfaced by coverage gaps -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Small focused tests that exercise specific branches missed by the
+;; per-function suites. Each test points at the line range it covers
+;; so future readers can correlate quickly.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'testutil-time (expand-file-name "testutil-time.el"))
+(require 'cl-lib)
+
+;;;; chime--day-wide-notification-text fallback (chime.el ~ "t branch")
+
+(ert-deftest test-chime-day-wide-notification-text-future-event-uses-today-fallback ()
+ "Boundary: a future event reaches the cond's `t' fallback when no advance notice is configured."
+ (let* ((future-time (test-time-tomorrow-at 10 0))
+ (timestamp (test-timestamp-string future-time t))
+ (event (chime--make-event (list (cons timestamp nil))
+ "Future Event"
+ '((0 . medium))))
+ (chime-day-wide-advance-notice nil))
+ (should (string= "Future Event is due or scheduled today"
+ (chime--day-wide-notification-text event)))))
+
+;;;; chime--format-event-for-tooltip pcase fallback
+
+(ert-deftest test-chime-format-event-for-tooltip-unknown-placeholder-passes-through ()
+ "Boundary: unknown %X placeholders fall through the pcase and are kept as-is."
+ (let* ((time (test-time-today-at 14 30))
+ (timestamp (test-timestamp-string time))
+ (chime-tooltip-event-format "%t %T %u %X %Z")
+ (result (chime--format-event-for-tooltip timestamp 30 "Meeting")))
+ (should (string-match-p "%X" result))
+ (should (string-match-p "%Z" result))))
+
+;;;; chime--build-upcoming-events-list show-all-day-p=nil branch
+
+(ert-deftest test-chime-build-upcoming-events-list-filters-day-wide-when-not-shown ()
+ "Normal: when SHOW-ALL-DAY-P is nil, the call routes through `chime--filter-day-wide-events'."
+ (let* ((now (test-time-now))
+ (timed-time (test-time-at 0 1 0))
+ (timed-ts (test-timestamp-string timed-time))
+ (all-day-ts (test-timestamp-string now t))
+ (event (chime--make-event
+ (list (cons timed-ts timed-time)
+ (cons all-day-ts nil))
+ "Mixed Event"
+ '((0 . medium))))
+ (lookahead 1440))
+ (let ((with-all (chime--build-upcoming-events-list (list event) now lookahead t))
+ (without-all (chime--build-upcoming-events-list (list event) now lookahead nil)))
+ (should (= 1 (length with-all)))
+ (should (= 1 (length without-all)))
+ ;; Both pick the timed entry as soonest; the branch coverage is the
+ ;; point of this test.
+ (should (equal (nth 1 (car with-all))
+ (nth 1 (car without-all)))))))
+
+;;;; chime--timestamp-parse error path without context arg
+
+(ert-deftest test-chime-timestamp-parse-error-without-context-omits-context-suffix ()
+ "Error: when CONTEXT is nil, the failure message omits the in '...' suffix."
+ (let ((captured nil))
+ (cl-letf (((symbol-function 'org-parse-time-string)
+ (lambda (&rest _) (error "synthetic parse failure")))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (push (apply #'format fmt args) captured))))
+ (should (null (chime--timestamp-parse "<2026-05-11 Mon 09:30>"))))
+ (let ((joined (mapconcat #'identity captured "\n")))
+ (should (string-match-p "Failed to parse timestamp" joined))
+ ;; No "in '...': " suffix when context is nil.
+ (should-not (string-match-p " in '" joined)))))
+
+(ert-deftest test-chime-timestamp-parse-error-with-context-includes-context-suffix ()
+ "Error: when CONTEXT is non-nil, the failure message includes the in '...' suffix."
+ (let ((captured nil))
+ (cl-letf (((symbol-function 'org-parse-time-string)
+ (lambda (&rest _) (error "synthetic parse failure")))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (push (apply #'format fmt args) captured))))
+ (should (null (chime--timestamp-parse "<2026-05-11 Mon 09:30>"
+ "ContextEvent"))))
+ (let ((joined (mapconcat #'identity captured "\n")))
+ (should (string-match-p "in 'ContextEvent'" joined)))))
+
+;;;; chime--extract-gcal-timestamps drawer without :END:
+
+(ert-deftest test-chime-extract-gcal-timestamps-drawer-without-end-returns-empty ()
+ "Boundary: drawer without a closing :END: yields no timestamps and does not error.
+Exercises the `(point)' fallback branch in the drawer-end computation."
+ (let* ((future (test-time-tomorrow-at 9 30))
+ (ts (test-timestamp-string future))
+ (content (format "* Meeting
+:org-gcal:
+%s
+" ts)))
+ (with-temp-buffer
+ (org-mode)
+ (insert content)
+ (goto-char (point-min))
+ (should (null (chime--extract-gcal-timestamps "Meeting"))))))
+
+;;;; chime--display-validation-results :error branch
+
+(ert-deftest test-chime-display-validation-results-counts-error-results ()
+ "Normal: results with :error severity get counted into the summary."
+ (let ((messages nil)
+ (chime-validation-summary-format "errs=%d%s warns=%d%s"))
+ (cl-letf (((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (push (apply #'format fmt args) messages))))
+ (chime--display-validation-results
+ '((:error "first failed" "first detail")
+ (:error "second failed" "second detail")
+ (:warning "third soft" "third detail")
+ (:ok "fourth ok" nil))))
+ (should (member "errs=2s warns=1" (nreverse messages)))))
+
+;;;; chime--record-async-failure with chime-debug feature loaded
+
+(ert-deftest test-chime-record-async-failure-calls-debug-logger-when-feature-loaded ()
+ "Normal: when the chime-debug feature is loaded, the debug logger is invoked."
+ (let ((logged nil)
+ (chime--consecutive-async-failures 0)
+ (chime-max-consecutive-failures 0)
+ (chime-modeline-no-events-text " ⏰")
+ (chime-modeline-string nil))
+ (cl-letf (((symbol-function 'featurep)
+ (lambda (feat &rest _) (eq feat 'chime-debug)))
+ ((symbol-function 'chime--debug-log-async-error)
+ (lambda (err) (setq logged err)))
+ ((symbol-function 'force-mode-line-update) (lambda (&optional _))))
+ (chime--record-async-failure '(error "boom") "Async error"))
+ (should (equal '(error "boom") logged))))
+
+;;;; chime--log-silently mid-line branch (point not at BOL)
+
+(ert-deftest test-chime-log-silently-inserts-leading-newline-when-not-at-bol ()
+ "Boundary: when *Messages* tail is mid-line, log-silently inserts a leading newline."
+ (with-current-buffer (get-buffer-create "*Messages*")
+ (let ((inhibit-read-only t))
+ (goto-char (point-max))
+ (insert "no-newline-prefix")
+ (let ((pos-before (point-max)))
+ (chime--log-silently "edge-coverage")
+ (goto-char pos-before)
+ ;; The inserted newline separates the two strings.
+ (should (looking-at "\n"))
+ (should (search-forward "edge-coverage" nil t))))))
+
+(provide 'test-chime-edge-coverage)
+;;; test-chime-edge-coverage.el ends here
diff --git a/tests/test-chime-fetch-and-process.el b/tests/test-chime-fetch-and-process.el
new file mode 100644
index 0000000..6e2917c
--- /dev/null
+++ b/tests/test-chime-fetch-and-process.el
@@ -0,0 +1,83 @@
+;;; test-chime-fetch-and-process.el --- Tests for chime--fetch-and-process branches -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;;; Commentary:
+
+;; Cover the two error branches inside the `chime--fetch-and-process'
+;; async callback:
+;;
+;; 1. The async subprocess returned an `(async-signal . ERR)' tuple.
+;; 2. The user-supplied callback raised an error during processing.
+;;
+;; Both are caught by the surrounding `condition-case' and routed through
+;; `chime--record-async-failure' with a distinct prefix.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+(ert-deftest test-chime-fetch-and-process-async-signal-records-failure ()
+ "Error: when async returns an `async-signal' tuple, failure is recorded with the async prefix."
+ (let ((recorded nil)
+ (chime--process nil)
+ (chime--last-check-time '(0 0))
+ (chime-modeline-no-events-text " ⏰")
+ (chime-modeline-string nil)
+ (chime--consecutive-async-failures 0)
+ (chime-max-consecutive-failures 0))
+ (cl-letf (((symbol-function 'async-start)
+ (lambda (_start-form finish-func)
+ (funcall finish-func '(async-signal error "boom"))
+ 'fake-process))
+ ((symbol-function 'chime--record-async-failure)
+ (lambda (err prefix) (setq recorded (cons prefix err))))
+ ((symbol-function 'force-mode-line-update) (lambda (&optional _))))
+ (chime--fetch-and-process (lambda (_events) nil)))
+ (should (equal "Async error" (car recorded)))
+ (should (equal '(error "boom") (cdr recorded)))))
+
+(ert-deftest test-chime-fetch-and-process-callback-error-records-failure ()
+ "Error: when the callback raises during processing, failure is recorded with the processing prefix."
+ (let ((recorded nil)
+ (chime--process nil)
+ (chime--last-check-time '(0 0))
+ (chime-modeline-no-events-text " ⏰")
+ (chime-modeline-string nil)
+ (chime--consecutive-async-failures 0)
+ (chime-max-consecutive-failures 0))
+ (cl-letf (((symbol-function 'async-start)
+ (lambda (_start-form finish-func)
+ (funcall finish-func '(((title . "Event"))))
+ 'fake-process))
+ ((symbol-function 'chime--record-async-failure)
+ (lambda (err prefix) (setq recorded (cons prefix err))))
+ ((symbol-function 'force-mode-line-update) (lambda (&optional _))))
+ (chime--fetch-and-process (lambda (_events) (error "callback boom"))))
+ (should (equal "Error processing events" (car recorded)))
+ (should (string-match-p "callback boom"
+ (error-message-string (cdr recorded))))))
+
+(ert-deftest test-chime-fetch-and-process-skips-when-process-live ()
+ "Boundary: an active live process blocks a fresh fetch."
+ (let ((fetched nil)
+ (chime--process 'fake-live-process))
+ (cl-letf (((symbol-function 'process-live-p)
+ (lambda (proc) (eq proc 'fake-live-process)))
+ ((symbol-function 'async-start)
+ (lambda (&rest _)
+ (setq fetched t)
+ 'unused)))
+ (chime--fetch-and-process (lambda (_events) nil)))
+ (should-not fetched)))
+
+(provide 'test-chime-fetch-and-process)
+;;; test-chime-fetch-and-process.el ends here
diff --git a/tests/test-chime-jump-to-event.el b/tests/test-chime-jump-to-event.el
new file mode 100644
index 0000000..6db885b
--- /dev/null
+++ b/tests/test-chime-jump-to-event.el
@@ -0,0 +1,125 @@
+;;; test-chime-jump-to-event.el --- Tests for chime--jump-to-event navigation -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;;; Commentary:
+
+;; Unit tests for `chime--jump-to-event'. The function reconstructs an
+;; org buffer position from the serialized marker-file + marker-pos pair
+;; that survives the async boundary, opens the file, moves point, and
+;; reveals the entry.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'testutil-general (expand-file-name "testutil-general.el"))
+
+(defun test-chime-jump-to-event--make-temp-org-file (content)
+ "Write CONTENT to a temp .org file under the test base dir and return its path."
+ (let* ((base (chime-create-test-base-dir))
+ (path (expand-file-name
+ (concat (make-temp-name "jump-to-event-") ".org")
+ base)))
+ (with-temp-file path
+ (insert content))
+ path))
+
+(ert-deftest test-chime-jump-to-event-normal-opens-file-and-moves-point ()
+ "Normal: opens the event's file and moves point to the recorded position."
+ (unwind-protect
+ (let* ((content "* First heading
+some body
+
+* Target heading
+target body
+")
+ (file (test-chime-jump-to-event--make-temp-org-file content))
+ (target-pos (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ (search-forward "* Target heading")
+ (line-beginning-position)))
+ (event (chime--make-event
+ '(("<2026-05-10 Sun 09:30>" . (26760 32460)))
+ "Target heading"
+ '((0 . medium))
+ file
+ target-pos))
+ (buffer-before-jump nil))
+ (unwind-protect
+ (progn
+ (setq buffer-before-jump (current-buffer))
+ (chime--jump-to-event event)
+ (should (string= file (buffer-file-name)))
+ (should (= target-pos (point))))
+ (when (and (get-file-buffer file))
+ (kill-buffer (get-file-buffer file)))
+ (when (buffer-live-p buffer-before-jump)
+ (switch-to-buffer buffer-before-jump))))
+ (chime-delete-test-base-dir)))
+
+(ert-deftest test-chime-jump-to-event-boundary-nil-marker-file-no-op ()
+ "Boundary: an event with nil marker-file does nothing (no error, no jump)."
+ (let* ((event (chime--make-event
+ '(("<2026-05-10 Sun 09:30>" . (26760 32460)))
+ "No File"
+ '((0 . medium))
+ nil
+ 123))
+ (buffer-before (current-buffer)))
+ (chime--jump-to-event event)
+ (should (eq buffer-before (current-buffer)))))
+
+(ert-deftest test-chime-jump-to-event-boundary-missing-file-no-op ()
+ "Boundary: when the recorded file no longer exists, the jump is a no-op."
+ (let* ((event (chime--make-event
+ '(("<2026-05-10 Sun 09:30>" . (26760 32460)))
+ "Missing File"
+ '((0 . medium))
+ "/tmp/chime-this-path-does-not-exist-xyz.org"
+ 42))
+ (buffer-before (current-buffer)))
+ (chime--jump-to-event event)
+ (should (eq buffer-before (current-buffer)))))
+
+(ert-deftest test-chime-jump-to-event-boundary-uses-org-show-entry-fallback ()
+ "Boundary: when `org-fold-show-entry' is unbound, falls back to `org-show-entry'."
+ (unwind-protect
+ (let* ((content "* Heading
+body
+")
+ (file (test-chime-jump-to-event--make-temp-org-file content))
+ (event (chime--make-event
+ '(("<2026-05-10 Sun 09:30>" . (26760 32460)))
+ "Heading"
+ '((0 . medium))
+ file
+ 1))
+ (fallback-called nil)
+ (orig-fboundp (symbol-function 'fboundp))
+ (buffer-before-jump (current-buffer)))
+ (unwind-protect
+ (cl-letf (((symbol-function 'fboundp)
+ (lambda (sym)
+ (and (not (eq sym 'org-fold-show-entry))
+ (funcall orig-fboundp sym))))
+ ((symbol-function 'org-show-entry)
+ (lambda () (setq fallback-called t))))
+ (chime--jump-to-event event)
+ (should fallback-called)
+ (should (string= file (buffer-file-name))))
+ (when (get-file-buffer file)
+ (kill-buffer (get-file-buffer file)))
+ (when (buffer-live-p buffer-before-jump)
+ (switch-to-buffer buffer-before-jump))))
+ (chime-delete-test-base-dir)))
+
+(provide 'test-chime-jump-to-event)
+;;; test-chime-jump-to-event.el ends here
diff --git a/tests/test-chime-lifecycle.el b/tests/test-chime-lifecycle.el
new file mode 100644
index 0000000..8ce9c13
--- /dev/null
+++ b/tests/test-chime-lifecycle.el
@@ -0,0 +1,87 @@
+;;; test-chime-lifecycle.el --- Tests for chime--stop and chime--start -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;;; Commentary:
+
+;; Unit tests for the internal lifecycle entry points `chime--stop' and
+;; `chime--start'. Tests cover branches not exercised by higher-level
+;; mode-toggle tests: in-progress process cleanup and the debug-only
+;; scheduling log line.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+(ert-deftest test-chime-stop-interrupts-running-process ()
+ "Normal: when chime--process is set, `chime--stop' interrupts it and clears the var."
+ (let ((interrupted nil)
+ (chime--timer nil)
+ (chime--process 'fake-process)
+ (chime--validation-done t)
+ (chime--validation-retry-count 5))
+ (cl-letf (((symbol-function 'interrupt-process)
+ (lambda (proc) (setq interrupted proc))))
+ (chime--stop))
+ (should (eq 'fake-process interrupted))
+ (should (null chime--process))
+ (should-not chime--validation-done)
+ (should (= 0 chime--validation-retry-count))))
+
+(ert-deftest test-chime-stop-no-process-skips-interrupt ()
+ "Boundary: with chime--process nil, `interrupt-process' is never called."
+ (let ((interrupted nil)
+ (chime--timer nil)
+ (chime--process nil)
+ (chime--validation-done t)
+ (chime--validation-retry-count 5))
+ (cl-letf (((symbol-function 'interrupt-process)
+ (lambda (proc) (setq interrupted proc))))
+ (chime--stop))
+ (should (null interrupted))
+ (should-not chime--validation-done)
+ (should (= 0 chime--validation-retry-count))))
+
+(ert-deftest test-chime-start-logs-debug-message-when-feature-loaded ()
+ "Normal: with the chime-debug feature loaded, start emits a scheduling log line."
+ (let ((logged nil)
+ (chime--timer nil)
+ (chime--process nil))
+ (cl-letf (((symbol-function 'featurep)
+ (lambda (feat &rest _) (eq feat 'chime-debug)))
+ ((symbol-function 'run-at-time)
+ (lambda (&rest _) 'fake-timer))
+ ((symbol-function 'chime--log-silently)
+ (lambda (fmt &rest args)
+ (push (apply #'format fmt args) logged))))
+ (chime--start))
+ (should (cl-some (lambda (m) (string-match-p "Scheduling first check" m))
+ logged))
+ (should (eq 'fake-timer chime--timer))))
+
+(ert-deftest test-chime-start-skips-debug-log-when-feature-absent ()
+ "Boundary: without the chime-debug feature loaded, no log line is emitted."
+ (let ((logged nil)
+ (chime--timer nil)
+ (chime--process nil))
+ (cl-letf (((symbol-function 'featurep)
+ (lambda (feat &rest _) (not (eq feat 'chime-debug))))
+ ((symbol-function 'run-at-time)
+ (lambda (&rest _) 'fake-timer))
+ ((symbol-function 'chime--log-silently)
+ (lambda (fmt &rest args)
+ (push (apply #'format fmt args) logged))))
+ (chime--start))
+ (should (null logged))
+ (should (eq 'fake-timer chime--timer))))
+
+(provide 'test-chime-lifecycle)
+;;; test-chime-lifecycle.el ends here