aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-ai-term--agent-buffers.el2
-rw-r--r--tests/test-ai-term--close.el4
-rw-r--r--tests/test-ai-term--collapse-split.el2
-rw-r--r--tests/test-ai-term--dispatch.el2
-rw-r--r--tests/test-ai-term--display-saved.el2
-rw-r--r--tests/test-ai-term--displayed-agent-window.el2
-rw-r--r--tests/test-ai-term--keybindings.el28
-rw-r--r--tests/test-ai-term--launch-command.el2
-rw-r--r--tests/test-ai-term--reuse-edge-window.el2
-rw-r--r--tests/test-ai-term--reuse-existing-agent.el2
-rw-r--r--tests/test-ai-term--server-display.el2
-rw-r--r--tests/test-ai-term--show-or-create.el98
-rw-r--r--tests/test-ai-term--single-window-toggle.el116
-rw-r--r--tests/test-auto-dim-config.el5
-rw-r--r--tests/test-calendar-sync--deferred-start.el43
-rw-r--r--tests/test-calibredb-epub-config--bookmark-name.el94
-rw-r--r--tests/test-calibredb-epub-config--open-to-favorites.el57
-rw-r--r--tests/test-calibredb-epub-config.el11
-rw-r--r--tests/test-custom-buffer-file--buffer-differs-prompt.el197
-rw-r--r--tests/test-custom-buffer-file--diff-whitespace-only.el146
-rw-r--r--tests/test-custom-buffer-file--save-some-buffers.el123
-rw-r--r--tests/test-custom-counts--count-characters.el (renamed from tests/test-custom-misc-cj--count-characters.el)34
-rw-r--r--tests/test-custom-counts-count-characters-buffer-or-region.el (renamed from tests/test-custom-misc-cj-count-characters-buffer-or-region.el)32
-rw-r--r--tests/test-custom-counts-count-words.el (renamed from tests/test-custom-misc-count-words.el)10
-rw-r--r--tests/test-custom-format-format-region.el (renamed from tests/test-custom-misc-format-region.el)10
-rw-r--r--tests/test-custom-line-paragraph-jump-to-matching-paren.el (renamed from tests/test-custom-misc-jump-to-matching-paren.el)10
-rw-r--r--tests/test-custom-text-transform-replace-fraction-glyphs.el (renamed from tests/test-custom-misc-replace-fraction-glyphs.el)10
-rw-r--r--tests/test-dashboard-config-launchers.el32
-rw-r--r--tests/test-dirvish-config--dired-keys.el23
-rw-r--r--tests/test-eshell-config--prompt.el75
-rw-r--r--tests/test-external-open-commands.el85
-rw-r--r--tests/test-font-config--frame-lifecycle.el10
-rw-r--r--tests/test-font-config.el12
-rw-r--r--tests/test-init-module-headers.el11
-rw-r--r--tests/test-local-repository--car-member.el40
-rw-r--r--tests/test-meta-package-headers.el98
-rw-r--r--tests/test-music-config--faces.el25
-rw-r--r--tests/test-nov-reading--palette.el92
-rw-r--r--tests/test-nov-reading--text-scale.el105
-rw-r--r--tests/test-org-capture-config-popup-window.el35
-rw-r--r--tests/test-system-commands-keymap.el37
-rw-r--r--tests/test-system-commands-resolve-and-run.el10
-rw-r--r--tests/test-system-defaults-functions.el8
-rw-r--r--tests/test-system-defaults.el2
-rw-r--r--tests/test-system-lib--completion-file-annotator.el54
-rw-r--r--tests/test-system-utils-commands.el9
-rw-r--r--tests/test-term-config--f8-in-term.el42
-rw-r--r--tests/test-term-tmux-history.el370
-rw-r--r--tests/test-term-toggle--buffer-filter.el52
-rw-r--r--tests/test-term-toggle--dispatch.el4
-rw-r--r--tests/test-term-toggle--display.el2
-rw-r--r--tests/test-transcription-process-and-sentinel.el50
-rw-r--r--tests/test-ui-navigation--window-resize.el45
-rw-r--r--tests/test-undead-buffers--buffer-undead-p.el52
-rw-r--r--tests/test-undead-buffers.el2
-rw-r--r--tests/testutil-general.el2
-rw-r--r--tests/testutil-terminal-buffers.el (renamed from tests/testutil-ghostel-buffers.el)41
57 files changed, 1858 insertions, 613 deletions
diff --git a/tests/test-ai-term--agent-buffers.el b/tests/test-ai-term--agent-buffers.el
index 20c661c45..e0d8faa79 100644
--- a/tests/test-ai-term--agent-buffers.el
+++ b/tests/test-ai-term--agent-buffers.el
@@ -14,7 +14,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(ert-deftest test-ai-term--agent-buffers-empty-when-none-exist ()
"Boundary: no agent-prefixed buffers anywhere -> empty list."
diff --git a/tests/test-ai-term--close.el b/tests/test-ai-term--close.el
index 4098c091e..242bfd749 100644
--- a/tests/test-ai-term--close.el
+++ b/tests/test-ai-term--close.el
@@ -2,7 +2,7 @@
;;; Commentary:
;; `cj/ai-term-close' tears an agent down gracefully: kill its tmux
-;; session (stopping the agent process), kill the ghostel buffer, and
+;; session (stopping the agent process), kill the agent buffer, and
;; remove its window. These tests cover the pure pieces -- the
;; tmux-kill helper, the per-buffer teardown, and the target selection --
;; with `process-file' and the prompt mocked at the boundary.
@@ -15,7 +15,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(ert-deftest test-ai-term--kill-tmux-session-runs-kill-session ()
"Normal: invokes `tmux kill-session -t <session>'."
diff --git a/tests/test-ai-term--collapse-split.el b/tests/test-ai-term--collapse-split.el
index a09af5598..bae913624 100644
--- a/tests/test-ai-term--collapse-split.el
+++ b/tests/test-ai-term--collapse-split.el
@@ -23,7 +23,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
;;; cj/--ai-term-most-recent-non-agent-buffer
diff --git a/tests/test-ai-term--dispatch.el b/tests/test-ai-term--dispatch.el
index 91b5e1bc6..129c53cda 100644
--- a/tests/test-ai-term--dispatch.el
+++ b/tests/test-ai-term--dispatch.el
@@ -16,7 +16,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(ert-deftest test-ai-term--dispatch-window-displayed-returns-toggle-off ()
"Normal: displayed agent window -> (toggle-off . WIN)."
diff --git a/tests/test-ai-term--display-saved.el b/tests/test-ai-term--display-saved.el
index 51c22fde9..5707bea5b 100644
--- a/tests/test-ai-term--display-saved.el
+++ b/tests/test-ai-term--display-saved.el
@@ -26,7 +26,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(ert-deftest test-ai-term--display-saved-uses-desktop-defaults-when-state-nil ()
"Normal: nil state on a desktop -> rightmost, size=cj/ai-term-desktop-width.
diff --git a/tests/test-ai-term--displayed-agent-window.el b/tests/test-ai-term--displayed-agent-window.el
index eeb40ed31..ced3ff414 100644
--- a/tests/test-ai-term--displayed-agent-window.el
+++ b/tests/test-ai-term--displayed-agent-window.el
@@ -12,7 +12,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(ert-deftest test-ai-term--displayed-agent-window-no-buffers-returns-nil ()
"Boundary: no agent buffers anywhere -> nil."
diff --git a/tests/test-ai-term--keybindings.el b/tests/test-ai-term--keybindings.el
index a8b92ffa8..6f7f53a5e 100644
--- a/tests/test-ai-term--keybindings.el
+++ b/tests/test-ai-term--keybindings.el
@@ -4,12 +4,11 @@
;; ai-term lives under the C-; a prefix (vacated when gptel was archived), with
;; the frequent "swap to the next agent" also on M-SPC for a fast chord. M-SPC
;; must reach Emacs from inside an agent buffer, so it is bound in
-;; `ghostel-mode-map' and added to `ghostel-keymap-exceptions' (the semi-char
-;; map otherwise forwards it to the pty). C-; is already an exception via
-;; term-config, so the C-; a family resolves through the global prefix. These
-;; tests require ghostel (so ai-term's `with-eval-after-load' fires) before
-;; ai-term, then confirm the bindings landed and the old F9 family is gone.
-;; `(require 'ghostel)' does not load the native module, so this stays light.
+;; `eat-semi-char-mode-map' (EAT forwards unbound keys to the pty otherwise).
+;; C-; is already bound there via eat-config, so the C-; a family resolves
+;; through the global prefix. These tests require eat (so ai-term's
+;; `with-eval-after-load' fires) before ai-term, then confirm the bindings
+;; landed and the old F9 family is gone.
;;; Code:
@@ -19,7 +18,7 @@
(setq package-user-dir (expand-file-name "elpa" user-emacs-directory))
(package-initialize)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-(require 'ghostel)
+(require 'eat)
(require 'ai-term)
(ert-deftest test-ai-term-keymap-leaf-bindings ()
@@ -37,16 +36,11 @@
"Normal: M-SPC runs `cj/ai-term-next' (the fast swap chord)."
(should (eq (lookup-key (current-global-map) (kbd "M-SPC")) #'cj/ai-term-next)))
-(ert-deftest test-ai-term-meta-space-bound-in-ghostel-mode-map ()
- "Normal: M-SPC is bound in `ghostel-mode-map' so swap works inside an agent."
- (should (eq (keymap-lookup ghostel-mode-map "M-SPC") #'cj/ai-term-next)))
-
-(ert-deftest test-ai-term-meta-space-in-keymap-exceptions ()
- "Regression: M-SPC is in `ghostel-keymap-exceptions' so semi-char mode lets it
-reach Emacs instead of forwarding it to the pty."
- (should (member "M-SPC" ghostel-keymap-exceptions))
- (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "M-SPC")
- 'ghostel--send-event)))
+(ert-deftest test-ai-term-meta-space-bound-in-eat-semi-char-mode-map ()
+ "Normal: M-SPC is bound in `eat-semi-char-mode-map' so swap works inside an
+agent. EAT forwards unbound keys to the pty, so the bind is what lets it reach
+Emacs -- no ghostel-style exception list or rebuild is needed."
+ (should (eq (keymap-lookup eat-semi-char-mode-map "M-SPC") #'cj/ai-term-next)))
(ert-deftest test-ai-term-f9-family-removed-globally ()
"Regression: the old F9 family no longer binds the ai-term commands globally."
diff --git a/tests/test-ai-term--launch-command.el b/tests/test-ai-term--launch-command.el
index 246e70a3f..e61c0e579 100644
--- a/tests/test-ai-term--launch-command.el
+++ b/tests/test-ai-term--launch-command.el
@@ -1,7 +1,7 @@
;;; test-ai-term--launch-command.el --- Tests for cj/--ai-term-launch-command -*- lexical-binding: t; -*-
;;; Commentary:
-;; The launch command is what gets typed into a fresh ghostel shell to bring
+;; The launch command is what gets typed into a fresh shell to bring
;; up the agent inside a per-project tmux session. The session is named
;; `cj/ai-term-tmux-session-prefix' + the project basename, so a second
;; F9 on the same project reattaches to the running agent rather than
diff --git a/tests/test-ai-term--reuse-edge-window.el b/tests/test-ai-term--reuse-edge-window.el
index a9a0529e8..8ba2f759f 100644
--- a/tests/test-ai-term--reuse-edge-window.el
+++ b/tests/test-ai-term--reuse-edge-window.el
@@ -25,7 +25,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(defun cj/test--displayed-buffer-names ()
"Return the buffer names shown in the selected frame, left/top to right/bottom."
diff --git a/tests/test-ai-term--reuse-existing-agent.el b/tests/test-ai-term--reuse-existing-agent.el
index 3f0c64493..361e94be9 100644
--- a/tests/test-ai-term--reuse-existing-agent.el
+++ b/tests/test-ai-term--reuse-existing-agent.el
@@ -17,7 +17,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(ert-deftest test-ai-term--reuse-existing-agent-swaps-buffer-when-window-exists ()
"Normal: an agent window exists -> swap its buffer, return the window."
diff --git a/tests/test-ai-term--server-display.el b/tests/test-ai-term--server-display.el
index b3d32dc83..6db9cf2d3 100644
--- a/tests/test-ai-term--server-display.el
+++ b/tests/test-ai-term--server-display.el
@@ -16,7 +16,7 @@
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
(require 'server)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
(ert-deftest test-ai-term--non-agent-window-finds-code-window ()
"Normal: agent on the right, code on the left -> returns the code window."
diff --git a/tests/test-ai-term--show-or-create.el b/tests/test-ai-term--show-or-create.el
index c6653dcdd..4f5f1f67f 100644
--- a/tests/test-ai-term--show-or-create.el
+++ b/tests/test-ai-term--show-or-create.el
@@ -3,13 +3,13 @@
;;; Commentary:
;; Tests the show-or-create branching:
;;
-;; - buffer absent -> ghostel called, agent command + newline sent
-;; - buffer present, live -> ghostel not called, buffer displayed
-;; - buffer present, dead -> old buffer killed, ghostel recreates
+;; - buffer absent -> eat called, agent command + newline sent
+;; - buffer present, live -> eat not called, buffer displayed
+;; - buffer present, dead -> old buffer killed, eat recreates
;;
-;; ghostel functions are stubbed so the test does no process spawning and
-;; never loads the native module. Production calls (ghostel) with no name and
-;; relies on the dynamically bound `ghostel-buffer-name'; the mock honors that.
+;; eat + the send helper are stubbed so the test does no process spawning.
+;; Production calls (eat) and relies on the dynamically bound `eat-buffer-name';
+;; the mock honors that.
;;; Code:
@@ -19,19 +19,17 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'ai-term)
-;; ghostel isn't loaded in batch -- provide stubs so cl-letf has overrides.
-(unless (fboundp 'ghostel)
- (defun ghostel (&optional _arg) nil))
-(unless (fboundp 'ghostel-send-string)
- (defun ghostel-send-string (_s) nil))
+;; eat isn't loaded in batch -- provide a stub so cl-letf has an override.
+(unless (fboundp 'eat)
+ (defun eat (&optional _program _arg) nil))
-(defmacro test-ai-term--with-mock-ghostel (vars &rest body)
- "Run BODY with ghostel + ghostel-send-string mocked.
+(defmacro test-ai-term--with-mock-eat (vars &rest body)
+ "Run BODY with eat + `cj/--ai-term-send-string' mocked.
-VARS is a plist of capture variable names: :calls (buffer names ghostel
-was asked to create), :strings (sent strings), :default-dir. The mocked
-`ghostel' creates and returns a buffer named after the dynamically bound
-`ghostel-buffer-name', mirroring the real entry point."
+VARS is a plist of capture variable names: :calls (buffer names eat was asked
+to create), :strings (sent strings), :default-dir. The mocked `eat' creates
+and returns a buffer named after the dynamically bound `eat-buffer-name',
+mirroring the real entry point."
(declare (indent 1) (debug t))
(let ((calls (plist-get vars :calls))
(strings (plist-get vars :strings))
@@ -39,14 +37,14 @@ was asked to create), :strings (sent strings), :default-dir. The mocked
`(let ((,calls '())
(,strings '())
(,ddir nil))
- (cl-letf (((symbol-function 'ghostel)
- (lambda (&optional _arg)
+ (cl-letf (((symbol-function 'eat)
+ (lambda (&optional _program _arg)
(setq ,ddir default-directory)
- (let ((b (get-buffer-create ghostel-buffer-name)))
+ (let ((b (get-buffer-create eat-buffer-name)))
(push (buffer-name b) ,calls)
b)))
- ((symbol-function 'ghostel-send-string)
- (lambda (s) (push s ,strings))))
+ ((symbol-function 'cj/--ai-term-send-string)
+ (lambda (_buf s) (push s ,strings))))
,@body))))
(defun test-ai-term--cleanup (name)
@@ -55,33 +53,33 @@ was asked to create), :strings (sent strings), :default-dir. The mocked
(kill-buffer name)))
(ert-deftest test-ai-term--show-or-create-creates-when-buffer-missing ()
- "Normal: no existing buffer -> ghostel called once, launch cmd + newline
-sent, the project recorded at the front of the MRU list."
+ "Normal: no existing buffer -> eat called once, launch cmd + newline sent,
+the project recorded at the front of the MRU list."
(let ((name "agent [normal-create-test]")
(cj/--ai-term-mru nil))
(test-ai-term--cleanup name)
(unwind-protect
- (test-ai-term--with-mock-ghostel (:calls calls :strings strings
- :default-dir ddir)
+ (test-ai-term--with-mock-eat (:calls calls :strings strings
+ :default-dir ddir)
(cj/--ai-term-show-or-create "/tmp/some-project" name)
(should (equal calls (list name)))
- (should (equal (reverse strings)
- (list (cj/--ai-term-launch-command "/tmp/some-project")
- "\n")))
+ (should (equal strings
+ (list (concat (cj/--ai-term-launch-command "/tmp/some-project")
+ "\n"))))
(should (equal ddir "/tmp/some-project"))
(should (equal (car cj/--ai-term-mru) "/tmp/some-project")))
(test-ai-term--cleanup name))))
(ert-deftest test-ai-term--show-or-create-displays-existing-when-process-live ()
- "Normal: buffer exists with live process -> ghostel not called."
+ "Normal: buffer exists with live process -> eat not called."
(let ((name "agent [reuse-test]"))
(test-ai-term--cleanup name)
(unwind-protect
(let ((buf (get-buffer-create name)))
(cl-letf (((symbol-function 'cj/--ai-term-process-live-p)
(lambda (b) (and (eq b buf) t))))
- (test-ai-term--with-mock-ghostel (:calls calls :strings strings
- :default-dir _ddir)
+ (test-ai-term--with-mock-eat (:calls calls :strings strings
+ :default-dir _ddir)
(cj/--ai-term-show-or-create "/tmp/reuse" name)
(should (null calls))
(should (null strings)))))
@@ -95,27 +93,27 @@ sent, the project recorded at the front of the MRU list."
(let ((stale (get-buffer-create name)))
(cl-letf (((symbol-function 'cj/--ai-term-process-live-p)
(lambda (_b) nil)))
- (test-ai-term--with-mock-ghostel (:calls calls :strings strings
- :default-dir _ddir)
+ (test-ai-term--with-mock-eat (:calls calls :strings strings
+ :default-dir _ddir)
(cj/--ai-term-show-or-create "/tmp/dead" name)
(should (equal calls (list name)))
- (should (equal (reverse strings)
- (list (cj/--ai-term-launch-command "/tmp/dead")
- "\n")))
+ (should (equal strings
+ (list (concat (cj/--ai-term-launch-command "/tmp/dead")
+ "\n"))))
(should-not (buffer-live-p stale)))))
(test-ai-term--cleanup name))))
(ert-deftest test-ai-term--show-or-create-preserves-selected-window ()
- "Regression: ghostel's same-window switch must not bury the dashboard.
+ "Regression: eat's same-window switch must not bury the dashboard.
-Real `ghostel' switches the selected window to its buffer as a side-effect of
+Real `eat' switches the selected window to its buffer as a side-effect of
construction. On a fresh-boot frame (one window showing the dashboard), that
side-effect would otherwise leave the original window pointing at the new
-agent buffer. The wrapper runs `(ghostel)' inside `save-window-excursion' so
-the original window state is restored before `display-buffer' fires, leaving
-the dashboard put and letting the alist place agent into a fresh split.
+agent buffer. The wrapper runs `(eat)' inside `save-window-excursion' so the
+original window state is restored before `display-buffer' fires, leaving the
+dashboard put and letting the alist place agent into a fresh split.
-This test stubs `ghostel' to mimic the same-window side-effect and asserts the
+This test stubs `eat' to mimic the same-window side-effect and asserts the
originally-selected window still shows its original buffer afterward."
(let ((agent-name "agent [preserve-window-test]")
(orig-name "*test-original-buffer*"))
@@ -128,24 +126,24 @@ originally-selected window still shows its original buffer afterward."
(orig-win (selected-window)))
(set-window-buffer orig-win orig-buf)
(cl-letf
- (((symbol-function 'ghostel)
- (lambda (&optional _arg)
- (let ((buf (get-buffer-create ghostel-buffer-name)))
+ (((symbol-function 'eat)
+ (lambda (&optional _program _arg)
+ (let ((buf (get-buffer-create eat-buffer-name)))
(set-window-buffer (selected-window) buf)
buf)))
- ((symbol-function 'ghostel-send-string)
- (lambda (_s) nil)))
+ ((symbol-function 'cj/--ai-term-send-string)
+ (lambda (_buf _s) nil)))
(cj/--ai-term-show-or-create "/tmp/preserve" agent-name)
(should (eq (window-buffer orig-win) orig-buf)))))
(test-ai-term--cleanup agent-name)
(when (get-buffer orig-name) (kill-buffer orig-name)))))
(ert-deftest test-ai-term--show-or-create-returns-buffer ()
- "Normal: return value is the ghostel buffer named after the project."
+ "Normal: return value is the eat buffer named after the project."
(let ((name "agent [return-test]"))
(test-ai-term--cleanup name)
(unwind-protect
- (test-ai-term--with-mock-ghostel (:calls _c :strings _s :default-dir _d)
+ (test-ai-term--with-mock-eat (:calls _c :strings _s :default-dir _d)
(let ((result (cj/--ai-term-show-or-create "/tmp/return" name)))
(should (bufferp result))
(should (equal (buffer-name result) name))))
diff --git a/tests/test-ai-term--single-window-toggle.el b/tests/test-ai-term--single-window-toggle.el
index aa507f032..dd5adbcc3 100644
--- a/tests/test-ai-term--single-window-toggle.el
+++ b/tests/test-ai-term--single-window-toggle.el
@@ -19,7 +19,7 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
-(require 'testutil-ghostel-buffers)
+(require 'testutil-terminal-buffers)
;;; Normal Cases
@@ -182,5 +182,119 @@ the flag nil (no spurious set)."
(kill-buffer "*test-sw-untouched-left*"))
(cj/test--kill-agent-buffers))))
+;;; Geometry tracking (Approach B: remember the agent's fullscreen state)
+
+(ert-deftest test-ai-term--track-geometry-sole-sets-fullscreen ()
+ "Normal: an agent window that is the sole window in its frame sets
+`cj/--ai-term-last-fullscreen'."
+ (cj/test--kill-agent-buffers)
+ (let ((agent-name "agent [track-sole]")
+ (cj/--ai-term-last-fullscreen nil))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (let ((agent-buf (get-buffer-create agent-name)))
+ (set-window-buffer (selected-window) agent-buf)
+ (should (one-window-p))
+ (cj/--ai-term-track-geometry)
+ (should (eq cj/--ai-term-last-fullscreen t))))
+ (cj/test--kill-agent-buffers))))
+
+(ert-deftest test-ai-term--track-geometry-split-clears-fullscreen ()
+ "Normal: an agent window shown as a split clears `cj/--ai-term-last-fullscreen'.
+The tracker must NOT re-capture dock direction/size here -- doing so on every
+window change drifts the dock height per cycle; toggle-off owns that capture."
+ (cj/test--kill-agent-buffers)
+ (let ((agent-name "agent [track-split]")
+ (left-name "*test-track-left*")
+ (cj/--ai-term-last-fullscreen t) ; pretend it was fullscreen
+ (cj/--ai-term-last-direction nil)
+ (cj/--ai-term-last-size nil))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (let ((agent-buf (get-buffer-create agent-name))
+ (left-buf (get-buffer-create left-name)))
+ (set-window-buffer (selected-window) left-buf)
+ (let ((agent-win (split-window (selected-window) nil 'right)))
+ (set-window-buffer agent-win agent-buf)
+ (should-not (one-window-p))
+ (cj/--ai-term-track-geometry)
+ (should-not cj/--ai-term-last-fullscreen) ; flag cleared
+ (should-not cj/--ai-term-last-size)))) ; dock size NOT re-captured here
+ (when (get-buffer left-name) (kill-buffer left-name))
+ (cj/test--kill-agent-buffers))))
+
+(ert-deftest test-ai-term--track-geometry-no-agent-retains-state ()
+ "Boundary: with no agent window displayed, the tracker leaves the last-seen
+fullscreen flag untouched -- that is the just-left state to replay."
+ (cj/test--kill-agent-buffers)
+ (let ((cj/--ai-term-last-fullscreen t))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window)
+ (get-buffer-create "*test-track-none*"))
+ (should-not (cj/--ai-term-displayed-agent-window))
+ (cj/--ai-term-track-geometry)
+ (should (eq cj/--ai-term-last-fullscreen t))) ; unchanged
+ (when (get-buffer "*test-track-none*") (kill-buffer "*test-track-none*"))
+ (cj/test--kill-agent-buffers))))
+
+(ert-deftest test-ai-term--display-saved-restores-fullscreen-when-last-fullscreen ()
+ "Normal: when the agent was last fullscreen and the target frame is a single
+window, display-saved restores it in place rather than docking -- Craig's case
+of leaving a fullscreen agent, switching to another fullscreen buffer, then
+M-SPC. A stale dock size is on record; the split path must NOT run."
+ (cj/test--kill-agent-buffers)
+ (let ((agent-name "agent [restore-fullscreen]")
+ (cj/--ai-term-last-fullscreen t)
+ (cj/--ai-term-last-was-bury nil)
+ (cj/--ai-term-last-direction 'right)
+ (cj/--ai-term-last-size 40))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (let* ((other-buf (get-buffer-create "*test-rfs-other*"))
+ (agent-buf (get-buffer-create agent-name))
+ (win (selected-window))
+ (split-called nil))
+ (set-window-buffer win other-buf)
+ (should (one-window-p))
+ (cl-letf (((symbol-function 'display-buffer-in-direction)
+ (lambda (&rest _) (setq split-called t) (selected-window))))
+ (cj/--ai-term-display-saved agent-buf nil))
+ (should (one-window-p)) ; no split -- stayed full-frame
+ (should (eq (window-buffer win) agent-buf)) ; agent took the lone window
+ (should-not split-called))) ; dock path never ran
+ (when (get-buffer "*test-rfs-other*") (kill-buffer "*test-rfs-other*"))
+ (cj/test--kill-agent-buffers))))
+
+(ert-deftest test-ai-term--display-saved-docks-when-not-fullscreen ()
+ "Boundary: without the fullscreen flag (or a bury), a single-window summon
+docks via the saved-direction split. The discriminator is the remembered
+state, not merely `one-window-p', so first-open and ordinary summons still
+dock rather than seizing the whole frame."
+ (cj/test--kill-agent-buffers)
+ (let ((agent-name "agent [dock-not-fullscreen]")
+ (cj/--ai-term-last-fullscreen nil)
+ (cj/--ai-term-last-was-bury nil)
+ (cj/--ai-term-last-direction 'right)
+ (cj/--ai-term-last-size 40))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (let ((agent-buf (get-buffer-create agent-name))
+ (split-called nil))
+ (set-window-buffer (selected-window)
+ (get-buffer-create "*test-dock-other*"))
+ (should (one-window-p))
+ (cl-letf (((symbol-function 'display-buffer-in-direction)
+ (lambda (&rest _) (setq split-called t) (selected-window))))
+ (cj/--ai-term-display-saved agent-buf nil))
+ (should split-called))) ; dock path ran despite one-window-p
+ (when (get-buffer "*test-dock-other*") (kill-buffer "*test-dock-other*"))
+ (cj/test--kill-agent-buffers))))
+
(provide 'test-ai-term--single-window-toggle)
;;; test-ai-term--single-window-toggle.el ends here
diff --git a/tests/test-auto-dim-config.el b/tests/test-auto-dim-config.el
index 532e7dfae..2686b88f3 100644
--- a/tests/test-auto-dim-config.el
+++ b/tests/test-auto-dim-config.el
@@ -8,8 +8,9 @@
;; in ~/code and may be absent on a clean checkout.
;;
;; The vterm dim-integration tests were removed when the terminal engine moved
-;; to ghostel: ghostel bakes its palette per-terminal (no per-window color
-;; hook), so terminal buffers no longer participate in window dimming.
+;; off vterm. EAT (the current engine) renders in real Emacs faces and uses the
+;; `default' face for its background, so terminal buffers dim like any other
+;; buffer with no dedicated integration.
;;; Code:
diff --git a/tests/test-calendar-sync--deferred-start.el b/tests/test-calendar-sync--deferred-start.el
new file mode 100644
index 000000000..a3a9c0198
--- /dev/null
+++ b/tests/test-calendar-sync--deferred-start.el
@@ -0,0 +1,43 @@
+;;; test-calendar-sync--deferred-start.el --- Deferred auto-start tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; calendar-sync arms its auto-sync on the first org-agenda use instead of at
+;; load, so a cold gpg-agent is not prompted for the authinfo passphrase at
+;; startup (the :secret-host feed URLs decrypt authinfo.gpg). These tests cover
+;; the one-shot helper: it starts sync once and removes itself, even when the
+;; start call errors.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'calendar-sync)
+
+;; org-agenda need not be loaded under `make test'; declare the hook special so
+;; the dynamic `let' bindings below shadow it cleanly.
+(defvar org-agenda-mode-hook)
+
+(ert-deftest test-calendar-sync-deferred-start-fires-once-and-unhooks ()
+ "Normal: the one-shot starts sync once and removes itself from the hook."
+ (let ((started 0)
+ (org-agenda-mode-hook (list #'calendar-sync--auto-start-on-first-agenda)))
+ (cl-letf (((symbol-function 'calendar-sync-start)
+ (lambda (&rest _) (setq started (1+ started)))))
+ (calendar-sync--auto-start-on-first-agenda))
+ (should (= started 1))
+ (should-not (member #'calendar-sync--auto-start-on-first-agenda
+ org-agenda-mode-hook))))
+
+(ert-deftest test-calendar-sync-deferred-start-unhooks-even-when-start-errors ()
+ "Error: a start failure still leaves the hook removed, so it cannot re-fire."
+ (let ((org-agenda-mode-hook (list #'calendar-sync--auto-start-on-first-agenda)))
+ (cl-letf (((symbol-function 'calendar-sync-start)
+ (lambda (&rest _) (error "boom"))))
+ (should-error (calendar-sync--auto-start-on-first-agenda)))
+ (should-not (member #'calendar-sync--auto-start-on-first-agenda
+ org-agenda-mode-hook))))
+
+(provide 'test-calendar-sync--deferred-start)
+;;; test-calendar-sync--deferred-start.el ends here
diff --git a/tests/test-calibredb-epub-config--bookmark-name.el b/tests/test-calibredb-epub-config--bookmark-name.el
index 2e1d253e9..7e9ffa345 100644
--- a/tests/test-calibredb-epub-config--bookmark-name.el
+++ b/tests/test-calibredb-epub-config--bookmark-name.el
@@ -1,10 +1,13 @@
-;;; test-calibredb-epub-config--bookmark-name.el --- Nov bookmark naming tests -*- lexical-binding: t; -*-
+;;; test-calibredb-epub-config--bookmark-name.el --- Reading bookmark naming tests -*- lexical-binding: t; -*-
;;; Commentary:
-;; Tests for the clean "Author, Title" bookmark naming that replaces nov.el's
-;; filename-based default. The name is parsed from the EPUB filename (Calibre's
-;; "<Title> - <Author>.epub" convention), restoring colons that Calibre
-;; sanitized to underscores and reordering to "Author, Title".
+;; Tests for the clean "Author, Title" bookmark naming that replaces the
+;; filename-based default for both EPUB (nov) and PDF (pdf-view) bookmarks.
+;; The name is parsed from the file's name (Calibre's "<Title> - <Author>.<ext>"
+;; convention), restoring colons that Calibre sanitized to underscores and
+;; reordering to "Author, Title". The parser is extension-agnostic, so the
+;; same advice serves both nov-bookmark-make-record and
+;; pdf-view-bookmark-make-record.
;;; Code:
@@ -12,76 +15,93 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'calibredb-epub-config)
-;;; cj/--nov-clean-title
+;;; cj/--reading-clean-title
-(ert-deftest test-nov-clean-title-passthrough ()
+(ert-deftest test-reading-clean-title-passthrough ()
"Normal: a clean string is returned unchanged."
- (should (equal (cj/--nov-clean-title "Agatha Christie") "Agatha Christie"))
- (should (equal (cj/--nov-clean-title "The A.B.C. Murders") "The A.B.C. Murders")))
+ (should (equal (cj/--reading-clean-title "Agatha Christie") "Agatha Christie"))
+ (should (equal (cj/--reading-clean-title "The A.B.C. Murders") "The A.B.C. Murders")))
-(ert-deftest test-nov-clean-title-restores-colon ()
+(ert-deftest test-reading-clean-title-restores-colon ()
"Boundary: Calibre's \"_ \" colon substitution is restored to \": \"."
- (should (equal (cj/--nov-clean-title "Frege_ A Guide for the Perplexed")
+ (should (equal (cj/--reading-clean-title "Frege_ A Guide for the Perplexed")
"Frege: A Guide for the Perplexed"))
- (should (equal (cj/--nov-clean-title "The Fool's Progress_ An Honest Novel")
+ (should (equal (cj/--reading-clean-title "The Fool's Progress_ An Honest Novel")
"The Fool's Progress: An Honest Novel")))
-(ert-deftest test-nov-clean-title-stray-underscore-and-whitespace ()
+(ert-deftest test-reading-clean-title-stray-underscore-and-whitespace ()
"Boundary: a non-colon underscore becomes a space; whitespace collapses."
- (should (equal (cj/--nov-clean-title "a_b") "a b"))
- (should (equal (cj/--nov-clean-title " x y ") "x y")))
+ (should (equal (cj/--reading-clean-title "a_b") "a b"))
+ (should (equal (cj/--reading-clean-title " x y ") "x y")))
-(ert-deftest test-nov-clean-title-rejects-blank-and-nonstring ()
+(ert-deftest test-reading-clean-title-rejects-blank-and-nonstring ()
"Error: nil, empty, all-whitespace, or non-string yields nil."
- (should-not (cj/--nov-clean-title nil))
- (should-not (cj/--nov-clean-title ""))
- (should-not (cj/--nov-clean-title " "))
- (should-not (cj/--nov-clean-title 42)))
+ (should-not (cj/--reading-clean-title nil))
+ (should-not (cj/--reading-clean-title ""))
+ (should-not (cj/--reading-clean-title " "))
+ (should-not (cj/--reading-clean-title 42)))
-;;; cj/--nov-bookmark-name-from-file
+;;; cj/--reading-bookmark-name-from-file
-(ert-deftest test-nov-bookmark-name-real-examples ()
+(ert-deftest test-reading-bookmark-name-real-examples ()
"Normal: real Calibre filenames become \"Author, Title\" with colons restored."
- (should (equal (cj/--nov-bookmark-name-from-file
+ (should (equal (cj/--reading-bookmark-name-from-file
"/books/Frege_ A Guide for the Perplexed - Edward Kanterian.epub")
"Edward Kanterian, Frege: A Guide for the Perplexed"))
- (should (equal (cj/--nov-bookmark-name-from-file
+ (should (equal (cj/--reading-bookmark-name-from-file
"/books/The A.B.C. Murders - Agatha Christie.epub")
"Agatha Christie, The A.B.C. Murders"))
- (should (equal (cj/--nov-bookmark-name-from-file
+ (should (equal (cj/--reading-bookmark-name-from-file
"/books/The Fool's Progress_ An Honest Novel - Edward Abbey.epub")
"Edward Abbey, The Fool's Progress: An Honest Novel")))
-(ert-deftest test-nov-bookmark-name-splits-on-last-separator ()
+(ert-deftest test-reading-bookmark-name-pdf-extension ()
+ "Normal: a PDF filename is parsed the same way -- the parser is extension-
+agnostic, so PDF bookmarks reformat like EPUB ones."
+ (should (equal (cj/--reading-bookmark-name-from-file
+ "/books/Engines of Logic_ Mathematicians and the O - Martin Davis.pdf")
+ "Martin Davis, Engines of Logic: Mathematicians and the O")))
+
+(ert-deftest test-reading-bookmark-name-splits-on-last-separator ()
"Boundary: a title containing \" - \" splits on the LAST separator."
- (should (equal (cj/--nov-bookmark-name-from-file "/b/Title - Part Two - Some Author.epub")
+ (should (equal (cj/--reading-bookmark-name-from-file "/b/Title - Part Two - Some Author.epub")
"Some Author, Title - Part Two")))
-(ert-deftest test-nov-bookmark-name-no-separator ()
+(ert-deftest test-reading-bookmark-name-no-separator ()
"Boundary: a filename with no \" - \" falls back to the cleaned whole name."
- (should (equal (cj/--nov-bookmark-name-from-file "/b/Untitled_ Draft.epub")
+ (should (equal (cj/--reading-bookmark-name-from-file "/b/Untitled_ Draft.epub")
"Untitled: Draft")))
-(ert-deftest test-nov-bookmark-name-nil-and-empty ()
+(ert-deftest test-reading-bookmark-name-nil-and-empty ()
"Error: nil or empty path yields nil."
- (should-not (cj/--nov-bookmark-name-from-file nil))
- (should-not (cj/--nov-bookmark-name-from-file "")))
+ (should-not (cj/--reading-bookmark-name-from-file nil))
+ (should-not (cj/--reading-bookmark-name-from-file "")))
-;;; cj/--nov-bookmark-rename-record
+;;; cj/--reading-bookmark-rename-record
-(ert-deftest test-nov-bookmark-rename-record-replaces-name ()
+(ert-deftest test-reading-bookmark-rename-record-replaces-name ()
"Normal: the record's name is rebuilt from its filename; the alist is kept."
(let* ((record (cons "The A.B.C. Murders - Agatha Christie.epub"
'((filename . "/b/The A.B.C. Murders - Agatha Christie.epub")
(index . 0))))
- (out (cj/--nov-bookmark-rename-record record)))
+ (out (cj/--reading-bookmark-rename-record record)))
(should (equal (car out) "Agatha Christie, The A.B.C. Murders"))
(should (equal (cdr out) (cdr record)))))
-(ert-deftest test-nov-bookmark-rename-record-keeps-original-without-filename ()
+(ert-deftest test-reading-bookmark-rename-record-pdf ()
+ "Normal: a PDF-shaped record (filename from `bookmark-make-record-default')
+gets the same \"Author, Title\" rename."
+ (let* ((record (cons "Engines of Logic_ Mathematicians and the O - Martin Davis.pdf"
+ '((filename . "/b/Engines of Logic_ Mathematicians and the O - Martin Davis.pdf")
+ (page . 12))))
+ (out (cj/--reading-bookmark-rename-record record)))
+ (should (equal (car out) "Martin Davis, Engines of Logic: Mathematicians and the O"))
+ (should (equal (cdr out) (cdr record)))))
+
+(ert-deftest test-reading-bookmark-rename-record-keeps-original-without-filename ()
"Boundary: a record with no usable filename is returned unchanged."
(let ((record (cons "whatever" '((index . 0)))))
- (should (equal (cj/--nov-bookmark-rename-record record) record))))
+ (should (equal (cj/--reading-bookmark-rename-record record) record))))
(provide 'test-calibredb-epub-config--bookmark-name)
;;; test-calibredb-epub-config--bookmark-name.el ends here
diff --git a/tests/test-calibredb-epub-config--open-to-favorites.el b/tests/test-calibredb-epub-config--open-to-favorites.el
new file mode 100644
index 000000000..d11618081
--- /dev/null
+++ b/tests/test-calibredb-epub-config--open-to-favorites.el
@@ -0,0 +1,57 @@
+;;; test-calibredb-epub-config--open-to-favorites.el --- in-progress open filter -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--calibredb-open-to-favorites' advises `calibredb' :after so every launch
+;; lands filtered to `calibredb-favorite-keyword' (Craig's "in-progress" books).
+;; It scopes the filter to the TAG field (sets `calibredb-tag-filter-p', clears
+;; the other filter-p flags) before delegating to `calibredb-search-keyword-filter',
+;; so the keyword can't over-match in a book's title or description. It no-ops
+;; when no usable keyword is set.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'calibredb-epub-config)
+
+;; calibredb defcustom + internal flags; declared special so `let' binds them
+;; dynamically (all unbound under `make test', which never runs the use-package
+;; :config or loads calibredb).
+(defvar calibredb-favorite-keyword)
+(defvar calibredb-tag-filter-p)
+(defvar calibredb-favorite-filter-p)
+(defvar calibredb-author-filter-p)
+(defvar calibredb-date-filter-p)
+(defvar calibredb-format-filter-p)
+
+(ert-deftest test-calibredb-open-to-favorites-applies-keyword-scoped-to-tags ()
+ "Normal: with a favorite keyword set, the filter runs with that keyword and is
+scoped to the tag field (so it can't over-match a description); a stale
+non-tag filter flag is cleared."
+ (let ((applied :unset)
+ (calibredb-favorite-keyword "in-progress")
+ (calibredb-tag-filter-p nil)
+ (calibredb-favorite-filter-p t) ; stale, must be cleared
+ (calibredb-author-filter-p nil)
+ (calibredb-date-filter-p nil)
+ (calibredb-format-filter-p nil))
+ (cl-letf (((symbol-function 'calibredb-search-keyword-filter)
+ (lambda (kw) (setq applied kw))))
+ (cj/--calibredb-open-to-favorites))
+ (should (equal applied "in-progress")) ; keyword applied
+ (should (eq calibredb-tag-filter-p t)) ; scoped to the tag field
+ (should-not calibredb-favorite-filter-p))) ; stale flag cleared
+
+(ert-deftest test-calibredb-open-to-favorites-noop-without-usable-keyword ()
+ "Boundary/Error: nil, empty, or non-string keyword applies no filter."
+ (dolist (kw (list nil "" 42))
+ (let ((applied :unset)
+ (calibredb-favorite-keyword kw))
+ (cl-letf (((symbol-function 'calibredb-search-keyword-filter)
+ (lambda (k) (setq applied k))))
+ (cj/--calibredb-open-to-favorites))
+ (should (eq applied :unset)))))
+
+(provide 'test-calibredb-epub-config--open-to-favorites)
+;;; test-calibredb-epub-config--open-to-favorites.el ends here
diff --git a/tests/test-calibredb-epub-config.el b/tests/test-calibredb-epub-config.el
index cb3a9ba74..71581d4c9 100644
--- a/tests/test-calibredb-epub-config.el
+++ b/tests/test-calibredb-epub-config.el
@@ -173,12 +173,13 @@ re-render of the document."
(should (commandp #'cj/nov-narrow-text)))
(ert-deftest test-calibredb-epub-nov-width-commands-bound-in-nov-mode-map ()
- "Normal: +/= widen and -/_ narrow the text column in `nov-mode-map'."
+ "Normal: { } adjust the text column in `nov-mode-map' (+/-/= are font size)."
(skip-unless (and (require 'nov nil t) (boundp 'nov-mode-map)))
- (should (eq (keymap-lookup nov-mode-map "+") #'cj/nov-widen-text))
- (should (eq (keymap-lookup nov-mode-map "=") #'cj/nov-widen-text))
- (should (eq (keymap-lookup nov-mode-map "-") #'cj/nov-narrow-text))
- (should (eq (keymap-lookup nov-mode-map "_") #'cj/nov-narrow-text)))
+ (should (eq (keymap-lookup nov-mode-map "}") #'cj/nov-widen-text))
+ (should (eq (keymap-lookup nov-mode-map "{") #'cj/nov-narrow-text))
+ (should (eq (keymap-lookup nov-mode-map "+") #'cj/nov-reading-text-bigger))
+ (should (eq (keymap-lookup nov-mode-map "-") #'cj/nov-reading-text-smaller))
+ (should (eq (keymap-lookup nov-mode-map "=") #'cj/nov-reading-text-reset)))
;;; -------------------------- cj/nov-apply-preferences ------------------------
diff --git a/tests/test-custom-buffer-file--buffer-differs-prompt.el b/tests/test-custom-buffer-file--buffer-differs-prompt.el
new file mode 100644
index 000000000..109ca121f
--- /dev/null
+++ b/tests/test-custom-buffer-file--buffer-differs-prompt.el
@@ -0,0 +1,197 @@
+;;; test-custom-buffer-file--buffer-differs-prompt.el --- disk-changed save prompt pieces -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Pure-logic tests for the disk-changed save prompt (the C-x C-s case where the
+;; buffer is modified AND the file changed on disk): the question string (with a
+;; terse whitespace-only parenthetical), the labeled-but-terse choice list, and
+;; the key->action mapping. The interactive read loop, the diff display, and the
+;; save/revert dispatch are exercised live, not here.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'custom-buffer-file)
+
+(declare-function cj/--buffer-differs-prompt-string "custom-buffer-file" (name ws-only-p))
+(declare-function cj/--buffer-differs-choices "custom-buffer-file" ())
+(declare-function cj/--buffer-differs-action "custom-buffer-file" (key))
+
+;;; ------------------- cj/--buffer-differs-prompt-string ----------------------
+
+(ert-deftest test-cbf-buffer-differs-prompt-plain ()
+ "Normal: without whitespace-only, the prompt names the buffer, no parenthetical."
+ (let ((s (cj/--buffer-differs-prompt-string "todo.org" nil)))
+ (should (string-match-p "todo\\.org" s))
+ (should-not (string-match-p "whitespace" s))))
+
+(ert-deftest test-cbf-buffer-differs-prompt-whitespace-only ()
+ "Normal: whitespace-only folds in a terse \"(whitespace only)\" parenthetical."
+ (let ((s (cj/--buffer-differs-prompt-string "todo.org" t)))
+ (should (string-match-p "todo\\.org" s))
+ (should (string-match-p "(whitespace only)" s))))
+
+;;; ---------------------- cj/--buffer-differs-choices -------------------------
+
+(ert-deftest test-cbf-buffer-differs-choices-keys ()
+ "Normal: the menu offers save, diff, clean, revert, and cancel."
+ (let ((c (cj/--buffer-differs-choices)))
+ (dolist (key '(?s ?d ?w ?r ?c))
+ (should (assq key c)))))
+
+(ert-deftest test-cbf-buffer-differs-choices-terse-names ()
+ "Boundary: inline names stay terse (one word) so the menu fits at a glance."
+ (dolist (entry (cj/--buffer-differs-choices))
+ (let ((name (nth 1 entry)))
+ (should (stringp name))
+ (should-not (string-match-p " " name)))))
+
+(ert-deftest test-cbf-buffer-differs-choices-clean-help-mentions-whitespace ()
+ "Normal: the clean option's description (the ? help) names whitespace."
+ (let ((entry (assq ?w (cj/--buffer-differs-choices))))
+ (should (string-match-p "whitespace" (or (nth 2 entry) "")))))
+
+(ert-deftest test-cbf-buffer-differs-choices-revert-help-mentions-disk ()
+ "Normal: the revert option's description makes clear it rereads from disk."
+ (let ((entry (assq ?r (cj/--buffer-differs-choices))))
+ (should (string-match-p "disk" (or (nth 2 entry) "")))))
+
+;;; ---------------------- cj/--buffer-differs-action --------------------------
+
+(ert-deftest test-cbf-buffer-differs-action-save ()
+ "Normal: s overwrites the file with the buffer."
+ (should (eq (cj/--buffer-differs-action ?s) 'save)))
+
+(ert-deftest test-cbf-buffer-differs-action-clean ()
+ "Normal: w cleans whitespace, then saves."
+ (should (eq (cj/--buffer-differs-action ?w) 'clean-save)))
+
+(ert-deftest test-cbf-buffer-differs-action-revert ()
+ "Normal: r discards edits and rereads from disk."
+ (should (eq (cj/--buffer-differs-action ?r) 'revert)))
+
+(ert-deftest test-cbf-buffer-differs-action-diff ()
+ "Normal: d peeks at the diff (re-prompt is the caller's concern)."
+ (should (eq (cj/--buffer-differs-action ?d) 'diff)))
+
+(ert-deftest test-cbf-buffer-differs-action-cancel ()
+ "Boundary: c cancels, leaving the buffer untouched."
+ (should (eq (cj/--buffer-differs-action ?c) 'cancel)))
+
+(ert-deftest test-cbf-buffer-differs-action-unknown ()
+ "Error: an unmapped key returns nil."
+ (should-not (cj/--buffer-differs-action ?z)))
+
+;;; ------------------- cj/--buffer-changed-on-disk-p --------------------------
+;; Real visited-file buffers; modtime state is driven (set-visited-file-modtime),
+;; not mocked. The trigger is the disk-changed conflict: modified AND the file
+;; changed on disk since visited.
+
+(declare-function cj/--buffer-changed-on-disk-p "custom-buffer-file" (buffer))
+
+(defun test-cbf-cod--with-visited (edit-fn body-fn)
+ "Visit a temp file, run EDIT-FN in its buffer, call BODY-FN with the buffer."
+ (let ((f (make-temp-file "cbf-cod-" nil ".txt")))
+ (unwind-protect
+ (progn
+ (with-temp-file f (insert "original\n"))
+ (let ((buf (find-file-noselect f)))
+ (unwind-protect
+ (with-current-buffer buf (funcall edit-fn) (funcall body-fn buf))
+ (with-current-buffer buf (set-buffer-modified-p nil))
+ (kill-buffer buf))))
+ (when (file-exists-p f) (delete-file f)))))
+
+(ert-deftest test-cbf-changed-on-disk-detects ()
+ "Normal: modified buffer whose recorded modtime no longer matches is changed-on-disk."
+ (test-cbf-cod--with-visited
+ (lambda () (goto-char (point-max)) (insert "edit\n") (set-visited-file-modtime '(0 0)))
+ (lambda (buf) (should (cj/--buffer-changed-on-disk-p buf)))))
+
+(ert-deftest test-cbf-changed-on-disk-clean-modtime ()
+ "Boundary: modified buffer whose modtime still matches is not changed-on-disk."
+ (test-cbf-cod--with-visited
+ (lambda () (goto-char (point-max)) (insert "edit\n"))
+ (lambda (buf) (should-not (cj/--buffer-changed-on-disk-p buf)))))
+
+(ert-deftest test-cbf-changed-on-disk-unmodified ()
+ "Boundary: an unmodified buffer is never changed-on-disk (nothing of mine to lose)."
+ (test-cbf-cod--with-visited
+ (lambda () (set-visited-file-modtime '(0 0)))
+ (lambda (buf) (should-not (cj/--buffer-changed-on-disk-p buf)))))
+
+;;; -------------- cj/--buffer-differs-dispatch (data direction) ---------------
+;; The destructive directions, driven against real files: `save' overwrites the
+;; disk with the buffer (buffer wins); `revert' discards the buffer's edits and
+;; rereads the disk (disk wins); `clean-save' strips trailing whitespace first.
+
+(declare-function cj/--buffer-differs-dispatch "custom-buffer-file" (buffer action))
+(declare-function cj/save-buffer "custom-buffer-file" ())
+
+(defun test-cbf-disp--with-conflict (buffer-insert disk-content body-fn)
+ "Visit a temp file, BUFFER-INSERT into the buffer, overwrite the file with
+DISK-CONTENT underneath, then call BODY-FN with the buffer and file path."
+ (let ((f (make-temp-file "cbf-disp-" nil ".txt")))
+ (unwind-protect
+ (progn
+ (with-temp-file f (insert "original\n"))
+ (let ((buf (find-file-noselect f)))
+ (unwind-protect
+ (with-current-buffer buf
+ (goto-char (point-max)) (insert buffer-insert)
+ (with-temp-file f (insert disk-content))
+ (funcall body-fn buf f))
+ (with-current-buffer buf (set-buffer-modified-p nil))
+ (kill-buffer buf))))
+ (when (file-exists-p f) (delete-file f)))))
+
+(defun test-cbf-disp--disk (f)
+ "Return the on-disk contents of F."
+ (with-temp-buffer (insert-file-contents f) (buffer-string)))
+
+(ert-deftest test-cbf-buffer-differs-dispatch-save-overwrites-disk ()
+ "Normal: save writes the buffer over the disk version (buffer wins)."
+ (test-cbf-disp--with-conflict
+ "my edit\n" "disk changed underneath\n"
+ (lambda (buf f)
+ (cj/--buffer-differs-dispatch buf 'save)
+ (should-not (buffer-modified-p buf))
+ (should (string= (test-cbf-disp--disk f) "original\nmy edit\n")))))
+
+(ert-deftest test-cbf-buffer-differs-dispatch-revert-discards-edits ()
+ "Normal: revert discards the buffer's edits and rereads the disk (disk wins)."
+ (test-cbf-disp--with-conflict
+ "my edit\n" "disk version\n"
+ (lambda (buf f)
+ (cj/--buffer-differs-dispatch buf 'revert)
+ (should-not (buffer-modified-p buf))
+ (should (string= (with-current-buffer buf (buffer-string)) "disk version\n")))))
+
+(ert-deftest test-cbf-buffer-differs-dispatch-clean-save-strips-whitespace ()
+ "Normal: clean-save strips trailing whitespace, then overwrites the disk."
+ (test-cbf-disp--with-conflict
+ "edit \n" "disk changed\n"
+ (lambda (buf f)
+ (cj/--buffer-differs-dispatch buf 'clean-save)
+ (should-not (buffer-modified-p buf))
+ (should (string= (test-cbf-disp--disk f) "original\nedit\n")))))
+
+(ert-deftest test-cbf-save-buffer-fast-path-no-conflict ()
+ "Boundary: with no disk conflict, cj/save-buffer just saves (no prompt path)."
+ (let ((f (make-temp-file "cbf-fast-" nil ".txt")))
+ (unwind-protect
+ (progn
+ (with-temp-file f (insert "base\n"))
+ (let ((buf (find-file-noselect f)))
+ (unwind-protect
+ (with-current-buffer buf
+ (goto-char (point-max)) (insert "added\n")
+ (cj/save-buffer) ; modtime matches -> fast path
+ (should-not (buffer-modified-p))
+ (should (string= (test-cbf-disp--disk f) "base\nadded\n")))
+ (with-current-buffer buf (set-buffer-modified-p nil))
+ (kill-buffer buf))))
+ (when (file-exists-p f) (delete-file f)))))
+
+(provide 'test-custom-buffer-file--buffer-differs-prompt)
+;;; test-custom-buffer-file--buffer-differs-prompt.el ends here
diff --git a/tests/test-custom-buffer-file--diff-whitespace-only.el b/tests/test-custom-buffer-file--diff-whitespace-only.el
new file mode 100644
index 000000000..e792637b5
--- /dev/null
+++ b/tests/test-custom-buffer-file--diff-whitespace-only.el
@@ -0,0 +1,146 @@
+;;; test-custom-buffer-file--diff-whitespace-only.el --- whitespace-only diff detection -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--diff-whitespace-only-p, the route-1 detector behind the
+;; buffer-differs save prompt: two files differ ONLY in whitespace when a plain
+;; diff finds changes but `diff -w' (ignore all whitespace) finds none. Uses
+;; real temp files and the real diff(1) binary (a system boundary we keep), so
+;; nothing is mocked.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'custom-buffer-file)
+
+(declare-function cj/--diff-whitespace-only-p "custom-buffer-file" (file-a file-b))
+
+(defun test-cbf-ws--two-files (content-a content-b fn)
+ "Write CONTENT-A and CONTENT-B to temp files, call FN with their paths, clean up."
+ (let ((a (make-temp-file "cbf-ws-a-"))
+ (b (make-temp-file "cbf-ws-b-")))
+ (unwind-protect
+ (progn
+ (with-temp-file a (insert content-a))
+ (with-temp-file b (insert content-b))
+ (funcall fn a b))
+ (delete-file a)
+ (delete-file b))))
+
+(ert-deftest test-cbf-diff-whitespace-only-trailing ()
+ "Normal: files differing only by trailing whitespace are whitespace-only."
+ (test-cbf-ws--two-files
+ "alpha\nbeta\n" "alpha \nbeta\n"
+ (lambda (a b) (should (cj/--diff-whitespace-only-p a b)))))
+
+(ert-deftest test-cbf-diff-whitespace-only-indentation ()
+ "Normal: files differing only by leading indentation are whitespace-only."
+ (test-cbf-ws--two-files
+ "(foo)\n(bar)\n" "(foo)\n (bar)\n"
+ (lambda (a b) (should (cj/--diff-whitespace-only-p a b)))))
+
+(ert-deftest test-cbf-diff-whitespace-only-real-content ()
+ "Normal: files differing in actual content are NOT whitespace-only."
+ (test-cbf-ws--two-files
+ "alpha\nbeta\n" "alpha\nGAMMA\n"
+ (lambda (a b) (should-not (cj/--diff-whitespace-only-p a b)))))
+
+(ert-deftest test-cbf-diff-whitespace-only-identical ()
+ "Boundary: identical files do not differ at all, so not whitespace-only."
+ (test-cbf-ws--two-files
+ "alpha\nbeta\n" "alpha\nbeta\n"
+ (lambda (a b) (should-not (cj/--diff-whitespace-only-p a b)))))
+
+(ert-deftest test-cbf-diff-whitespace-only-mixed ()
+ "Boundary: whitespace change plus a real content change is NOT whitespace-only."
+ (test-cbf-ws--two-files
+ "alpha\nbeta\n" "alpha \nGAMMA\n"
+ (lambda (a b) (should-not (cj/--diff-whitespace-only-p a b)))))
+
+;;; -------------------- cj/--diff-buffer-renderer -----------------------------
+;; Which renderer the diff command uses: whitespace-only diffs go to a plain
+;; unified diff (trailing whitespace highlighted, so it is actually visible)
+;; because difftastic treats trailing-whitespace as no change and renders it
+;; blank. Real content diffs use difftastic when available, else plain diff.
+
+(declare-function cj/--diff-buffer-renderer "custom-buffer-file" (ws-only difft-available))
+
+(ert-deftest test-cbf-diff-renderer-whitespace-over-difftastic ()
+ "Normal: a whitespace-only diff uses the whitespace renderer even when difft is present."
+ (should (eq (cj/--diff-buffer-renderer t t) 'whitespace)))
+
+(ert-deftest test-cbf-diff-renderer-whitespace-no-difft ()
+ "Boundary: whitespace-only still uses the whitespace renderer without difft."
+ (should (eq (cj/--diff-buffer-renderer t nil) 'whitespace)))
+
+(ert-deftest test-cbf-diff-renderer-content-uses-difftastic ()
+ "Normal: a content diff uses difftastic when it is available."
+ (should (eq (cj/--diff-buffer-renderer nil t) 'difftastic)))
+
+(ert-deftest test-cbf-diff-renderer-content-no-difft-regular ()
+ "Boundary: a content diff falls back to the regular renderer without difft."
+ (should (eq (cj/--diff-buffer-renderer nil nil) 'regular)))
+
+;;; --------------- cj/--buffer-file-whitespace-only-p (buffer) ----------------
+;; Buffer-vs-its-file variant: writes the buffer to a temp file and reuses the
+;; detector against the buffer's visited file. Uses a real visited-file buffer.
+
+(declare-function cj/--buffer-file-whitespace-only-p "custom-buffer-file" (buffer))
+
+(defun test-cbf-ws--with-visited (disk-content edit-fn body-fn)
+ "Visit a temp file holding DISK-CONTENT, apply EDIT-FN in it, call BODY-FN with the buffer."
+ (let ((f (make-temp-file "cbf-bws-" nil ".txt")))
+ (unwind-protect
+ (progn
+ (with-temp-file f (insert disk-content))
+ (let ((buf (find-file-noselect f)))
+ (unwind-protect
+ (with-current-buffer buf
+ (funcall edit-fn)
+ (funcall body-fn buf))
+ (with-current-buffer buf (set-buffer-modified-p nil))
+ (kill-buffer buf))))
+ (when (file-exists-p f) (delete-file f)))))
+
+(ert-deftest test-cbf-buffer-file-ws-only-trailing ()
+ "Normal: an unsaved trailing-whitespace edit is whitespace-only vs the file."
+ (test-cbf-ws--with-visited
+ "alpha\nbeta\n"
+ (lambda () (goto-char (point-min)) (end-of-line) (insert " "))
+ (lambda (buf) (should (cj/--buffer-file-whitespace-only-p buf)))))
+
+(ert-deftest test-cbf-buffer-file-ws-only-content ()
+ "Normal: an unsaved content edit is NOT whitespace-only vs the file."
+ (test-cbf-ws--with-visited
+ "alpha\nbeta\n"
+ (lambda () (goto-char (point-max)) (insert "gamma\n"))
+ (lambda (buf) (should-not (cj/--buffer-file-whitespace-only-p buf)))))
+
+;;; --------- cj/diff-buffer-with-file return value (for the toggle) -----------
+
+(ert-deftest test-cbf-diff-returns-buffer-when-differs ()
+ "Normal: cj/diff-buffer-with-file returns the live diff buffer when the buffer differs."
+ (test-cbf-ws--with-visited
+ "x\ny\n"
+ (lambda () (goto-char (point-max)) (insert "ADDED\n"))
+ (lambda (buf)
+ (let ((db (with-current-buffer buf (cj/diff-buffer-with-file))))
+ (should (bufferp db))
+ (should (buffer-live-p db))))))
+
+(ert-deftest test-cbf-diff-returns-nil-when-identical ()
+ "Boundary: with no differences, cj/diff-buffer-with-file returns nil."
+ (test-cbf-ws--with-visited
+ "x\ny\n"
+ (lambda () nil)
+ (lambda (buf)
+ (should-not (with-current-buffer buf (cj/diff-buffer-with-file))))))
+
+(ert-deftest test-cbf-buffer-file-ws-only-non-file ()
+ "Boundary: a buffer not visiting a file is not whitespace-only."
+ (with-temp-buffer
+ (insert "scratch")
+ (should-not (cj/--buffer-file-whitespace-only-p (current-buffer)))))
+
+(provide 'test-custom-buffer-file--diff-whitespace-only)
+;;; test-custom-buffer-file--diff-whitespace-only.el ends here
diff --git a/tests/test-custom-buffer-file--save-some-buffers.el b/tests/test-custom-buffer-file--save-some-buffers.el
new file mode 100644
index 000000000..d4ecd318d
--- /dev/null
+++ b/tests/test-custom-buffer-file--save-some-buffers.el
@@ -0,0 +1,123 @@
+;;; test-custom-buffer-file--save-some-buffers.el --- save-loop prompt pieces -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Pure-logic tests for the read-multiple-choice save loop that replaces
+;; save-some-buffers' terse map-y-or-n-p prompt: the key->action mapping (what
+;; to do with this buffer, and how the choice steers the rest of the loop) and
+;; the labeled choice list. The interactive loop, the file saves, and the
+;; override wiring are exercised live, not here.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'custom-buffer-file)
+
+(declare-function cj/--save-some-buffers-action "custom-buffer-file" (key))
+(declare-function cj/--save-some-buffers-choices "custom-buffer-file" ())
+
+;;; --------------------- cj/--save-some-buffers-action ------------------------
+;; Each result is (THIS-ACTION . LOOP-EFFECT):
+;; THIS-ACTION ∈ save | clean-save | skip | diff
+;; LOOP-EFFECT ∈ continue | save-rest | stop | reprompt
+
+(ert-deftest test-cbf-ssb-action-save ()
+ "Normal: y saves this buffer and continues prompting."
+ (should (equal (cj/--save-some-buffers-action ?y) '(save . continue))))
+
+(ert-deftest test-cbf-ssb-action-skip ()
+ "Normal: n skips this buffer and continues."
+ (should (equal (cj/--save-some-buffers-action ?n) '(skip . continue))))
+
+(ert-deftest test-cbf-ssb-action-clean-save ()
+ "Normal: w cleans whitespace, saves this buffer, and continues."
+ (should (equal (cj/--save-some-buffers-action ?w) '(clean-save . continue))))
+
+(ert-deftest test-cbf-ssb-action-save-rest ()
+ "Boundary: ! saves this buffer and all remaining without asking."
+ (should (equal (cj/--save-some-buffers-action ?!) '(save . save-rest))))
+
+(ert-deftest test-cbf-ssb-action-save-this-stop ()
+ "Boundary: . saves this buffer and skips the rest."
+ (should (equal (cj/--save-some-buffers-action ?.) '(save . stop))))
+
+(ert-deftest test-cbf-ssb-action-quit ()
+ "Boundary: q saves no more buffers (skips this and the rest)."
+ (should (equal (cj/--save-some-buffers-action ?q) '(skip . stop))))
+
+(ert-deftest test-cbf-ssb-action-diff ()
+ "Normal: d views the diff and re-prompts rather than resolving."
+ (should (equal (cj/--save-some-buffers-action ?d) '(diff . reprompt))))
+
+(ert-deftest test-cbf-ssb-action-unknown ()
+ "Error: an unmapped key returns nil."
+ (should-not (cj/--save-some-buffers-action ?z)))
+
+;;; --------------------- cj/--save-some-buffers-choices -----------------------
+
+(ert-deftest test-cbf-ssb-choices-cover-all-keys ()
+ "Normal: the choice list offers every save-loop key, each labeled."
+ (let ((choices (cj/--save-some-buffers-choices)))
+ (dolist (key '(?y ?n ?w ?d ?! ?. ?q))
+ (let ((entry (assq key choices)))
+ (should entry)
+ ;; entry is (KEY NAME &optional DESC); NAME must be a non-empty label.
+ (should (stringp (nth 1 entry)))
+ (should (> (length (nth 1 entry)) 0))))))
+
+(ert-deftest test-cbf-ssb-choices-terse-names ()
+ "Boundary: inline names are single words so the menu takes minimum space."
+ (dolist (entry (cj/--save-some-buffers-choices))
+ (let ((name (nth 1 entry)))
+ (should (stringp name))
+ (should-not (string-match-p " " name)))))
+
+(ert-deftest test-cbf-ssb-choices-clean-mentions-whitespace ()
+ "Normal: the clean-and-save choice is labeled with whitespace."
+ (let ((entry (assq ?w (cj/--save-some-buffers-choices))))
+ (should (string-match-p "whitespace"
+ (mapconcat #'identity (cdr entry) " ")))))
+
+;;; ---------------------- cj/--save-some-buffers-plan -------------------------
+;; The pure planner: given the candidate BUFFERS and a KEY-FN that yields a
+;; (non-diff) key per buffer, resolve each to `save' / `clean-save' / `skip',
+;; honoring ! (save the rest) and . / q (stop after this). Buffers are opaque
+;; here (symbols stand in), so the planner is testable without real buffers.
+
+(declare-function cj/--save-some-buffers-plan "custom-buffer-file" (buffers key-fn))
+
+(ert-deftest test-cbf-ssb-plan-mixed ()
+ "Normal: y / n / w resolve per-buffer to save / skip / clean-save."
+ (let* ((keys '((a . ?y) (b . ?n) (c . ?w)))
+ (plan (cj/--save-some-buffers-plan
+ '(a b c) (lambda (buf) (cdr (assq buf keys))))))
+ (should (equal plan '((a . save) (b . skip) (c . clean-save))))))
+
+(ert-deftest test-cbf-ssb-plan-save-rest ()
+ "Boundary: ! saves this and all remaining without consulting KEY-FN again."
+ (let* ((asked nil)
+ (plan (cj/--save-some-buffers-plan
+ '(a b c)
+ (lambda (buf) (push buf asked) (if (eq buf 'a) ?! ?n)))))
+ (should (equal plan '((a . save) (b . save) (c . save))))
+ ;; key-fn consulted only for the first buffer; the rest ride save-all.
+ (should (equal asked '(a)))))
+
+(ert-deftest test-cbf-ssb-plan-save-this-stop ()
+ "Boundary: . saves this buffer and skips the rest."
+ (let ((plan (cj/--save-some-buffers-plan
+ '(a b c) (lambda (buf) (if (eq buf 'a) ?. ?y)))))
+ (should (equal plan '((a . save) (b . skip) (c . skip))))))
+
+(ert-deftest test-cbf-ssb-plan-quit-skips-all ()
+ "Boundary: q skips this buffer and all remaining."
+ (let ((plan (cj/--save-some-buffers-plan
+ '(a b c) (lambda (_) ?q))))
+ (should (equal plan '((a . skip) (b . skip) (c . skip))))))
+
+(ert-deftest test-cbf-ssb-plan-empty ()
+ "Boundary: no candidate buffers yields an empty plan."
+ (should-not (cj/--save-some-buffers-plan '() (lambda (_) ?y))))
+
+(provide 'test-custom-buffer-file--save-some-buffers)
+;;; test-custom-buffer-file--save-some-buffers.el ends here
diff --git a/tests/test-custom-misc-cj--count-characters.el b/tests/test-custom-counts--count-characters.el
index 1834b5c4f..8abd759f9 100644
--- a/tests/test-custom-misc-cj--count-characters.el
+++ b/tests/test-custom-counts--count-characters.el
@@ -1,7 +1,7 @@
-;;; test-custom-misc-cj--count-characters.el --- Tests for cj/--count-characters -*- lexical-binding: t; -*-
+;;; test-custom-counts--count-characters.el --- Tests for cj/--count-characters -*- lexical-binding: t; -*-
;;; Commentary:
-;; Tests for the cj/--count-characters internal implementation function from custom-misc.el
+;; Tests for the cj/--count-characters internal implementation function from custom-counts.el
;;
;; This internal function counts characters between START and END positions.
;; It validates that START is not greater than END and returns the character count.
@@ -18,7 +18,7 @@
"Stub keymap for testing.")
;; Now load the actual production module
-(require 'custom-misc)
+(require 'custom-counts)
;;; Setup and Teardown
@@ -34,7 +34,7 @@
;;; Normal Cases
-(ert-deftest test-custom-misc-cj--count-characters-normal-simple-text-returns-count ()
+(ert-deftest test-custom-counts--count-characters-normal-simple-text-returns-count ()
"Should count characters in simple text region."
(test-count-characters-setup)
(unwind-protect
@@ -44,7 +44,7 @@
(should (= result 13))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-normal-partial-region-returns-count ()
+(ert-deftest test-custom-counts--count-characters-normal-partial-region-returns-count ()
"Should count characters in partial region."
(test-count-characters-setup)
(unwind-protect
@@ -54,7 +54,7 @@
(should (= result 5))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-normal-multiline-returns-count ()
+(ert-deftest test-custom-counts--count-characters-normal-multiline-returns-count ()
"Should count characters including newlines."
(test-count-characters-setup)
(unwind-protect
@@ -67,7 +67,7 @@
;;; Boundary Cases
-(ert-deftest test-custom-misc-cj--count-characters-boundary-empty-region-returns-zero ()
+(ert-deftest test-custom-counts--count-characters-boundary-empty-region-returns-zero ()
"Should return 0 for empty region (start equals end)."
(test-count-characters-setup)
(unwind-protect
@@ -77,7 +77,7 @@
(should (= result 0))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-boundary-single-character-returns-one ()
+(ert-deftest test-custom-counts--count-characters-boundary-single-character-returns-one ()
"Should return 1 for single character region."
(test-count-characters-setup)
(unwind-protect
@@ -87,7 +87,7 @@
(should (= result 1))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-boundary-large-region-returns-count ()
+(ert-deftest test-custom-counts--count-characters-boundary-large-region-returns-count ()
"Should handle very large region."
(test-count-characters-setup)
(unwind-protect
@@ -98,7 +98,7 @@
(should (= result 100000)))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-boundary-unicode-returns-count ()
+(ert-deftest test-custom-counts--count-characters-boundary-unicode-returns-count ()
"Should count unicode characters (emoji, RTL text, combining characters)."
(test-count-characters-setup)
(unwind-protect
@@ -110,7 +110,7 @@
(should (= result (- (point-max) (point-min))))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-boundary-whitespace-only-returns-count ()
+(ert-deftest test-custom-counts--count-characters-boundary-whitespace-only-returns-count ()
"Should count whitespace characters."
(test-count-characters-setup)
(unwind-protect
@@ -121,7 +121,7 @@
(should (= result 7))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-boundary-newlines-at-boundaries-returns-count ()
+(ert-deftest test-custom-counts--count-characters-boundary-newlines-at-boundaries-returns-count ()
"Should count newlines at start and end."
(test-count-characters-setup)
(unwind-protect
@@ -132,7 +132,7 @@
(should (= result 9))))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-boundary-binary-content-returns-count ()
+(ert-deftest test-custom-counts--count-characters-boundary-binary-content-returns-count ()
"Should handle binary content."
(test-count-characters-setup)
(unwind-protect
@@ -144,7 +144,7 @@
;;; Error Cases
-(ert-deftest test-custom-misc-cj--count-characters-error-start-greater-than-end-signals-error ()
+(ert-deftest test-custom-counts--count-characters-error-start-greater-than-end-signals-error ()
"Should signal error when start is greater than end."
(test-count-characters-setup)
(unwind-protect
@@ -154,7 +154,7 @@
:type 'error))
(test-count-characters-teardown)))
-(ert-deftest test-custom-misc-cj--count-characters-error-positions-out-of-bounds-handled ()
+(ert-deftest test-custom-counts--count-characters-error-positions-out-of-bounds-handled ()
"Should handle positions beyond buffer bounds (Emacs handles this)."
(test-count-characters-setup)
(unwind-protect
@@ -167,5 +167,5 @@
(should (= result 5))))
(test-count-characters-teardown)))
-(provide 'test-custom-misc-cj--count-characters)
-;;; test-custom-misc-cj--count-characters.el ends here
+(provide 'test-custom-counts--count-characters)
+;;; test-custom-counts--count-characters.el ends here
diff --git a/tests/test-custom-misc-cj-count-characters-buffer-or-region.el b/tests/test-custom-counts-count-characters-buffer-or-region.el
index dbbda00d8..adeb812a8 100644
--- a/tests/test-custom-misc-cj-count-characters-buffer-or-region.el
+++ b/tests/test-custom-counts-count-characters-buffer-or-region.el
@@ -1,7 +1,7 @@
-;;; test-custom-misc-cj-count-characters-buffer-or-region.el --- Tests for cj/count-characters-buffer-or-region -*- lexical-binding: t; -*-
+;;; test-custom-counts-count-characters-buffer-or-region.el --- Tests for cj/count-characters-buffer-or-region -*- lexical-binding: t; -*-
;;; Commentary:
-;; Tests for the cj/count-characters-buffer-or-region function from custom-misc.el
+;; Tests for the cj/count-characters-buffer-or-region function from custom-counts.el
;;
;; This function counts characters in the active region or the entire buffer
;; if no region is active. It displays the count in the minibuffer.
@@ -18,7 +18,7 @@
"Stub keymap for testing.")
;; Now load the actual production module
-(require 'custom-misc)
+(require 'custom-counts)
;;; Setup and Teardown
@@ -35,7 +35,7 @@
;;; Normal Cases
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-normal-whole-buffer-counts-all ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-normal-whole-buffer-counts-all ()
"Should count all characters in buffer when no region is active."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -51,7 +51,7 @@
(should (string-match-p "13 characters.*buffer" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-normal-active-region-counts-region ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-normal-active-region-counts-region ()
"Should count characters in active region."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -70,7 +70,7 @@
(should (string-match-p "5 characters.*region" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-normal-multiline-buffer-counts-all ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-normal-multiline-buffer-counts-all ()
"Should count characters including newlines in buffer."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -86,7 +86,7 @@
(should (string-match-p "20 characters.*buffer" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-normal-multiline-region-counts-region ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-normal-multiline-region-counts-region ()
"Should count characters including newlines in region."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -108,7 +108,7 @@
;;; Boundary Cases
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-boundary-empty-buffer-returns-zero ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-boundary-empty-buffer-returns-zero ()
"Should return 0 for empty buffer."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -122,7 +122,7 @@
(should (string-match-p "0 characters.*buffer" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-boundary-empty-region-counts-buffer ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-boundary-empty-region-counts-buffer ()
"Should count whole buffer when region is empty (point equals mark).
When mark and point are at the same position, use-region-p returns nil,
so the function correctly falls back to counting the entire buffer."
@@ -144,7 +144,7 @@ so the function correctly falls back to counting the entire buffer."
(should (string-match-p "13 characters.*buffer" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-boundary-large-buffer-counts-all ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-boundary-large-buffer-counts-all ()
"Should handle very large buffer."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -160,7 +160,7 @@ so the function correctly falls back to counting the entire buffer."
(should (string-match-p "100000 characters.*buffer" message-output))))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-boundary-unicode-counts-correctly ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-boundary-unicode-counts-correctly ()
"Should count unicode characters (emoji, RTL text) correctly."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -177,7 +177,7 @@ so the function correctly falls back to counting the entire buffer."
message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-boundary-whitespace-only-counts-whitespace ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-boundary-whitespace-only-counts-whitespace ()
"Should count whitespace characters."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -193,7 +193,7 @@ so the function correctly falls back to counting the entire buffer."
(should (string-match-p "7 characters.*buffer" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-boundary-single-character-returns-one ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-boundary-single-character-returns-one ()
"Should return 1 for single character buffer."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -208,7 +208,7 @@ so the function correctly falls back to counting the entire buffer."
(should (string-match-p "1 character.*buffer" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(ert-deftest test-custom-misc-cj-count-characters-buffer-or-region-boundary-narrowed-buffer-counts-visible ()
+(ert-deftest test-custom-counts-count-characters-buffer-or-region-boundary-narrowed-buffer-counts-visible ()
"Should count only visible characters in narrowed buffer."
(test-count-characters-buffer-or-region-setup)
(unwind-protect
@@ -227,5 +227,5 @@ so the function correctly falls back to counting the entire buffer."
(should (string-match-p "7 characters.*buffer" message-output)))))
(test-count-characters-buffer-or-region-teardown)))
-(provide 'test-custom-misc-cj-count-characters-buffer-or-region)
-;;; test-custom-misc-cj-count-characters-buffer-or-region.el ends here
+(provide 'test-custom-counts-count-characters-buffer-or-region)
+;;; test-custom-counts-count-characters-buffer-or-region.el ends here
diff --git a/tests/test-custom-misc-count-words.el b/tests/test-custom-counts-count-words.el
index f2bf793f4..642a5a411 100644
--- a/tests/test-custom-misc-count-words.el
+++ b/tests/test-custom-counts-count-words.el
@@ -1,7 +1,7 @@
-;;; test-custom-misc-count-words.el --- Tests for cj/--count-words -*- lexical-binding: t; -*-
+;;; test-custom-counts-count-words.el --- Tests for cj/--count-words -*- lexical-binding: t; -*-
;;; Commentary:
-;; Tests for the cj/--count-words function from custom-misc.el
+;; Tests for the cj/--count-words function from custom-counts.el
;;
;; This function counts words in a region using Emacs's built-in count-words.
;; A word is defined by Emacs's word boundaries, which generally means
@@ -24,7 +24,7 @@
"Stub keymap for testing.")
;; Now load the actual production module
-(require 'custom-misc)
+(require 'custom-counts)
;;; Test Helpers
@@ -144,5 +144,5 @@ words in it."))
(let ((end (match-end 0)))
(should (= 3 (cj/--count-words start end)))))))
-(provide 'test-custom-misc-count-words)
-;;; test-custom-misc-count-words.el ends here
+(provide 'test-custom-counts-count-words)
+;;; test-custom-counts-count-words.el ends here
diff --git a/tests/test-custom-misc-format-region.el b/tests/test-custom-format-format-region.el
index c40a8898e..27f1c6b99 100644
--- a/tests/test-custom-misc-format-region.el
+++ b/tests/test-custom-format-format-region.el
@@ -1,7 +1,7 @@
-;;; test-custom-misc-format-region.el --- Tests for cj/--format-region -*- lexical-binding: t; -*-
+;;; test-custom-format-format-region.el --- Tests for cj/--format-region -*- lexical-binding: t; -*-
;;; Commentary:
-;; Tests for the cj/--format-region function from custom-misc.el
+;; Tests for the cj/--format-region function from custom-format.el
;;
;; This function reformats text by applying three operations:
;; 1. untabify - converts tabs to spaces
@@ -28,7 +28,7 @@
"Stub keymap for testing.")
;; Now load the actual production module
-(require 'custom-misc)
+(require 'custom-format)
;;; Test Helpers
@@ -157,5 +157,5 @@ Returns the buffer string after operation."
;; Should complete without error
(should (string= (buffer-string) "hello world")))))
-(provide 'test-custom-misc-format-region)
-;;; test-custom-misc-format-region.el ends here
+(provide 'test-custom-format-format-region)
+;;; test-custom-format-format-region.el ends here
diff --git a/tests/test-custom-misc-jump-to-matching-paren.el b/tests/test-custom-line-paragraph-jump-to-matching-paren.el
index 973b6dfa9..31853da67 100644
--- a/tests/test-custom-misc-jump-to-matching-paren.el
+++ b/tests/test-custom-line-paragraph-jump-to-matching-paren.el
@@ -1,7 +1,7 @@
-;;; test-custom-misc-jump-to-matching-paren.el --- Tests for cj/jump-to-matching-paren -*- lexical-binding: t; -*-
+;;; test-custom-line-paragraph-jump-to-matching-paren.el --- Tests for cj/jump-to-matching-paren -*- lexical-binding: t; -*-
;;; Commentary:
-;; Tests for the cj/jump-to-matching-paren function from custom-misc.el
+;; Tests for the cj/jump-to-matching-paren function from custom-line-paragraph.el
;;
;; This function jumps to matching delimiters using Emacs's sexp navigation.
;; It works with any delimiter that has matching syntax according to the
@@ -32,7 +32,7 @@
"Stub keymap for testing.")
;; Now load the actual production module
-(require 'custom-misc)
+(require 'custom-line-paragraph)
;;; Test Helpers
@@ -193,5 +193,5 @@ POINT-POSITION is 1-indexed (1 = first character)."
;; The parens in the string should be ignored
(should (= 18 (test-jump-to-matching-paren "(\"hello (world)\")" 1))))
-(provide 'test-custom-misc-jump-to-matching-paren)
-;;; test-custom-misc-jump-to-matching-paren.el ends here
+(provide 'test-custom-line-paragraph-jump-to-matching-paren)
+;;; test-custom-line-paragraph-jump-to-matching-paren.el ends here
diff --git a/tests/test-custom-misc-replace-fraction-glyphs.el b/tests/test-custom-text-transform-replace-fraction-glyphs.el
index 81d1546e1..ed961c63e 100644
--- a/tests/test-custom-misc-replace-fraction-glyphs.el
+++ b/tests/test-custom-text-transform-replace-fraction-glyphs.el
@@ -1,7 +1,7 @@
-;;; test-custom-misc-replace-fraction-glyphs.el --- Tests for cj/--replace-fraction-glyphs -*- lexical-binding: t; -*-
+;;; test-custom-text-transform-replace-fraction-glyphs.el --- Tests for cj/--replace-fraction-glyphs -*- lexical-binding: t; -*-
;;; Commentary:
-;; Tests for the cj/--replace-fraction-glyphs function from custom-misc.el
+;; Tests for the cj/--replace-fraction-glyphs function from custom-text-transform.el
;;
;; This function bidirectionally converts between text fractions (1/4) and
;; Unicode fraction glyphs (¼). It supports 5 common fractions:
@@ -28,7 +28,7 @@
"Stub keymap for testing.")
;; Now load the actual production module
-(require 'custom-misc)
+(require 'custom-text-transform)
;;; Test Helpers
@@ -181,5 +181,5 @@ Returns the buffer string after operation."
;; Should complete without error
(should (string= (buffer-string) "1/4")))))
-(provide 'test-custom-misc-replace-fraction-glyphs)
-;;; test-custom-misc-replace-fraction-glyphs.el ends here
+(provide 'test-custom-text-transform-replace-fraction-glyphs)
+;;; test-custom-text-transform-replace-fraction-glyphs.el ends here
diff --git a/tests/test-dashboard-config-launchers.el b/tests/test-dashboard-config-launchers.el
index a9a871979..53c46caa9 100644
--- a/tests/test-dashboard-config-launchers.el
+++ b/tests/test-dashboard-config-launchers.el
@@ -27,19 +27,20 @@
;; Telegram moved from "g" to "G" so "g" is free for dashboard refresh.
;; Signal ("S") added as the 14th launcher.
-(defconst test-dash--keys '("c" "d" "t" "a" "r" "b" "f" "m" "e" "i" "G" "s" "l" "S"))
+;; Weather ("w") added after Agenda as the 15th launcher (top-row daily glance).
+(defconst test-dash--keys '("c" "d" "t" "a" "w" "r" "b" "f" "m" "e" "i" "G" "s" "l" "S"))
;; ----------------------------- launcher table --------------------------------
(ert-deftest test-dashboard-launchers-keys-in-order ()
- "Normal: 14 launchers with the expected keys in display order."
- (should (= 14 (length cj/dashboard--launchers)))
+ "Normal: 15 launchers with the expected keys in display order."
+ (should (= 15 (length cj/dashboard--launchers)))
(should (equal test-dash--keys (mapcar (lambda (l) (nth 0 l)) cj/dashboard--launchers))))
(ert-deftest test-dashboard-launchers-labels-in-order ()
"Normal: labels in display order (Telegram and Slack reordered so Slack sits
next to Linear on the last navigator row)."
- (should (equal '("Code" "Files" "Terminal" "Agenda" "Feeds" "Books"
+ (should (equal '("Code" "Files" "Terminal" "Agenda" "Weather" "Feeds" "Books"
"Flashcards" "Music" "Email" "IRC" "Telegram" "Slack" "Linear" "Signal")
(mapcar (lambda (l) (nth 3 l)) cj/dashboard--launchers))))
@@ -50,18 +51,19 @@ next to Linear on the last navigator row)."
;; --------------------------- navigator rows ----------------------------------
-(ert-deftest test-dashboard-navigator-rows-grouped-4-4-3-3 ()
- "Normal: navigator derives rows per `cj/dashboard--row-sizes' (4 4 3 3), with
-Slack, Linear, and Signal sharing the last row."
+(ert-deftest test-dashboard-navigator-rows-grouped-5-4-3-3 ()
+ "Normal: navigator derives rows per `cj/dashboard--row-sizes' (5 4 3 3), with
+Weather joining the top row and Slack, Linear, and Signal sharing the last row."
(cl-letf (((symbol-function 'nerd-icons-faicon) (lambda (n &rest _) (concat "I:" n)))
((symbol-function 'nerd-icons-devicon) (lambda (n &rest _) (concat "I:" n)))
((symbol-function 'nerd-icons-mdicon) (lambda (n &rest _) (concat "I:" n)))
((symbol-function 'nerd-icons-octicon) (lambda (n &rest _) (concat "I:" n)))
- ((symbol-function 'nerd-icons-codicon) (lambda (n &rest _) (concat "I:" n))))
+ ((symbol-function 'nerd-icons-codicon) (lambda (n &rest _) (concat "I:" n)))
+ ((symbol-function 'nerd-icons-wicon) (lambda (n &rest _) (concat "I:" n))))
(let ((rows (cj/dashboard--navigator-rows)))
(should (= 4 (length rows)))
- (should (equal '(4 4 3 3) (mapcar #'length rows)))
- (should (equal '("Code" "Files" "Terminal" "Agenda")
+ (should (equal '(5 4 3 3) (mapcar #'length rows)))
+ (should (equal '("Code" "Files" "Terminal" "Agenda" "Weather")
(mapcar (lambda (b) (nth 1 b)) (nth 0 rows))))
(should (equal '("Slack" "Linear" "Signal")
(mapcar (lambda (b) (nth 1 b)) (nth 3 rows))))
@@ -86,7 +88,7 @@ Slack, Linear, and Signal sharing the last row."
(let ((map (make-sparse-keymap)) (calls nil))
(cl-letf (((symbol-function 'projectile-switch-project) (lambda (&rest _) (push 'code calls)))
((symbol-function 'dirvish) (lambda (&rest _) (push 'files calls)))
- ((symbol-function 'ghostel) (lambda (&rest _) (push 'term calls)))
+ ((symbol-function 'cj/term-toggle) (lambda (&rest _) (push 'term calls)))
((symbol-function 'cj/main-agenda-display) (lambda (&rest _) (push 'agenda calls)))
((symbol-function 'cj/elfeed-open) (lambda (&rest _) (push 'feeds calls)))
((symbol-function 'calibredb) (lambda (&rest _) (push 'books calls)))
@@ -98,7 +100,10 @@ Slack, Linear, and Signal sharing the last row."
((symbol-function 'cj/slack-start) (lambda (&rest _) (push 'slack calls)))
((symbol-function 'cj/telega) (lambda (&rest _) (push 'tg calls)))
((symbol-function 'pearl-list-issues) (lambda (&rest _) (push 'linear calls)))
- ((symbol-function 'cj/signel-message) (lambda (&rest _) (push 'signal calls))))
+ ((symbol-function 'cj/signel-message) (lambda (&rest _) (push 'signal calls)))
+ ;; wttrin is invoked via `call-interactively', so the stub must be
+ ;; a command -- a plain variadic lambda masked the real arity bug.
+ ((symbol-function 'wttrin) (lambda (&rest _) (interactive) (push 'weather calls))))
(cj/dashboard--bind-launchers map)
(dolist (key test-dash--keys)
(call-interactively (keymap-lookup map key)))
@@ -108,7 +113,8 @@ Slack, Linear, and Signal sharing the last row."
(should (memq 'm-toggle calls))
(should (memq 'm-load calls))
(should (memq 'signal calls))
- (should (= 15 (length calls)))))) ; 14 keys, Music fires two
+ (should (memq 'weather calls))
+ (should (= 16 (length calls)))))) ; 15 keys, Music fires two
(provide 'test-dashboard-config-launchers)
;;; test-dashboard-config-launchers.el ends here
diff --git a/tests/test-dirvish-config--dired-keys.el b/tests/test-dirvish-config--dired-keys.el
new file mode 100644
index 000000000..2df0e8db6
--- /dev/null
+++ b/tests/test-dirvish-config--dired-keys.el
@@ -0,0 +1,23 @@
+;;; test-dirvish-config--dired-keys.el --- dired d=diff / D=delete bindings -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Regression: d and D in dired (and dirvish, which uses dired-mode-map) are the
+;; diff and delete pair, matching the convention under C-; b and in ibuffer. A
+;; mismatch -- or a swapped which-key label -- once led to deleting a file while
+;; trying to diff it.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'dired)
+(require 'dirvish-config)
+
+(ert-deftest test-dirvish-dired-d-diffs-D-deletes ()
+ "Normal: dired d runs the ediff diff and D deletes, matching the d=diff /
+D=delete convention used under C-; b and in ibuffer."
+ (should (eq (keymap-lookup dired-mode-map "d") #'cj/dired-ediff-files))
+ (should (eq (keymap-lookup dired-mode-map "D") #'dired-do-delete)))
+
+(provide 'test-dirvish-config--dired-keys)
+;;; test-dirvish-config--dired-keys.el ends here
diff --git a/tests/test-eshell-config--prompt.el b/tests/test-eshell-config--prompt.el
new file mode 100644
index 000000000..7073c7e0b
--- /dev/null
+++ b/tests/test-eshell-config--prompt.el
@@ -0,0 +1,75 @@
+;;; test-eshell-config--prompt.el --- Tests for eshell prompt helpers -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the pure prompt-segment helpers added for zsh parity: the
+;; .git/HEAD branch reader and the exit-status segment.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'eshell-config)
+
+(defvar eshell-last-command-status) ; declared special for the status tests
+
+;;; cj/--eshell-git-branch
+
+(ert-deftest test-eshell-git-branch-reads-head ()
+ "Normal: a .git/HEAD pointing at a branch returns the branch name."
+ (let ((dir (make-temp-file "esh-git-" t)))
+ (unwind-protect
+ (progn
+ (make-directory (expand-file-name ".git" dir))
+ (with-temp-file (expand-file-name ".git/HEAD" dir)
+ (insert "ref: refs/heads/feature-x\n"))
+ (let ((default-directory (file-name-as-directory dir)))
+ (should (equal (cj/--eshell-git-branch) "feature-x"))))
+ (delete-directory dir t))))
+
+(ert-deftest test-eshell-git-branch-no-repo-nil ()
+ "Boundary: a directory with no .git returns nil."
+ (let ((dir (make-temp-file "esh-nogit-" t)))
+ (unwind-protect
+ (let ((default-directory (file-name-as-directory dir)))
+ (should-not (cj/--eshell-git-branch)))
+ (delete-directory dir t))))
+
+(ert-deftest test-eshell-git-branch-detached-nil ()
+ "Boundary: a detached HEAD (a raw SHA, no ref) returns nil."
+ (let ((dir (make-temp-file "esh-detached-" t)))
+ (unwind-protect
+ (progn
+ (make-directory (expand-file-name ".git" dir))
+ (with-temp-file (expand-file-name ".git/HEAD" dir)
+ (insert "a1b2c3d4e5f6\n"))
+ (let ((default-directory (file-name-as-directory dir)))
+ (should-not (cj/--eshell-git-branch))))
+ (delete-directory dir t))))
+
+(ert-deftest test-eshell-git-branch-remote-skipped ()
+ "Boundary: a remote default-directory is skipped (no TRAMP read)."
+ (let ((default-directory "/ssh:host:/some/path/"))
+ (should-not (cj/--eshell-git-branch))))
+
+;;; cj/--eshell-prompt-status-segment
+
+(ert-deftest test-eshell-prompt-status-zero-empty ()
+ "Normal: a zero exit status yields an empty segment."
+ (let ((eshell-last-command-status 0))
+ (should (equal (cj/--eshell-prompt-status-segment) ""))))
+
+(ert-deftest test-eshell-prompt-status-nonzero-bracketed ()
+ "Normal: a non-zero exit status is shown in brackets."
+ (let ((eshell-last-command-status 1))
+ (should (equal (cj/--eshell-prompt-status-segment) " [1]")))
+ (let ((eshell-last-command-status 130))
+ (should (equal (cj/--eshell-prompt-status-segment) " [130]"))))
+
+(ert-deftest test-eshell-prompt-status-unset-empty ()
+ "Boundary: an unset status yields an empty segment, no error."
+ (let ((eshell-last-command-status nil))
+ (should (equal (cj/--eshell-prompt-status-segment) ""))))
+
+(provide 'test-eshell-config--prompt)
+;;; test-eshell-config--prompt.el ends here
diff --git a/tests/test-external-open-commands.el b/tests/test-external-open-commands.el
index c0c83a340..3d8adc15e 100644
--- a/tests/test-external-open-commands.el
+++ b/tests/test-external-open-commands.el
@@ -81,8 +81,9 @@
;;; cj/find-file-auto
(ert-deftest test-external-open-find-file-auto-routes-media-externally ()
- "Normal: a `.mp4' filename (in `default-open-extensions') triggers
-`cj/xdg-open' instead of the original `find-file'."
+ "Normal: a non-video external extension (`.docx', in
+`default-open-extensions') triggers `cj/xdg-open' instead of the original
+`find-file'."
(let ((opened nil)
(orig-called nil))
(cl-letf (((symbol-function 'cj/xdg-open)
@@ -90,8 +91,23 @@
;; orig-fun replacement -- shouldn't run for a routed extension.
((symbol-function 'cj/find-file-auto--orig-stub)
(lambda (&rest _) (setq orig-called t))))
- (cj/find-file-auto #'cj/find-file-auto--orig-stub "/tmp/video.mp4"))
- (should (equal opened "/tmp/video.mp4"))
+ (cj/find-file-auto #'cj/find-file-auto--orig-stub "/tmp/report.docx"))
+ (should (equal opened "/tmp/report.docx"))
+ (should-not orig-called)))
+
+(ert-deftest test-external-open-find-file-auto-routes-video-to-looping-player ()
+ "Normal: a video filename triggers `cj/open-video-looping', not `cj/xdg-open'
+or the original `find-file'."
+ (let ((looped nil) (xdg nil) (orig-called nil))
+ (cl-letf (((symbol-function 'cj/open-video-looping)
+ (lambda (file) (setq looped file)))
+ ((symbol-function 'cj/xdg-open)
+ (lambda (_) (setq xdg t)))
+ ((symbol-function 'cj/find-file-auto--orig-stub)
+ (lambda (&rest _) (setq orig-called t))))
+ (cj/find-file-auto #'cj/find-file-auto--orig-stub "/tmp/clip.mp4"))
+ (should (equal looped "/tmp/clip.mp4"))
+ (should-not xdg)
(should-not orig-called)))
(ert-deftest test-external-open-find-file-auto-passes-through-text-files ()
@@ -116,5 +132,66 @@
(cj/find-file-auto #'cj/find-file-auto--orig-stub nil))
(should orig-called)))
+;;; cj/--video-file-p
+
+(ert-deftest test-external-open-video-file-p-matches-video ()
+ "Normal: common video extensions match, case-insensitively."
+ (should (cj/--video-file-p "/tmp/a.mp4"))
+ (should (cj/--video-file-p "/tmp/a.mkv"))
+ (should (cj/--video-file-p "/tmp/a.webm"))
+ (should (cj/--video-file-p "/tmp/A.MP4")))
+
+(ert-deftest test-external-open-video-file-p-rejects-non-video ()
+ "Boundary: audio, docs, and nil do not match."
+ (should-not (cj/--video-file-p "/tmp/a.mp3"))
+ (should-not (cj/--video-file-p "/tmp/a.txt"))
+ (should-not (cj/--video-file-p "/tmp/a.docx"))
+ (should-not (cj/--video-file-p nil)))
+
+;;; cj/--video-open-arglist
+
+(ert-deftest test-external-open-video-arglist-appends-file-after-args ()
+ "Normal: the player args precede the file in the argument list."
+ (let ((cj/video-open-args '("--loop-file=inf")))
+ (should (equal (cj/--video-open-arglist "/tmp/a.mp4")
+ '("--loop-file=inf" "/tmp/a.mp4")))))
+
+(ert-deftest test-external-open-video-arglist-respects-custom-args ()
+ "Boundary: custom args are honored; empty args yields just the file."
+ (let ((cj/video-open-args '("--loop=inf" "--mute=yes")))
+ (should (equal (cj/--video-open-arglist "/tmp/a.mkv")
+ '("--loop=inf" "--mute=yes" "/tmp/a.mkv"))))
+ (let ((cj/video-open-args nil))
+ (should (equal (cj/--video-open-arglist "/tmp/a.mkv") '("/tmp/a.mkv")))))
+
+;;; cj/open-video-looping
+
+(ert-deftest test-external-open-video-looping-calls-player-with-loop-args ()
+ "Normal: posix path calls the player with loop args + file, async (no wait)."
+ (let ((tmp (make-temp-file "test-ext-video-" nil ".mp4"))
+ (call nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'env-windows-p) (lambda () nil))
+ ((symbol-function 'call-process)
+ (lambda (prog _infile dest _disp &rest args)
+ (setq call (list prog dest args))
+ 0)))
+ (let ((cj/video-open-command "mpv")
+ (cj/video-open-args '("--loop-file=inf")))
+ (cj/open-video-looping tmp)))
+ (delete-file tmp))
+ (should (equal (nth 0 call) "mpv"))
+ (should (equal (nth 1 call) 0)) ; async destination: don't wait
+ (should (member "--loop-file=inf" (nth 2 call)))
+ (should (cl-find-if (lambda (a) (and (stringp a)
+ (string-match-p "\\.mp4\\'" a)))
+ (nth 2 call)))))
+
+(ert-deftest test-external-open-video-looping-errors-when-no-file ()
+ "Error: a buffer with no associated file signals user-error."
+ (with-temp-buffer
+ (cl-letf (((symbol-function 'cj/file-from-context) (lambda (_) nil)))
+ (should-error (cj/open-video-looping) :type 'user-error))))
+
(provide 'test-external-open-commands)
;;; test-external-open-commands.el ends here
diff --git a/tests/test-font-config--frame-lifecycle.el b/tests/test-font-config--frame-lifecycle.el
index 826edbd69..8f338b996 100644
--- a/tests/test-font-config--frame-lifecycle.el
+++ b/tests/test-font-config--frame-lifecycle.el
@@ -2,7 +2,7 @@
;;; Commentary:
;; cj/apply-font-settings-to-frame, cj/cleanup-frame-list, and
-;; cj/maybe-install-all-the-icons-fonts were defined inside use-package
+;; cj/maybe-install-nerd-icons-fonts were defined inside use-package
;; :config / with-eval-after-load (unreachable under `make test'). Lifting
;; them to top level makes their branching unit-testable; env-gui-p and the
;; package side-effect calls are mocked at the boundary.
@@ -57,9 +57,9 @@
(let ((installed nil))
(cl-letf (((symbol-function 'env-gui-p) (lambda () t))
((symbol-function 'cj/font-installed-p) (lambda (_n) nil))
- ((symbol-function 'all-the-icons-install-fonts) (lambda (&rest _) (setq installed t)))
+ ((symbol-function 'nerd-icons-install-fonts) (lambda (&rest _) (setq installed t)))
((symbol-function 'remove-hook) #'ignore))
- (cj/maybe-install-all-the-icons-fonts))
+ (cj/maybe-install-nerd-icons-fonts))
(should installed)))
(ert-deftest test-font-maybe-install-icons-already-present-skips ()
@@ -67,8 +67,8 @@
(let ((installed nil))
(cl-letf (((symbol-function 'env-gui-p) (lambda () t))
((symbol-function 'cj/font-installed-p) (lambda (_n) t))
- ((symbol-function 'all-the-icons-install-fonts) (lambda (&rest _) (setq installed t))))
- (cj/maybe-install-all-the-icons-fonts))
+ ((symbol-function 'nerd-icons-install-fonts) (lambda (&rest _) (setq installed t))))
+ (cj/maybe-install-nerd-icons-fonts))
(should-not installed)))
(provide 'test-font-config--frame-lifecycle)
diff --git a/tests/test-font-config.el b/tests/test-font-config.el
index 8fada25e2..393a77584 100644
--- a/tests/test-font-config.el
+++ b/tests/test-font-config.el
@@ -5,9 +5,10 @@
;; font-config.el is mostly top-level font/package setup. These smoke tests
;; cover the logic that should stay correct regardless of which fonts are
;; installed: the install check, and the daemon-frame font applier (env-gui-p
-;; guard plus idempotency). The module :demand's fontaine and all-the-icons,
-;; so the tests skip when those packages are absent rather than failing on a
-;; bare checkout. GUI and font lookups are stubbed so the run stays headless.
+;; guard plus idempotency). The module :demand's fontaine and references
+;; nerd-icons, so the tests skip when those packages are absent rather than
+;; failing on a bare checkout. GUI and font lookups are stubbed so the run
+;; stays headless.
;;; Code:
@@ -21,9 +22,8 @@
(defconst test-font-config--available
(and (locate-library "fontaine")
- (locate-library "all-the-icons")
- (locate-library "all-the-icons-nerd-fonts"))
- "Non-nil when the packages font-config :demand's are loadable.")
+ (locate-library "nerd-icons"))
+ "Non-nil when the packages font-config needs are loadable.")
;;; cj/font-installed-p
diff --git a/tests/test-init-module-headers.el b/tests/test-init-module-headers.el
index 478819b89..f395fd71f 100644
--- a/tests/test-init-module-headers.el
+++ b/tests/test-init-module-headers.el
@@ -35,7 +35,9 @@
"custom-datetime"
"custom-buffer-file"
"custom-line-paragraph"
- "custom-misc"
+ "custom-counts"
+ "custom-format"
+ "custom-text-transform"
"custom-ordering"
"custom-text-enclose"
"custom-whitespace"
@@ -97,6 +99,10 @@
"ai-term"
"browser-config"
"calendar-sync"
+ "calendar-sync-ics"
+ "calendar-sync-recurrence"
+ "calendar-sync-org"
+ "calendar-sync-source"
"calibredb-epub-config"
"chrono-tools"
"dirvish-config"
@@ -129,7 +135,8 @@
"tramp-config"
"transcription-config"
"video-audio-recording"
- "term-config"
+ "video-audio-recording-devices"
+ "video-audio-recording-capture"
"weather-config"
"wrap-up")
"Modules annotated with the load-graph header contract.
diff --git a/tests/test-local-repository--car-member.el b/tests/test-local-repository--car-member.el
index 8b8c9a7db..30ae58c6b 100644
--- a/tests/test-local-repository--car-member.el
+++ b/tests/test-local-repository--car-member.el
@@ -1,7 +1,7 @@
-;;; test-local-repository--car-member.el --- Tests for car-member -*- lexical-binding: t -*-
+;;; test-local-repository--car-member.el --- Tests for localrepo--car-member -*- lexical-binding: t -*-
;;; Commentary:
-;; Tests for `car-member' in local-repository.el — the predicate
+;; Tests for `localrepo--car-member' in local-repository.el — the predicate
;; localrepo-initialize uses to check whether an archive id is already
;; registered in package-archives / package-archive-priorities.
@@ -12,47 +12,47 @@
;;; Normal Cases
-(ert-deftest test-local-repository-car-member-found ()
+(ert-deftest test-local-repository-localrepo--car-member-found ()
"Normal: VALUE present as a car returns the matching tail (non-nil)."
- (should (equal (car-member 'b '((a . 1) (b . 2) (c . 3)))
+ (should (equal (localrepo--car-member 'b '((a . 1) (b . 2) (c . 3)))
'(b c))))
-(ert-deftest test-local-repository-car-member-not-found ()
+(ert-deftest test-local-repository-localrepo--car-member-not-found ()
"Normal: VALUE absent from every car returns nil."
- (should-not (car-member 'z '((a . 1) (b . 2)))))
+ (should-not (localrepo--car-member 'z '((a . 1) (b . 2)))))
-(ert-deftest test-local-repository-car-member-string-car ()
+(ert-deftest test-local-repository-localrepo--car-member-string-car ()
"Normal: car comparison uses `equal', so string keys match by value."
- (should (car-member "localrepo"
+ (should (localrepo--car-member "localrepo"
'(("gnu" . "url1") ("localrepo" . "url2")))))
;;; Boundary Cases
-(ert-deftest test-local-repository-car-member-empty-list ()
+(ert-deftest test-local-repository-localrepo--car-member-empty-list ()
"Boundary: an empty list never matches."
- (should-not (car-member 'a nil)))
+ (should-not (localrepo--car-member 'a nil)))
-(ert-deftest test-local-repository-car-member-single-match ()
+(ert-deftest test-local-repository-localrepo--car-member-single-match ()
"Boundary: a single-element list whose car matches returns non-nil."
- (should (car-member 'only '((only . 1)))))
+ (should (localrepo--car-member 'only '((only . 1)))))
-(ert-deftest test-local-repository-car-member-single-no-match ()
+(ert-deftest test-local-repository-localrepo--car-member-single-no-match ()
"Boundary: a single-element list whose car differs returns nil."
- (should-not (car-member 'x '((only . 1)))))
+ (should-not (localrepo--car-member 'x '((only . 1)))))
-(ert-deftest test-local-repository-car-member-nil-value-with-nil-car ()
+(ert-deftest test-local-repository-localrepo--car-member-nil-value-with-nil-car ()
"Boundary: a nil VALUE matches a cons whose car is nil."
- (should (car-member nil '((nil . 1) (a . 2)))))
+ (should (localrepo--car-member nil '((nil . 1) (a . 2)))))
-(ert-deftest test-local-repository-car-member-nil-value-no-nil-car ()
+(ert-deftest test-local-repository-localrepo--car-member-nil-value-no-nil-car ()
"Boundary: a nil VALUE with no nil car returns nil."
- (should-not (car-member nil '((a . 1) (b . 2)))))
+ (should-not (localrepo--car-member nil '((a . 1) (b . 2)))))
;;; Error Cases
-(ert-deftest test-local-repository-car-member-non-cons-element ()
+(ert-deftest test-local-repository-localrepo--car-member-non-cons-element ()
"Error: a non-cons element makes `car' signal wrong-type-argument."
- (should-error (car-member 'x '(1 2)) :type 'wrong-type-argument))
+ (should-error (localrepo--car-member 'x '(1 2)) :type 'wrong-type-argument))
(provide 'test-local-repository--car-member)
;;; test-local-repository--car-member.el ends here
diff --git a/tests/test-meta-package-headers.el b/tests/test-meta-package-headers.el
new file mode 100644
index 000000000..f9b57cbfc
--- /dev/null
+++ b/tests/test-meta-package-headers.el
@@ -0,0 +1,98 @@
+;;; test-meta-package-headers.el --- Enforce Elisp package-header conventions -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Checks that every owned active config module follows the standard Emacs
+;; Library Header conventions -- the part test-init-module-headers.el does not
+;; cover (it enforces the load-graph metadata block inside the Commentary):
+;;
+;; 1. First line is ;;; NAME.el --- SUMMARY -*- ... -*- (name carries the
+;; .el, summary present, file-local-variable cookie present).
+;; 2. ;;; Commentary: appears before ;;; Code:.
+;; 3. A (provide 'NAME) footer, so the file is require-able.
+;; 4. No UTF-8 BOM before the header.
+;;
+;; Scope is modules/*.el, the owned active module set. Vendored (custom/),
+;; generated (themes/, browser-choice.el), archived (archive/), and private
+;; (*.local.el) files are out of scope by design -- classifying those is the
+;; file-class policy task, not this test. The checker reads files on disk
+;; without loading them, so it adds no startup or package dependency.
+
+;;; Code:
+
+(require 'ert)
+
+(defconst test-pkg-header--exempt '()
+ "Basenames under modules/ exempt from the package-header checks.
+Empty today. Add a basename with a comment when a module is intentionally
+shaped differently, so the exemption is explicit rather than silent.")
+
+(defun test-pkg-header--check (name text)
+ "Return the list of violation symbols for module NAME given file TEXT.
+NAME is the basename (e.g. \"font-config.el\"). An empty list means the
+file is conformant. Possible symbols: `bom', `header', `markers',
+`order', `provide'."
+ (let ((violations '()))
+ (when (string-prefix-p "" text)
+ (push 'bom violations))
+ (let ((first-line (car (split-string text "\n"))))
+ (unless (string-match-p
+ (concat "\\`;;; " (regexp-quote name) " --- .+-\\*-.*-\\*-")
+ first-line)
+ (push 'header violations)))
+ (let ((commentary (string-match "^;;; Commentary:" text))
+ (code (string-match "^;;; Code:" text)))
+ (cond ((or (null commentary) (null code)) (push 'markers violations))
+ ((>= commentary code) (push 'order violations))))
+ (let ((stem (file-name-sans-extension name)))
+ (unless (string-match-p (concat "^(provide '" (regexp-quote stem) ")") text)
+ (push 'provide violations)))
+ (nreverse violations)))
+
+(ert-deftest test-pkg-header-checker-flags-malformed ()
+ "Error: the checker catches each malformed shape."
+ (should (memq 'bom
+ (test-pkg-header--check
+ "foo.el"
+ ";;; foo.el --- x -*- lexical-binding: t; -*-\n;;; Commentary:\n;;; Code:\n(provide 'foo)")))
+ (should (memq 'header
+ (test-pkg-header--check
+ "foo.el"
+ ";;; foo --- x -*- lexical-binding: t; -*-\n;;; Commentary:\n;;; Code:\n(provide 'foo)")))
+ (should (memq 'order
+ (test-pkg-header--check
+ "foo.el"
+ ";;; foo.el --- x -*- lexical-binding: t; -*-\n;;; Code:\n;;; Commentary:\n(provide 'foo)")))
+ (should (memq 'provide
+ (test-pkg-header--check
+ "foo.el"
+ ";;; foo.el --- x -*- lexical-binding: t; -*-\n;;; Commentary:\n;;; Code:\n"))))
+
+(ert-deftest test-pkg-header-checker-passes-conformant ()
+ "Normal: a well-formed module yields no violations."
+ (should-not (test-pkg-header--check
+ "foo.el"
+ ";;; foo.el --- A thing -*- lexical-binding: t; -*-\n;;; Commentary:\n;; doc\n;;; Code:\n(provide 'foo)\n")))
+
+(ert-deftest test-pkg-header-checker-boundary-empty ()
+ "Boundary: empty file text reports every applicable violation, no crash."
+ (let ((v (test-pkg-header--check "foo.el" "")))
+ (should (memq 'header v))
+ (should (memq 'markers v))
+ (should (memq 'provide v))))
+
+(ert-deftest test-pkg-header-all-modules-conform ()
+ "Normal: every modules/*.el passes the package-header checks."
+ (let ((dir (expand-file-name "modules" user-emacs-directory))
+ (bad '()))
+ (dolist (file (directory-files dir t "\\.el\\'"))
+ (let ((name (file-name-nondirectory file)))
+ (unless (member name test-pkg-header--exempt)
+ (let* ((text (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string)))
+ (violations (test-pkg-header--check name text)))
+ (when violations (push (cons name violations) bad))))))
+ (should-not bad)))
+
+(provide 'test-meta-package-headers)
+;;; test-meta-package-headers.el ends here
diff --git a/tests/test-music-config--faces.el b/tests/test-music-config--faces.el
new file mode 100644
index 000000000..c45049e1a
--- /dev/null
+++ b/tests/test-music-config--faces.el
@@ -0,0 +1,25 @@
+;;; test-music-config--faces.el --- music playlist face definitions -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; The playlist header propertizes text with cj/music-* faces. Each must be a
+;; defined face (defface) or the reference is invalid -- an undefined face spams
+;; "Invalid face reference" on every header render. The faces inherit from
+;; themed base faces so the theme still owns their colors.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'music-config)
+
+(ert-deftest test-music-config-header-faces-are-defined ()
+ "Normal: every cj/music face the playlist header uses is a defined face."
+ (dolist (f '(cj/music-header-face
+ cj/music-header-value-face
+ cj/music-mode-on-face
+ cj/music-mode-off-face
+ cj/music-keyhint-face))
+ (should (facep f))))
+
+(provide 'test-music-config--faces)
+;;; test-music-config--faces.el ends here
diff --git a/tests/test-nov-reading--palette.el b/tests/test-nov-reading--palette.el
new file mode 100644
index 000000000..b34ea2cac
--- /dev/null
+++ b/tests/test-nov-reading--palette.el
@@ -0,0 +1,92 @@
+;;; test-nov-reading--palette.el --- nov reading-palette tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Pure-logic tests for the nov-mode reading-palette selector: name->face
+;; resolution and the cycle order (palettes, then the no-palette state, wrapping).
+;; The buffer-local face-remap application is exercised live, not here.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'nov-reading)
+
+(declare-function cj/nov--reading-palette-face "nov-reading" (name))
+(declare-function cj/nov--reading-palette-plist "nov-reading" (name))
+(declare-function cj/nov--next-reading-palette "nov-reading" (current names))
+(defvar cj/nov-reading-palettes)
+
+;; Each palette entry is a property list: :face supplies bg/fg, :heading and
+;; :link recolor shr's heading/link faces. Structural keys are optional.
+(defconst test-nov-reading--palettes
+ '(("sepia" :face cj/nov-reading-sepia
+ :heading cj/nov-reading-sepia-heading
+ :link cj/nov-reading-sepia-link)
+ ("dark" :face cj/nov-reading-dark))
+ "Bundle-shaped palette fixture: sepia carries structural faces, dark omits them.")
+
+;;; ----------------------- cj/nov--reading-palette-face -----------------------
+
+(ert-deftest test-nov-reading-palette-face-known ()
+ "Normal: a known palette name resolves to its :face."
+ (let ((cj/nov-reading-palettes test-nov-reading--palettes))
+ (should (eq (cj/nov--reading-palette-face "sepia") 'cj/nov-reading-sepia))
+ (should (eq (cj/nov--reading-palette-face "dark") 'cj/nov-reading-dark))))
+
+(ert-deftest test-nov-reading-palette-face-unknown ()
+ "Error: an unknown name resolves to nil."
+ (let ((cj/nov-reading-palettes test-nov-reading--palettes))
+ (should-not (cj/nov--reading-palette-face "nope"))))
+
+(ert-deftest test-nov-reading-palette-face-nil ()
+ "Boundary: a nil name resolves to nil."
+ (let ((cj/nov-reading-palettes test-nov-reading--palettes))
+ (should-not (cj/nov--reading-palette-face nil))))
+
+;;; ---------------------- cj/nov--reading-palette-plist -----------------------
+
+(ert-deftest test-nov-reading-palette-plist-structural-faces ()
+ "Normal: a palette's :heading and :link faces are retrievable from its plist."
+ (let ((cj/nov-reading-palettes test-nov-reading--palettes))
+ (should (eq (plist-get (cj/nov--reading-palette-plist "sepia") :heading)
+ 'cj/nov-reading-sepia-heading))
+ (should (eq (plist-get (cj/nov--reading-palette-plist "sepia") :link)
+ 'cj/nov-reading-sepia-link))))
+
+(ert-deftest test-nov-reading-palette-plist-omitted-structural ()
+ "Boundary: a palette that omits structural keys yields nil for them."
+ (let ((cj/nov-reading-palettes test-nov-reading--palettes))
+ (should (eq (plist-get (cj/nov--reading-palette-plist "dark") :face)
+ 'cj/nov-reading-dark))
+ (should-not (plist-get (cj/nov--reading-palette-plist "dark") :heading))
+ (should-not (plist-get (cj/nov--reading-palette-plist "dark") :link))))
+
+(ert-deftest test-nov-reading-palette-plist-unknown ()
+ "Error: an unknown palette name yields a nil plist."
+ (let ((cj/nov-reading-palettes test-nov-reading--palettes))
+ (should-not (cj/nov--reading-palette-plist "nope"))))
+
+;;; ----------------------- cj/nov--next-reading-palette -----------------------
+
+(ert-deftest test-nov-reading-next-palette-advances ()
+ "Normal: cycles to the next palette in order."
+ (should (equal (cj/nov--next-reading-palette "sepia" '("sepia" "dark" "light"))
+ "dark")))
+
+(ert-deftest test-nov-reading-next-palette-last-to-none ()
+ "Boundary: the last palette cycles to the no-palette state (nil)."
+ (should-not (cj/nov--next-reading-palette "light" '("sepia" "dark" "light"))))
+
+(ert-deftest test-nov-reading-next-palette-none-to-first ()
+ "Boundary: the no-palette state (nil) cycles to the first palette."
+ (should (equal (cj/nov--next-reading-palette nil '("sepia" "dark" "light"))
+ "sepia")))
+
+(ert-deftest test-nov-reading-next-palette-unknown-current-falls-to-first ()
+ "Error: an unknown current palette falls back to the first."
+ (should (equal (cj/nov--next-reading-palette "gone" '("sepia" "dark" "light"))
+ "sepia")))
+
+(provide 'test-nov-reading--palette)
+;;; test-nov-reading--palette.el ends here
diff --git a/tests/test-nov-reading--text-scale.el b/tests/test-nov-reading--text-scale.el
new file mode 100644
index 000000000..8c2fed8b4
--- /dev/null
+++ b/tests/test-nov-reading--text-scale.el
@@ -0,0 +1,105 @@
+;;; test-nov-reading--text-scale.el --- nov reading text-scale persistence tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the persisted global reading text-scale offset: parsing the stored
+;; value (pure) and the save/load round-trip through the data file. The live
+;; text-scale application in the +/-/= commands is exercised live, not here.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'nov-reading)
+
+(declare-function cj/nov-reading--parse-text-scale "nov-reading" (s))
+(declare-function cj/nov-reading--load-text-scale "nov-reading" ())
+(declare-function cj/nov-reading--save-text-scale "nov-reading" (amount))
+(defvar cj/nov-reading-text-scale-file)
+
+;;; --------------------- cj/nov-reading--parse-text-scale ----------------------
+
+(ert-deftest test-nov-reading-parse-text-scale-positive ()
+ "Normal: a positive integer string parses to that integer."
+ (should (= (cj/nov-reading--parse-text-scale "3") 3)))
+
+(ert-deftest test-nov-reading-parse-text-scale-negative ()
+ "Normal: a negative integer string parses to that integer."
+ (should (= (cj/nov-reading--parse-text-scale "-2") -2)))
+
+(ert-deftest test-nov-reading-parse-text-scale-trailing-newline ()
+ "Boundary: surrounding whitespace/newline is tolerated."
+ (should (= (cj/nov-reading--parse-text-scale "4\n") 4)))
+
+(ert-deftest test-nov-reading-parse-text-scale-zero ()
+ "Boundary: \"0\" parses to 0."
+ (should (= (cj/nov-reading--parse-text-scale "0") 0)))
+
+(ert-deftest test-nov-reading-parse-text-scale-nil ()
+ "Boundary: nil parses to 0."
+ (should (= (cj/nov-reading--parse-text-scale nil) 0)))
+
+(ert-deftest test-nov-reading-parse-text-scale-empty ()
+ "Boundary: an empty string parses to 0."
+ (should (= (cj/nov-reading--parse-text-scale "") 0)))
+
+(ert-deftest test-nov-reading-parse-text-scale-garbage ()
+ "Error: non-numeric content parses to 0."
+ (should (= (cj/nov-reading--parse-text-scale "garbage") 0)))
+
+(ert-deftest test-nov-reading-parse-text-scale-float-rejected ()
+ "Error: a non-integer numeric string parses to 0 (offsets are integers)."
+ (should (= (cj/nov-reading--parse-text-scale "3.5") 0)))
+
+;;; ------------------ cj/nov-reading--save/load round-trip ---------------------
+
+(ert-deftest test-nov-reading-save-load-roundtrip-positive ()
+ "Normal: a saved positive offset loads back unchanged."
+ (let ((cj/nov-reading-text-scale-file (make-temp-file "nov-scale-")))
+ (unwind-protect
+ (progn
+ (cj/nov-reading--save-text-scale 4)
+ (should (= (cj/nov-reading--load-text-scale) 4)))
+ (delete-file cj/nov-reading-text-scale-file))))
+
+(ert-deftest test-nov-reading-save-load-roundtrip-negative ()
+ "Normal: a saved negative offset loads back unchanged."
+ (let ((cj/nov-reading-text-scale-file (make-temp-file "nov-scale-")))
+ (unwind-protect
+ (progn
+ (cj/nov-reading--save-text-scale -3)
+ (should (= (cj/nov-reading--load-text-scale) -3)))
+ (delete-file cj/nov-reading-text-scale-file))))
+
+(ert-deftest test-nov-reading-save-load-roundtrip-zero ()
+ "Boundary: a saved 0 offset loads back as 0."
+ (let ((cj/nov-reading-text-scale-file (make-temp-file "nov-scale-")))
+ (unwind-protect
+ (progn
+ (cj/nov-reading--save-text-scale 0)
+ (should (= (cj/nov-reading--load-text-scale) 0)))
+ (delete-file cj/nov-reading-text-scale-file))))
+
+(ert-deftest test-nov-reading-load-missing-file-defaults-zero ()
+ "Boundary: loading when no file exists yet returns 0."
+ (let ((cj/nov-reading-text-scale-file
+ (expand-file-name "nov-scale-absent"
+ (make-temp-file "nov-scale-dir-" t))))
+ (unwind-protect
+ (should (= (cj/nov-reading--load-text-scale) 0))
+ (delete-directory (file-name-directory cj/nov-reading-text-scale-file) t))))
+
+(ert-deftest test-nov-reading-save-creates-missing-directory ()
+ "Boundary: save creates the data directory when it is absent."
+ (let* ((dir (make-temp-file "nov-scale-dir-" t))
+ (cj/nov-reading-text-scale-file
+ (expand-file-name "sub/nov-reading-text-scale" dir)))
+ (unwind-protect
+ (progn
+ (cj/nov-reading--save-text-scale 2)
+ (should (file-readable-p cj/nov-reading-text-scale-file))
+ (should (= (cj/nov-reading--load-text-scale) 2)))
+ (delete-directory dir t))))
+
+(provide 'test-nov-reading--text-scale)
+;;; test-nov-reading--text-scale.el ends here
diff --git a/tests/test-org-capture-config-popup-window.el b/tests/test-org-capture-config-popup-window.el
index 671d55ab9..af96ba012 100644
--- a/tests/test-org-capture-config-popup-window.el
+++ b/tests/test-org-capture-config-popup-window.el
@@ -110,11 +110,11 @@ deletes the popup frame instead of leaving it orphaned.
Components integrated:
- cj/quick-capture (real)
- org-capture (MOCKED — signals user-error \"Abort\")
-- cj/org-capture--delete-popup-frame (MOCKED — records the call)"
+- cj/org-capture-reap-popup-frames (MOCKED — records the call)"
(let ((deleted 0))
(cl-letf (((symbol-function 'org-capture)
(lambda (&rest _) (user-error "Abort")))
- ((symbol-function 'cj/org-capture--delete-popup-frame)
+ ((symbol-function 'cj/org-capture-reap-popup-frames)
(lambda () (cl-incf deleted))))
(cj/quick-capture))
(should (= deleted 1))))
@@ -124,7 +124,7 @@ Components integrated:
(let ((deleted 0))
(cl-letf (((symbol-function 'org-capture)
(lambda (&rest _) (signal 'quit nil)))
- ((symbol-function 'cj/org-capture--delete-popup-frame)
+ ((symbol-function 'cj/org-capture-reap-popup-frames)
(lambda () (cl-incf deleted))))
(cj/quick-capture))
(should (= deleted 1))))
@@ -134,19 +134,32 @@ Components integrated:
the finalize hook owns that."
(let ((deleted 0))
(cl-letf (((symbol-function 'org-capture) (lambda (&rest _) nil))
- ((symbol-function 'cj/org-capture--delete-popup-frame)
+ ((symbol-function 'cj/org-capture-reap-popup-frames)
(lambda () (cl-incf deleted))))
(cj/quick-capture))
(should (= deleted 0))))
-;;; cj/org-capture--popup-frame-p
+;;; cj/org-capture--frame-reapable-p
-(ert-deftest test-org-capture-config-popup-frame-p ()
- "Normal/Boundary: true only when the selected frame is named \"org-capture\"."
- (cl-letf (((symbol-function 'frame-parameter) (lambda (&rest _) "org-capture")))
- (should (cj/org-capture--popup-frame-p)))
- (cl-letf (((symbol-function 'frame-parameter) (lambda (&rest _) "emacs")))
- (should-not (cj/org-capture--popup-frame-p))))
+(ert-deftest test-org-capture-config-frame-reapable-p-no-capture-ui ()
+ "Normal: an \"org-capture\" frame showing only non-capture buffers is reapable."
+ (should (cj/org-capture--frame-reapable-p
+ "org-capture" '("agent [.emacs.d]" "*dashboard*"))))
+
+(ert-deftest test-org-capture-config-frame-reapable-p-capture-buffer-spares ()
+ "Boundary: a CAPTURE-* buffer means the popup is mid-capture — not reapable."
+ (should-not (cj/org-capture--frame-reapable-p
+ "org-capture" '("CAPTURE-todo.org" "*dashboard*"))))
+
+(ert-deftest test-org-capture-config-frame-reapable-p-select-menu-spares ()
+ "Boundary: the *Org Select* template menu means mid-capture — not reapable."
+ (should-not (cj/org-capture--frame-reapable-p
+ "org-capture" '("*Org Select*"))))
+
+(ert-deftest test-org-capture-config-frame-reapable-p-other-frame-never ()
+ "Error: a frame not named \"org-capture\" is never reapable, even when empty."
+ (should-not (cj/org-capture--frame-reapable-p
+ "Emacs 30.2 : agent [.emacs.d]" '("agent [.emacs.d]"))))
;;; cj/org-capture--popup-frame (find the popup frame by name)
diff --git a/tests/test-system-commands-keymap.el b/tests/test-system-commands-keymap.el
index ac78a25d5..82be1d8de 100644
--- a/tests/test-system-commands-keymap.el
+++ b/tests/test-system-commands-keymap.el
@@ -2,8 +2,9 @@
;;; Commentary:
-;; The system command keymap should remain mounted as a prefix under C-; ! so
-;; which-key can show the documented subcommands.
+;; C-; ! is bound directly to the completing-read menu. The per-command leaf
+;; keys (s/r/e/l/L/E/S) were removed to reclaim the key real-estate; every
+;; command stays reachable through the menu (see the menu-dispatch test).
;;; Code:
@@ -14,23 +15,21 @@
(require 'system-commands)
-(ert-deftest test-system-commands-keymap-normal-prefix-mounted ()
- "Normal: C-; ! remains a prefix keymap, not a direct command."
- (should (eq (keymap-lookup cj/custom-keymap "!")
- cj/system-command-map)))
-
-(ert-deftest test-system-commands-keymap-normal-documented-subkeys ()
- "Normal: documented system command subkeys resolve under the prefix."
- (dolist (binding '(("!" . cj/system-command-menu)
- ("L" . cj/system-cmd-logout)
- ("r" . cj/system-cmd-reboot)
- ("s" . cj/system-cmd-shutdown)
- ("S" . cj/system-cmd-suspend)
- ("l" . cj/system-cmd-lock)
- ("E" . cj/system-cmd-exit-emacs)
- ("e" . cj/system-cmd-restart-emacs)))
- (should (eq (keymap-lookup cj/system-command-map (car binding))
- (cdr binding)))))
+(ert-deftest test-system-commands-keymap-normal-menu-bound-directly ()
+ "Normal: C-; ! is the completing-read menu command, not a prefix keymap."
+ (let ((binding (keymap-lookup cj/custom-keymap "!")))
+ (should (eq binding 'cj/system-command-menu))
+ (should (commandp binding))))
+
+(ert-deftest test-system-commands-keymap-normal-leaf-subkeys-removed ()
+ "Normal: no subkeys hang off C-; !, and the commands remain defined."
+ ;; "!" is now a command, not a prefix, so there is no submap to walk into.
+ (should-not (keymapp (keymap-lookup cj/custom-keymap "!")))
+ ;; The commands themselves stay defined and reachable via the menu.
+ (dolist (cmd '(cj/system-cmd-logout cj/system-cmd-reboot cj/system-cmd-shutdown
+ cj/system-cmd-suspend cj/system-cmd-lock cj/system-cmd-exit-emacs
+ cj/system-cmd-restart-emacs))
+ (should (fboundp cmd))))
(provide 'test-system-commands-keymap)
;;; test-system-commands-keymap.el ends here
diff --git a/tests/test-system-commands-resolve-and-run.el b/tests/test-system-commands-resolve-and-run.el
index af2288fd9..9d92c5d68 100644
--- a/tests/test-system-commands-resolve-and-run.el
+++ b/tests/test-system-commands-resolve-and-run.el
@@ -172,9 +172,15 @@ does not run the command."
(ert-deftest test-system-cmd-restart-emacs-no-service-aborts ()
"Error: when no emacs.service exists, restart aborts without running anything."
(let ((ran nil))
+ ;; Drive the real service check to nil at its boundary (no systemctl on
+ ;; PATH) rather than mocking cj/system-cmd--emacs-service-available-p
+ ;; itself: cj/system-cmd-restart-emacs reaches that helper through a
+ ;; native-comp intra-file direct call that bypasses a symbol-function
+ ;; redefinition, so the helper-level mock silently no-ops and the real
+ ;; check passes on a machine that has emacs.service. executable-find is a
+ ;; subr the helper calls, and its trampoline honors the cl-letf swap.
(cl-letf (((symbol-function 'daemonp) (lambda () t))
- ((symbol-function 'cj/system-cmd--emacs-service-available-p)
- (lambda () nil))
+ ((symbol-function 'executable-find) (lambda (&rest _) nil))
((symbol-function 'read-char-choice) (lambda (&rest _) ?y))
((symbol-function 'call-process-shell-command)
(lambda (&rest _) (setq ran t))))
diff --git a/tests/test-system-defaults-functions.el b/tests/test-system-defaults-functions.el
index 2562ff6aa..c603fc7eb 100644
--- a/tests/test-system-defaults-functions.el
+++ b/tests/test-system-defaults-functions.el
@@ -9,7 +9,7 @@
;; cj/minibuffer-setup-hook -- inflate gc-cons-threshold while
;; typing in the minibuffer
;; cj/minibuffer-exit-hook -- restore gc-cons-threshold on exit
-;; unpropertize-kill-ring -- strip text properties from
+;; cj/--unpropertize-kill-ring -- strip text properties from
;; kill-ring at shutdown
;; cj/log-comp-warning -- route native-comp warnings to a
;; file rather than the *Warnings*
@@ -79,13 +79,13 @@
(should (eq (cj/disabled) nil))
(should (commandp #'cj/disabled)))
-;;; unpropertize-kill-ring
+;;; cj/--unpropertize-kill-ring
(ert-deftest test-system-defaults-unpropertize-kill-ring-strips-properties ()
"Normal: every kill-ring entry comes back with no text properties."
(let ((kill-ring (list (propertize "alpha" 'face 'bold)
(propertize "beta" 'face 'underline))))
- (unpropertize-kill-ring)
+ (cj/--unpropertize-kill-ring)
(should (equal kill-ring '("alpha" "beta")))
(should-not (text-properties-at 0 (nth 0 kill-ring)))
(should-not (text-properties-at 0 (nth 1 kill-ring)))))
@@ -93,7 +93,7 @@
(ert-deftest test-system-defaults-unpropertize-kill-ring-boundary-empty-ring ()
"Boundary: an empty `kill-ring' stays empty after the strip pass."
(let ((kill-ring nil))
- (unpropertize-kill-ring)
+ (cj/--unpropertize-kill-ring)
(should (null kill-ring))))
;;; cj/log-comp-warning
diff --git a/tests/test-system-defaults.el b/tests/test-system-defaults.el
index f653e1fbb..a641adea1 100644
--- a/tests/test-system-defaults.el
+++ b/tests/test-system-defaults.el
@@ -8,7 +8,7 @@
;; writes land, where backups go, and whether the minibuffer GC hooks are
;; installed. Load happens in the shared sandbox (testutil-system-defaults.el).
;;
-;; The module's functions (cj/disabled, the GC hook bodies, unpropertize-kill-ring,
+;; The module's functions (cj/disabled, the GC hook bodies, cj/--unpropertize-kill-ring,
;; cj/log-comp-warning) are covered by test-system-defaults-functions.el, and the
;; vc-follow-symlinks default by test-system-defaults-vc-follow-symlinks.el.
diff --git a/tests/test-system-lib--completion-file-annotator.el b/tests/test-system-lib--completion-file-annotator.el
new file mode 100644
index 000000000..9e1f4aa4a
--- /dev/null
+++ b/tests/test-system-lib--completion-file-annotator.el
@@ -0,0 +1,54 @@
+;;; test-system-lib--completion-file-annotator.el --- Tests for cj/completion-file-annotator -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for `cj/completion-file-annotator', the annotation-function
+;; factory used to annotate file-basename completion pickers with size and
+;; modification date.
+
+;;; Code:
+
+(require 'ert)
+(require 'system-lib)
+
+(ert-deftest test-system-lib-completion-file-annotator-normal-file-shows-size-and-date ()
+ "Normal: a regular file is annotated with a size and an ISO date."
+ (let ((file (make-temp-file "cfa-test-" nil ".txt" "hello world")))
+ (unwind-protect
+ (let* ((annotate (cj/completion-file-annotator
+ (lambda (_cand) file)))
+ (result (funcall annotate "anything")))
+ (should (stringp result))
+ ;; file-size-human-readable of 11 bytes is "11"
+ (should (string-match-p "11" result))
+ ;; ISO date for the file's mtime
+ (should (string-match-p
+ (format-time-string "%Y-%m-%d"
+ (file-attribute-modification-time
+ (file-attributes file)))
+ result)))
+ (delete-file file))))
+
+(ert-deftest test-system-lib-completion-file-annotator-boundary-directory-marked-dir ()
+ "Boundary: a directory candidate is annotated with the `dir' marker."
+ (let ((dir (make-temp-file "cfa-dir-" t)))
+ (unwind-protect
+ (let* ((annotate (cj/completion-file-annotator (lambda (_c) dir)))
+ (result (funcall annotate "d")))
+ (should (stringp result))
+ (should (string-match-p "dir" result)))
+ (delete-directory dir t))))
+
+(ert-deftest test-system-lib-completion-file-annotator-error-nil-path-returns-nil ()
+ "Error: a candidate whose path-resolver returns nil yields no annotation."
+ (let ((annotate (cj/completion-file-annotator (lambda (_c) nil))))
+ (should (null (funcall annotate "missing")))))
+
+(ert-deftest test-system-lib-completion-file-annotator-error-missing-file-returns-nil ()
+ "Error: a path that does not exist yields no annotation."
+ (let* ((path (expand-file-name "definitely-not-here-12345.txt"
+ temporary-file-directory))
+ (annotate (cj/completion-file-annotator (lambda (_c) path))))
+ (should (null (funcall annotate "gone")))))
+
+(provide 'test-system-lib--completion-file-annotator)
+;;; test-system-lib--completion-file-annotator.el ends here
diff --git a/tests/test-system-utils-commands.el b/tests/test-system-utils-commands.el
index b7b61dc22..6f2099a24 100644
--- a/tests/test-system-utils-commands.el
+++ b/tests/test-system-utils-commands.el
@@ -90,5 +90,14 @@ and lands in a dedicated output buffer."
(should saved)
(should killed)))
+;;; ibuffer delete/diff keybinding swap
+
+(ert-deftest test-system-utils-ibuffer-d-diffs-D-deletes ()
+ "Normal: in the ibuffer list, d diffs the buffer at point against its file and
+D marks it for deletion (the swap of ibuffer's default d/= bindings)."
+ (require 'ibuffer)
+ (should (eq (keymap-lookup ibuffer-mode-map "d") #'ibuffer-diff-with-file))
+ (should (eq (keymap-lookup ibuffer-mode-map "D") #'ibuffer-mark-for-delete)))
+
(provide 'test-system-utils-commands)
;;; test-system-utils-commands.el ends here
diff --git a/tests/test-term-config--f8-in-term.el b/tests/test-term-config--f8-in-term.el
deleted file mode 100644
index 6cee4ff46..000000000
--- a/tests/test-term-config--f8-in-term.el
+++ /dev/null
@@ -1,42 +0,0 @@
-;;; test-term-config--f8-in-term.el --- F8 reaches Emacs from inside a ghostel buffer -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; <f8> is a global binding (`cj/main-agenda-display', set in org-agenda-config).
-;; ghostel's semi-char mode forwards every key NOT in `ghostel-keymap-exceptions'
-;; to the terminal program, so a plain <f8> typed while point is in a ghostel
-;; buffer would be sent to the program instead of opening the agenda. Unlike the
-;; F9 family, F8 is NOT re-bound in `ghostel-mode-map' -- it simply falls through
-;; to the global map once the semi-char map stops forwarding it, so the only
-;; wiring term-config.el adds is the keymap-exceptions entry plus the rebuild.
-;; These tests require ghostel (so term-config's `with-eval-after-load' fires)
-;; BEFORE term-config, then confirm the exception landed and the rebuilt
-;; semi-char map no longer forwards <f8>. `(require 'ghostel)' does not load the
-;; native module, so this stays light.
-
-;;; Code:
-
-(require 'ert)
-(require 'package)
-
-(setq package-user-dir (expand-file-name "elpa" user-emacs-directory))
-(package-initialize)
-(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-(require 'ghostel)
-(require 'term-config)
-
-(ert-deftest test-term-config-f8-in-keymap-exceptions ()
- "Regression: <f8> is in `ghostel-keymap-exceptions' so semi-char mode lets it
-reach Emacs instead of forwarding it to the terminal program. This is what lets
-the global agenda binding work from inside a ghostel buffer."
- (should (member "<f8>" ghostel-keymap-exceptions)))
-
-(ert-deftest test-term-config-f8-not-forwarded-by-semi-char-map ()
- "Regression: the rebuilt semi-char map must no longer forward <f8> to the pty.
-`add-to-list' updates the exceptions list but not the already-built map -- only
-`ghostel--rebuild-semi-char-keymap' (run in term-config's :init) drops the
-forwarding binding so <f8> falls through to the global agenda command."
- (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f8>")
- 'ghostel--send-event)))
-
-(provide 'test-term-config--f8-in-term)
-;;; test-term-config--f8-in-term.el ends here
diff --git a/tests/test-term-tmux-history.el b/tests/test-term-tmux-history.el
index 0ea7cf37d..a5f5c93cd 100644
--- a/tests/test-term-tmux-history.el
+++ b/tests/test-term-tmux-history.el
@@ -1,14 +1,13 @@
-;;; test-term-tmux-history.el --- Tests for term-config tmux history + menu UX -*- lexical-binding: t; -*-
+;;; test-term-tmux-history.el --- Tests for the EAT terminal copy-mode + tmux history -*- lexical-binding: t; -*-
;;; Commentary:
-;; Exercises the term-config (ghostel) terminal UX: the Emacs-owned tmux
-;; history buffer, the copy-mode-dwim engine pick, the tmux pane-id /
-;; attached-client predicates, and the C-; x menu bindings.
+;; Exercises the terminal UX carried into eat-config for the EAT agent
+;; terminals: the Emacs-owned tmux history buffer, the copy-mode-dwim engine
+;; pick, the tmux pane-id / attached-client predicates, and the C-; x menu
+;; bindings. Agents run EAT over tmux, so copy-mode is tmux's own copy-mode.
;;
-;; ghostel is required (which defines `ghostel-mode-map' /
-;; `ghostel-keymap-exceptions' and lets term-config's `with-eval-after-load'
-;; fire) before term-config. `(require 'ghostel)' does not load the native
-;; module; tmux is mocked via `process-file', so nothing spawns.
+;; eat is required (so eat-config's `with-eval-after-load' fires for the C-<up>
+;; bind) before eat-config; tmux is mocked via `process-file', so nothing spawns.
;;; Code:
@@ -21,9 +20,9 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(setq load-prefer-newer t)
-(require 'ghostel)
-(require 'term-config)
-(require 'testutil-ghostel-buffers)
+(require 'eat)
+(require 'eat-config)
+(require 'testutil-terminal-buffers)
(defmacro test-term-tmux-history--with-tmux-mock (responses &rest body)
"Run BODY with `process-file' mocked for tmux RESPONSES.
@@ -51,6 +50,8 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)."
exit-code))))
,@body)))
+;;; tmux helpers
+
(ert-deftest test-term-tmux-history--pane-id-for-tty-matches-client ()
"Normal: current terminal pty maps to the active pane for that tmux client."
(test-term-tmux-history--with-tmux-mock
@@ -66,9 +67,32 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)."
(should (equal (cj/term--tmux-capture-pane "%8")
"first line\nsecond line\n"))))
+(ert-deftest test-term-current-tmux-pane-id-rejects-non-eat-buffer ()
+ "Error: pane-id lookup refuses a buffer that is not in `eat-mode'."
+ (with-temp-buffer
+ (should-error (cj/term--current-tmux-pane-id) :type 'user-error)))
+
+(ert-deftest test-term-current-tmux-pane-id-accepts-agent-named-buffer ()
+ "Normal: an agent-named eat buffer resolves by process TTY, not buffer name."
+ (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")))
+ (unwind-protect
+ (with-current-buffer agent
+ (cl-letf (((symbol-function 'get-buffer-process)
+ (lambda (_buffer) 'fake-process))
+ ((symbol-function 'process-tty-name)
+ (lambda (_process &rest _) "/dev/pts/8")))
+ (test-term-tmux-history--with-tmux-mock
+ '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0
+ "/dev/pts/1\t%1\n/dev/pts/8\t%8\n"))
+ (should (equal (cj/term--current-tmux-pane-id) "%8")))))
+ (when (buffer-live-p agent)
+ (kill-buffer agent)))))
+
+;;; tmux history buffer
+
(ert-deftest test-term-tmux-history-open-renders-read-only-history-buffer ()
- "Normal: command renders tmux history in a normal Emacs buffer."
- (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-origin*")))
+ "Normal: the command renders tmux history in a normal Emacs buffer."
+ (let ((origin (cj/test--make-fake-eat-buffer "*test-term-history-origin*")))
(unwind-protect
(save-window-excursion
(switch-to-buffer origin)
@@ -90,41 +114,8 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)."
(when (buffer-live-p origin)
(kill-buffer origin)))))
-(ert-deftest test-term-tmux-history-replaces-origin-buffer-in-same-window ()
- "Normal: the history view replaces the origin in the selected window.
-
-`cj/term-tmux-history' uses `switch-to-buffer' so reading scrollback keeps
-the terminal's frame slot rather than splitting or popping a new window."
- (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-inplace*")))
- (unwind-protect
- (save-window-excursion
- (delete-other-windows)
- (switch-to-buffer origin)
- (let ((win (selected-window)))
- (should (eq (window-buffer win) origin))
- (should (one-window-p))
- (cl-letf (((symbol-function 'get-buffer-process)
- (lambda (_buffer) 'fake-process))
- ((symbol-function 'process-tty-name)
- (lambda (_process &rest _) "/dev/pts/8")))
- (test-term-tmux-history--with-tmux-mock
- '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0
- "/dev/pts/8\t%8\n")
- (("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0
- "scrollback line\n"))
- (cj/term-tmux-history)))
- (should (one-window-p))
- (should (eq (selected-window) win))
- (should (string-prefix-p
- "*terminal tmux history:"
- (buffer-name (window-buffer win))))))
- (cj/test--kill-buffers-matching-prefix "*terminal tmux history")
- (when (buffer-live-p origin)
- (kill-buffer origin)))))
-
(ert-deftest test-term-tmux-history-quit-returns-to-origin ()
- "Normal: q / <escape> / C-g (cj/term-tmux-history-quit) kills the history
-buffer and restores the origin buffer, window, and point."
+ "Normal: quit kills the history buffer and restores origin buffer/window/point."
(let ((origin (get-buffer-create "*test-term-history-return*")))
(unwind-protect
(let ((history (get-buffer-create "*terminal tmux history: test*")))
@@ -149,10 +140,8 @@ buffer and restores the origin buffer, window, and point."
(kill-buffer origin)))))
(ert-deftest test-term-tmux-history-mode-keymap ()
- "Normal: in the history buffer M-w copies without quitting; q, <escape>,
-and C-g quit back to the terminal; RET is left unbound (no special exit)."
- (should (eq (keymap-lookup cj/term-tmux-history-mode-map "M-w")
- #'kill-ring-save))
+ "Normal: M-w copies; q/<escape>/C-g quit; RET is left unbound."
+ (should (eq (keymap-lookup cj/term-tmux-history-mode-map "M-w") #'kill-ring-save))
(should (eq (keymap-lookup cj/term-tmux-history-mode-map "q")
#'cj/term-tmux-history-quit))
(should (eq (keymap-lookup cj/term-tmux-history-mode-map "<escape>")
@@ -161,50 +150,11 @@ and C-g quit back to the terminal; RET is left unbound (no special exit)."
#'cj/term-tmux-history-quit))
(should-not (keymap-lookup cj/term-tmux-history-mode-map "RET")))
-(ert-deftest test-term-keymap-includes-history-and-copy-bindings ()
- "Normal: the personal terminal map owns the high-level UX commands, and C-;
-reaches Emacs inside ghostel buffers so the prefix works there."
- (should (member "C-;" ghostel-keymap-exceptions))
- (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/term-tmux-history))
- (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/term-copy-mode-dwim))
- (should (equal (keymap-lookup ghostel-mode-map "C-;") cj/custom-keymap))
- (should (eq (keymap-lookup ghostel-mode-map "C-; x h") #'cj/term-tmux-history))
- (should (eq (keymap-lookup ghostel-mode-map "C-; x c") #'cj/term-copy-mode-dwim)))
-
-(ert-deftest test-term-keymap-prompt-navigation ()
- "Normal: n/p navigate prompts, capital N creates a new terminal buffer."
- (should (eq (keymap-lookup cj/custom-keymap "x n") #'ghostel-next-prompt))
- (should (eq (keymap-lookup cj/custom-keymap "x p") #'ghostel-previous-prompt))
- (should (eq (keymap-lookup cj/custom-keymap "x N") #'ghostel)))
-
-(ert-deftest test-term-current-tmux-pane-id-rejects-non-ghostel-buffer ()
- "Error: pane-id lookup refuses a buffer that is not in `ghostel-mode'."
- (with-temp-buffer
- (should-error (cj/term--current-tmux-pane-id) :type 'user-error)))
-
-(ert-deftest test-term-current-tmux-pane-id-accepts-agent-named-buffer ()
- "Normal: an agent-named ghostel buffer resolves by process TTY.
-
-The pane lookup keys off the live process TTY, never the buffer name, so a
-buffer named `agent [repo]' (ai-term.el's naming) resolves like any other
-ghostel-mode terminal."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")))
- (unwind-protect
- (with-current-buffer agent
- (cl-letf (((symbol-function 'get-buffer-process)
- (lambda (_buffer) 'fake-process))
- ((symbol-function 'process-tty-name)
- (lambda (_process &rest _) "/dev/pts/8")))
- (test-term-tmux-history--with-tmux-mock
- '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0
- "/dev/pts/1\t%1\n/dev/pts/8\t%8\n"))
- (should (equal (cj/term--current-tmux-pane-id) "%8")))))
- (when (buffer-live-p agent)
- (kill-buffer agent)))))
+;;; in-tmux-p predicate
(ert-deftest test-term-in-tmux-p-true-when-client-attached ()
"Normal: predicate returns t when tmux reports a client for our tty."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")))
+ (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")))
(unwind-protect
(with-current-buffer agent
(cl-letf (((symbol-function 'get-buffer-process)
@@ -218,25 +168,18 @@ ghostel-mode terminal."
(when (buffer-live-p agent)
(kill-buffer agent)))))
-(ert-deftest test-term-in-tmux-p-nil-when-no-matching-client ()
- "Boundary: predicate returns nil when tmux runs but our tty has no client."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")))
- (unwind-protect
- (with-current-buffer agent
- (cl-letf (((symbol-function 'get-buffer-process)
- (lambda (_buffer) 'fake-process))
- ((symbol-function 'process-tty-name)
- (lambda (_process &rest _) "/dev/pts/8")))
- (test-term-tmux-history--with-tmux-mock
- '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0
- "/dev/pts/1\t%1\n"))
- (should-not (cj/term--in-tmux-p)))))
- (when (buffer-live-p agent)
- (kill-buffer agent)))))
+(ert-deftest test-term-in-tmux-p-nil-when-not-eat-mode ()
+ "Boundary: predicate refuses non-eat buffers without calling tmux."
+ (with-temp-buffer
+ (let ((tmux-called nil))
+ (cl-letf (((symbol-function 'process-file)
+ (lambda (&rest _) (setq tmux-called t) 0)))
+ (should-not (cj/term--in-tmux-p))
+ (should-not tmux-called)))))
(ert-deftest test-term-in-tmux-p-nil-when-tmux-fails ()
"Error: predicate swallows tmux failures and returns nil."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")))
+ (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")))
(unwind-protect
(with-current-buffer agent
(cl-letf (((symbol-function 'get-buffer-process)
@@ -250,117 +193,33 @@ ghostel-mode terminal."
(when (buffer-live-p agent)
(kill-buffer agent)))))
-(ert-deftest test-term-in-tmux-p-nil-when-not-ghostel-mode ()
- "Boundary: predicate refuses non-ghostel buffers without calling tmux."
- (with-temp-buffer
- (let ((tmux-called nil))
- (cl-letf (((symbol-function 'process-file)
- (lambda (&rest _) (setq tmux-called t) 0)))
- (should-not (cj/term--in-tmux-p))
- (should-not tmux-called)))))
+;;; copy-mode (tmux path -- the agent terminal case)
(ert-deftest test-term-copy-mode-dwim-sends-tmux-prefix-when-attached ()
- "Normal: with tmux attached, dwim writes C-b [ then C-a into the pty so
-tmux enters its own copy-mode and lands the cursor at the start of the
-line. Without the trailing C-a the cursor inherits the live column (far
-right after a prompt) and scrolling up runs up the right edge; start-of-line
-puts it at column 0 so it runs up the left."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))
- (sent nil)
- (copy-mode-called nil))
+ "Normal: with tmux attached, dwim writes C-b [ then C-a into the pty so tmux
+enters copy-mode with the cursor at column 0."
+ (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))
+ (sent nil))
(unwind-protect
(with-current-buffer agent
(cl-letf (((symbol-function 'get-buffer-process)
(lambda (_buffer) 'fake-process))
((symbol-function 'process-tty-name)
(lambda (_process &rest _) "/dev/pts/8"))
- ((symbol-function 'ghostel-send-string)
- (lambda (s) (push s sent)))
- ((symbol-function 'ghostel-copy-mode)
- (lambda () (setq copy-mode-called t))))
+ ((symbol-function 'cj/--term-send-string)
+ (lambda (s) (push s sent))))
(test-term-tmux-history--with-tmux-mock
'((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0
"/dev/pts/8\t%8\n"))
(cj/term-copy-mode-dwim)
- (should (equal sent '("\C-b[\C-a")))
- (should-not copy-mode-called))))
- (when (buffer-live-p agent)
- (kill-buffer agent)))))
-
-(ert-deftest test-term-copy-mode-dwim-falls-back-without-tmux ()
- "Boundary: without tmux, dwim calls `ghostel-copy-mode' then moves point
-to the start of the line and sends nothing to the pty. The
-`beginning-of-line' must run after `ghostel-copy-mode' so it repositions
-inside the copy view; column 0 keeps the cursor on the left edge while
-scrolling, parity with the tmux branch's trailing C-a."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))
- (sent nil)
- (dwim-order nil))
- (unwind-protect
- (with-current-buffer agent
- (cl-letf (((symbol-function 'get-buffer-process)
- (lambda (_buffer) 'fake-process))
- ((symbol-function 'process-tty-name)
- (lambda (_process &rest _) "/dev/pts/8"))
- ((symbol-function 'ghostel-send-string)
- (lambda (s) (push s sent)))
- ((symbol-function 'ghostel-copy-mode)
- (lambda () (push 'copy-mode dwim-order)))
- ((symbol-function 'beginning-of-line)
- (lambda (&optional _n) (push 'beginning-of-line dwim-order))))
- (test-term-tmux-history--with-tmux-mock
- '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1
- "no server running"))
- (cj/term-copy-mode-dwim)
- (should-not sent)
- (should (equal (reverse dwim-order) '(copy-mode beginning-of-line))))))
+ (should (equal sent '("\C-b[\C-a"))))))
(when (buffer-live-p agent)
(kill-buffer agent)))))
-(ert-deftest test-term-prefix-and-f12-in-keymap-exceptions ()
- "Regression: C-; and F12 are in `ghostel-keymap-exceptions' and the rebuilt
-semi-char map no longer forwards them to the pty, so the prefix keymap and the
-F12 toggle reach Emacs inside ghostel buffers."
- (dolist (key '("C-;" "<f12>"))
- (should (member key ghostel-keymap-exceptions)))
- (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f12>")
- 'ghostel--send-event)))
-
-(ert-deftest test-term-window-nav-keys-in-keymap-exceptions ()
- "Regression: windmove (S-arrows) and buffer-move (C-M-arrows) are in
-`ghostel-keymap-exceptions' so they reach Emacs from inside a ghostel buffer
-instead of being forwarded to the terminal program."
- (dolist (key '("S-<up>" "S-<down>" "S-<left>" "S-<right>"
- "C-M-<up>" "C-M-<down>" "C-M-<left>" "C-M-<right>"))
- (should (member key ghostel-keymap-exceptions)))
- (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "C-M-<left>")
- 'ghostel--send-event)))
-
-(ert-deftest test-term-f10-music-in-keymap-exceptions ()
- "Regression: F10 (music playlist toggle) is in `ghostel-keymap-exceptions'
-so it reaches Emacs from inside a ghostel buffer instead of being forwarded
-to the terminal program. It is a global binding, so dropping it from the
-semi-char map lets the lookup fall through to the global map. Server
-shutdown moved off C-F10 to C-x C, which is deliberately NOT an exception
-(C-x C stays forwarding to the terminal program inside an agent buffer)."
- (should (member "<f10>" ghostel-keymap-exceptions))
- (should-not (member "C-<f10>" ghostel-keymap-exceptions))
- (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f10>")
- 'ghostel--send-event)))
-
-(ert-deftest test-term-c-spc-forwarded-not-set-mark ()
- "Regression: C-SPC is forwarded to the terminal, not bound to the global
-`set-mark-command'. ghostel only forwards the `C-@' event, so without this an
-Emacs region gets stuck in the ghostel buffer and tmux copy-mode's
-begin-selection never starts."
- (should (eq (keymap-lookup ghostel-mode-map "C-SPC") #'cj/term-send-C-SPC)))
-
-;; ----------------------------- copy-mode scroll ------------------------------
-
(ert-deftest test-term-copy-mode-up-tmux-enters-then-scrolls-up ()
"Normal: from a live (non-copy) tmux pane, C-<up> enters copy-mode then sends
the up-arrow, so one stroke both enters copy-mode and scrolls up."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))
+ (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))
(sent nil))
(unwind-protect
(with-current-buffer agent
@@ -368,7 +227,7 @@ the up-arrow, so one stroke both enters copy-mode and scrolls up."
(lambda (_buffer) 'fake-process))
((symbol-function 'process-tty-name)
(lambda (_process &rest _) "/dev/pts/8"))
- ((symbol-function 'ghostel-send-string)
+ ((symbol-function 'cj/--term-send-string)
(lambda (s) (push s sent))))
(test-term-tmux-history--with-tmux-mock
'((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0
@@ -381,8 +240,8 @@ the up-arrow, so one stroke both enters copy-mode and scrolls up."
(ert-deftest test-term-copy-mode-up-tmux-already-in-mode-just-scrolls ()
"Normal: when the tmux pane is already in copy-mode, C-<up> only sends the
-up-arrow -- it does not re-enter (which would reset the cursor)."
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))
+up-arrow -- it does not re-enter and reset the cursor."
+ (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))
(sent nil))
(unwind-protect
(with-current-buffer agent
@@ -390,7 +249,7 @@ up-arrow -- it does not re-enter (which would reset the cursor)."
(lambda (_buffer) 'fake-process))
((symbol-function 'process-tty-name)
(lambda (_process &rest _) "/dev/pts/8"))
- ((symbol-function 'ghostel-send-string)
+ ((symbol-function 'cj/--term-send-string)
(lambda (s) (push s sent))))
(test-term-tmux-history--with-tmux-mock
'((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0
@@ -401,54 +260,61 @@ up-arrow -- it does not re-enter (which would reset the cursor)."
(when (buffer-live-p agent)
(kill-buffer agent)))))
-(ert-deftest test-term-copy-mode-up-nontmux-enters-then-moves-up ()
- "Boundary: without tmux and not yet in copy-mode, C-<up> enters
-ghostel-copy-mode then moves point up a line, sending nothing to the pty."
- (with-temp-buffer
- (insert "abc\ndef\nghi\n")
- (goto-char (point-min))
- (forward-line 2) ; land on line 3
- (let ((sent nil) (entered nil))
- (cl-letf (((symbol-function 'ghostel-send-string) (lambda (s) (push s sent)))
- ((symbol-function 'ghostel-copy-mode) (lambda () (setq entered t))))
- (cj/term-copy-mode-up)
- (should entered)
- (should-not sent)
- (should (= (line-number-at-pos) 2))))))
-
-(ert-deftest test-term-copy-mode-up-nontmux-already-in-copy-just-moves ()
- "Normal: when ghostel is already in copy-mode, C-<up> just moves point up --
-it does not call `ghostel-copy-mode' again (which would toggle copy-mode off)."
+;;; bindings
+
+(ert-deftest test-term-keymap-history-and-copy-bindings ()
+ "Normal: the C-; x terminal map owns the tmux-history and copy-mode commands."
+ (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/term-tmux-history))
+ (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/term-copy-mode-dwim))
+ (should (eq (keymap-lookup cj/custom-keymap "x t") #'cj/term-toggle)))
+
+(ert-deftest test-term-copy-mode-up-bound-in-eat-semi-char-map ()
+ "Normal: C-<up> enters copy-mode + scrolls up from inside an EAT terminal."
+ (should (eq (keymap-lookup eat-semi-char-mode-map "C-<up>")
+ #'cj/term-copy-mode-up)))
+
+(ert-deftest test-term-escape-bound-as-unified-exit ()
+ "Normal: Escape sends ESC in semi-char mode (cancels tmux copy-mode) and
+returns to semi-char from EAT's emacs/char mode -- one exit key for both."
+ (should (eq (keymap-lookup eat-semi-char-mode-map "<escape>")
+ #'cj/term-send-escape))
+ (should (eq (keymap-lookup eat-mode-map "<escape>") #'eat-semi-char-mode)))
+
+(ert-deftest test-term-send-escape-writes-esc-to-pty ()
+ "Normal: `cj/term-send-escape' sends a bare ESC to the terminal process."
+ (let ((sent nil))
+ (cl-letf (((symbol-function 'cj/--term-send-string)
+ (lambda (s) (push s sent))))
+ (cj/term-send-escape)
+ (should (equal sent '("\e"))))))
+
+(ert-deftest test-term-word-motion-arrows-forwarded-not-window-arrows ()
+ "Normal: C-/M-left/right forward to the terminal (word motion in the program's
+input) instead of moving Emacs point; windmove's S-arrows still reach Emacs."
+ (dolist (key '("C-<left>" "C-<right>" "M-<left>" "M-<right>"))
+ (should (eq (keymap-lookup eat-semi-char-mode-map key) #'eat-self-input)))
+ (dolist (key '("S-<left>" "S-<right>"))
+ (should-not (eq (keymap-lookup eat-semi-char-mode-map key) #'eat-self-input))))
+
+(ert-deftest test-term-eat-tame-scroll-sets-minimal-scroll ()
+ "Normal: `cj/--eat-tame-scroll' sets buffer-local minimal-scroll behavior so
+the EAT window line-scrolls instead of recentering on full-frame redraws."
(with-temp-buffer
- (insert "abc\ndef\nghi\n")
- (goto-char (point-min))
- (forward-line 2) ; land on line 3
- (setq-local ghostel--input-mode 'copy)
- (let ((sent nil) (entered nil))
- (cl-letf (((symbol-function 'ghostel-send-string) (lambda (s) (push s sent)))
- ((symbol-function 'ghostel-copy-mode) (lambda () (setq entered t))))
- (cj/term-copy-mode-up)
- (should-not entered)
- (should-not sent)
- (should (= (line-number-at-pos) 2))))))
-
-(ert-deftest test-term-copy-mode-only-c-up-bound ()
- "Normal/Regression: only C-<up> enters copy-mode in ghostel-mode-map; the
-other arrows are not bound to it, so they pass through to the terminal."
- (should (eq (keymap-lookup ghostel-mode-map "C-<up>") #'cj/term-copy-mode-up))
- (dolist (key '("C-<down>" "C-<left>" "C-<right>"
- "M-<up>" "M-<down>" "M-<left>" "M-<right>"))
- (should-not (eq (keymap-lookup ghostel-mode-map key) #'cj/term-copy-mode-up))))
-
-(ert-deftest test-term-copy-mode-only-c-up-in-keymap-exceptions ()
- "Regression (C-arrow copy-mode bug): only C-<up> is in
-`ghostel-keymap-exceptions'. C-<left>/<right>/<down> are readline word-motion
-at the shell prompt and the M-arrows have no copy-mode role, so none are
-exceptions -- they reach the terminal program instead of Emacs."
- (should (member "C-<up>" ghostel-keymap-exceptions))
- (dolist (key '("C-<down>" "C-<left>" "C-<right>"
- "M-<up>" "M-<down>" "M-<left>" "M-<right>"))
- (should-not (member key ghostel-keymap-exceptions))))
+ (cj/--eat-tame-scroll)
+ (should (= scroll-conservatively 101))
+ (should (= scroll-margin 0))
+ (should (null auto-window-vscroll))))
+
+(ert-deftest test-term-eat-reset-sgr-at-newline ()
+ "Normal: the SGR-reset advice injects a reset before each newline when enabled
+\(containing an unterminated color), and passes output through unchanged when
+disabled."
+ (let ((cj/eat-reset-sgr-at-newline t))
+ (should (equal (cj/--eat-reset-sgr-at-newline (list (quote term) "a\nb\n"))
+ (list (quote term) "a\e[0m\nb\e[0m\n"))))
+ (let ((cj/eat-reset-sgr-at-newline nil))
+ (should (equal (cj/--eat-reset-sgr-at-newline (list (quote term) "a\nb\n"))
+ (list (quote term) "a\nb\n")))))
(provide 'test-term-tmux-history)
;;; test-term-tmux-history.el ends here
diff --git a/tests/test-term-toggle--buffer-filter.el b/tests/test-term-toggle--buffer-filter.el
index 44f30aad6..f56459a06 100644
--- a/tests/test-term-toggle--buffer-filter.el
+++ b/tests/test-term-toggle--buffer-filter.el
@@ -4,9 +4,9 @@
;; Three closely-related helpers determine which terminal buffer F12
;; manages: the predicate `cj/--term-toggle-buffer-p', the list
;; `cj/--term-toggle-buffers', and the per-frame window finder
-;; `cj/--term-toggle-displayed-window'. F12 manages the EAT terminal;
-;; ghostel buffers (including ai-term's agent buffers) are NOT F12-managed --
-;; they live on M-SPC.
+;; `cj/--term-toggle-displayed-window'. F12 opens eshell (run through EAT via
+;; eat-eshell-mode), so it manages eshell-mode buffers. Standalone eat buffers
+;; and ai-term's agent buffers (also eat) are NOT F12-managed.
;;; Code:
@@ -14,26 +14,26 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
-(require 'term-config)
-(require 'testutil-ghostel-buffers)
+(require 'eat-config)
+(require 'testutil-terminal-buffers)
(defun test-term-toggle--cleanup ()
"Kill leftover agent- and *test-term- prefixed buffers."
(cj/test--kill-agent-buffers)
(cj/test--kill-test-term-buffers))
-(ert-deftest test-term-toggle--buffer-p-accepts-eat-mode ()
- "Normal: an eat-mode buffer qualifies as the F12 terminal."
+(ert-deftest test-term-toggle--buffer-p-accepts-eshell-mode ()
+ "Normal: an eshell-mode buffer qualifies as the F12 terminal."
(test-term-toggle--cleanup)
- (let ((buf (cj/test--make-fake-eat-buffer "*test-term-1*")))
+ (let ((buf (cj/test--make-fake-eshell-buffer "*test-term-1*")))
(unwind-protect
(should (cj/--term-toggle-buffer-p buf))
(kill-buffer buf))))
-(ert-deftest test-term-toggle--buffer-p-rejects-ghostel ()
- "Boundary: a ghostel buffer is NOT F12-managed (ghostel is ai-term's, M-SPC)."
+(ert-deftest test-term-toggle--buffer-p-rejects-eat ()
+ "Boundary: a standalone eat buffer is NOT F12-managed (F12 opens eshell)."
(test-term-toggle--cleanup)
- (let ((buf (cj/test--make-fake-ghostel-buffer "*test-term-ghostel*")))
+ (let ((buf (cj/test--make-fake-eat-buffer "*test-term-eat*")))
(unwind-protect
(should-not (cj/--term-toggle-buffer-p buf))
(kill-buffer buf))))
@@ -41,13 +41,13 @@
(ert-deftest test-term-toggle--buffer-p-rejects-agent ()
"Boundary: ai-term agent buffers are excluded from F12's set."
(test-term-toggle--cleanup)
- (let ((buf (cj/test--make-fake-ghostel-buffer "agent [project-a]")))
+ (let ((buf (cj/test--make-fake-eat-buffer "agent [project-a]")))
(unwind-protect
(should-not (cj/--term-toggle-buffer-p buf))
(kill-buffer buf))))
(ert-deftest test-term-toggle--buffer-p-rejects-non-terminal ()
- "Boundary: a regular buffer (not eat-mode, no terminal name prefix) -> nil."
+ "Boundary: a regular buffer (not eshell-mode) -> nil."
(test-term-toggle--cleanup)
(let ((buf (get-buffer-create "*test-term-regular*")))
(unwind-protect
@@ -57,40 +57,40 @@
(ert-deftest test-term-toggle--buffer-p-rejects-dead-buffer ()
"Boundary: nil and dead buffers -> nil."
(should-not (cj/--term-toggle-buffer-p nil))
- (let ((buf (cj/test--make-fake-eat-buffer "*test-term-dead*")))
+ (let ((buf (cj/test--make-fake-eshell-buffer "*test-term-dead*")))
(kill-buffer buf)
(should-not (cj/--term-toggle-buffer-p buf))))
-(ert-deftest test-term-toggle--buffers-returns-eat-excludes-others ()
- "Normal: returns the EAT terminal but not ghostel/agent buffers."
+(ert-deftest test-term-toggle--buffers-returns-eshell-excludes-others ()
+ "Normal: returns the eshell terminal but not eat/agent buffers."
(test-term-toggle--cleanup)
- (let ((eat (cj/test--make-fake-eat-buffer "*test-term-eat*"))
- (agent (cj/test--make-fake-ghostel-buffer "agent [for-test]")))
+ (let ((esh (cj/test--make-fake-eshell-buffer "*test-term-esh*"))
+ (agent (cj/test--make-fake-eat-buffer "agent [for-test]")))
(unwind-protect
(let ((result (cj/--term-toggle-buffers)))
- (should (memq eat result))
+ (should (memq esh result))
(should-not (memq agent result)))
- (kill-buffer eat)
+ (kill-buffer esh)
(kill-buffer agent))))
(ert-deftest test-term-toggle--displayed-window-finds-terminal ()
- "Normal: the EAT terminal in a window -> returns that window."
+ "Normal: the eshell terminal in a window -> returns that window."
(test-term-toggle--cleanup)
- (let ((eat (cj/test--make-fake-eat-buffer "*test-term-shown*")))
+ (let ((esh (cj/test--make-fake-eshell-buffer "*test-term-shown*")))
(unwind-protect
(save-window-excursion
(delete-other-windows)
(let ((win (split-window-right)))
- (set-window-buffer win eat)
+ (set-window-buffer win esh)
(let ((result (cj/--term-toggle-displayed-window)))
(should (windowp result))
- (should (eq (window-buffer result) eat)))))
- (kill-buffer eat))))
+ (should (eq (window-buffer result) esh)))))
+ (kill-buffer esh))))
(ert-deftest test-term-toggle--displayed-window-skips-agent ()
"Boundary: only an agent terminal is displayed -> nil (agent not F12-managed)."
(test-term-toggle--cleanup)
- (let ((agent (cj/test--make-fake-ghostel-buffer "agent [skip-test]")))
+ (let ((agent (cj/test--make-fake-eat-buffer "agent [skip-test]")))
(unwind-protect
(save-window-excursion
(delete-other-windows)
diff --git a/tests/test-term-toggle--dispatch.el b/tests/test-term-toggle--dispatch.el
index f13c2840b..43db4c3fe 100644
--- a/tests/test-term-toggle--dispatch.el
+++ b/tests/test-term-toggle--dispatch.el
@@ -14,8 +14,8 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
-(require 'term-config)
-(require 'testutil-ghostel-buffers)
+(require 'eat-config)
+(require 'testutil-terminal-buffers)
(ert-deftest test-term-toggle--dispatch-window-displayed-returns-toggle-off ()
"Normal: displayed terminal window -> (toggle-off . WIN)."
diff --git a/tests/test-term-toggle--display.el b/tests/test-term-toggle--display.el
index d6dd33da2..d59d23b15 100644
--- a/tests/test-term-toggle--display.el
+++ b/tests/test-term-toggle--display.el
@@ -14,7 +14,7 @@
(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-(require 'term-config)
+(require 'eat-config)
(ert-deftest test-term-toggle--capture-state-records-direction-and-size ()
"Normal: capture-state writes direction and integer size.
diff --git a/tests/test-transcription-process-and-sentinel.el b/tests/test-transcription-process-and-sentinel.el
index 90b56f0a5..185412934 100644
--- a/tests/test-transcription-process-and-sentinel.el
+++ b/tests/test-transcription-process-and-sentinel.el
@@ -96,6 +96,35 @@ the script and the audio path."
(should (equal (plist-get make-process-args :command)
(list script audio)))))
+(ert-deftest test-tx-start-process-stderr-is-a-buffer-not-a-path ()
+ "Normal: :stderr is a live buffer, not a file path.
+Passing a path string makes Emacs create a phantom buffer named after the
+path, so stderr never reaches the log file and that buffer leaks per run."
+ (let* ((audio (make-temp-file "cj-tx-audio-" nil ".mp3"))
+ (script (make-temp-file "cj-tx-script-"))
+ (cj/transcriptions-list nil)
+ make-process-args)
+ (set-file-modes script #o755)
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/--transcription-script-path)
+ (lambda () script))
+ ((symbol-function 'cj/--init-log-file) #'ignore)
+ ((symbol-function 'cj/--build-process-environment)
+ (lambda (_) '("FOO=bar")))
+ ((symbol-function 'make-process)
+ (lambda (&rest kwargs)
+ (setq make-process-args kwargs)
+ 'fake-process))
+ ((symbol-function 'cj/--notify) #'ignore)
+ ((symbol-function 'force-mode-line-update) #'ignore))
+ (cj/--start-transcription-process audio))
+ (delete-file audio)
+ (delete-file script))
+ (let ((stderr (plist-get make-process-args :stderr)))
+ (should (bufferp stderr))
+ (should (buffer-live-p stderr))
+ (when (buffer-live-p stderr) (kill-buffer stderr)))))
+
;;; cj/--transcription-sentinel
(ert-deftest test-tx-sentinel-success-writes-transcript-and-updates-status ()
@@ -105,6 +134,7 @@ the entry status to `complete', and fires a normal-urgency notification."
(log-file (make-temp-file "cj-tx-log-" nil ".log"))
(process-buffer (generate-new-buffer " *cj-tx-test*"))
(proc (list 'mock-process))
+ (stderr-buffer (generate-new-buffer " *cj-tx-test-stderr*"))
(cj/transcriptions-list (list (list proc "/tmp/audio.mp3"
(current-time) 'running)))
notify-urgency)
@@ -121,12 +151,15 @@ the entry status to `complete', and fires a normal-urgency notification."
(lambda (_t _m &optional u) (setq notify-urgency u))))
(cj/--transcription-sentinel proc "finished\n"
"/tmp/audio.mp3"
- txt-file log-file))
+ txt-file log-file stderr-buffer))
(when (buffer-live-p process-buffer) (kill-buffer process-buffer))
+ (when (buffer-live-p stderr-buffer) (kill-buffer stderr-buffer))
(delete-file txt-file)
(delete-file log-file))
;; success notification uses default (nil/normal) urgency.
(should-not notify-urgency)
+ ;; the stderr buffer is drained and killed, never leaked.
+ (should-not (buffer-live-p stderr-buffer))
;; entry status updated to complete.
(let ((entry (car cj/transcriptions-list)))
(should (eq (nth 3 entry) 'complete)))))
@@ -138,10 +171,14 @@ marks the entry as `error'."
(log-file (make-temp-file "cj-tx-log-" nil ".log"))
(process-buffer (generate-new-buffer " *cj-tx-fail*"))
(proc (list 'mock-fail))
+ (stderr-buffer (generate-new-buffer " *cj-tx-fail-stderr*"))
(cj/transcriptions-list (list (list proc "/tmp/audio.mp3"
(current-time) 'running)))
+ log-contents
notify-urgency)
- (with-current-buffer process-buffer (insert "stderr blob"))
+ (with-current-buffer process-buffer (insert "partial transcript"))
+ (with-current-buffer stderr-buffer (insert "whisper: CUDA out of memory"))
+ (with-temp-file log-file (insert "HEADER\n"))
(unwind-protect
(cl-letf (((symbol-function 'process-buffer)
(lambda (_) process-buffer))
@@ -153,11 +190,18 @@ marks the entry as `error'."
(lambda (_t _m &optional u) (setq notify-urgency u))))
(cj/--transcription-sentinel proc "exited abnormally\n"
"/tmp/audio.mp3"
- txt-file log-file))
+ txt-file log-file stderr-buffer)
+ (setq log-contents
+ (with-temp-buffer (insert-file-contents log-file) (buffer-string))))
(when (buffer-live-p process-buffer) (kill-buffer process-buffer))
+ (when (buffer-live-p stderr-buffer) (kill-buffer stderr-buffer))
(delete-file txt-file)
(delete-file log-file))
(should (eq notify-urgency 'critical))
+ ;; the actual stderr error text reaches the log on failure.
+ (should (string-match-p "CUDA out of memory" log-contents))
+ ;; the stderr buffer is killed, never leaked.
+ (should-not (buffer-live-p stderr-buffer))
(let ((entry (car cj/transcriptions-list)))
(should (eq (nth 3 entry) 'error)))))
diff --git a/tests/test-ui-navigation--window-resize.el b/tests/test-ui-navigation--window-resize.el
index 553219755..b011fb063 100644
--- a/tests/test-ui-navigation--window-resize.el
+++ b/tests/test-ui-navigation--window-resize.el
@@ -82,5 +82,50 @@ real window split happens under `--batch'."
(should (eq (keymap-lookup cj/buffer-and-file-map arrow)
#'cj/window-resize-sticky))))
+(ert-deftest test-ui-navigation-window-resize-sticky-meta-arrow-pulls-away ()
+ "Normal: M-<arrow> reaches the same pull-away as the bare arrow. The
+direction is derived with `event-basic-type', so the Meta modifier is stripped
+and a sole window pulls a sliver to the side opposite the arrow, exactly as the
+bare-arrow path does."
+ (dolist (case '((M-down . above)
+ (M-up . below)
+ (M-left . right)
+ (M-right . left)))
+ (let ((pulled nil)
+ (overriding-terminal-local-map nil)
+ (pre-command-hook nil))
+ (cl-letf (((symbol-function 'one-window-p) (lambda (&rest _) t))
+ ((symbol-function 'cj/window--pull-away)
+ (lambda (dir) (setq pulled dir))))
+ (let ((last-command-event (car case)))
+ (cj/window-resize-sticky)))
+ (should (eq pulled (cdr case))) ; meta stripped, pulled to opposite side
+ (should overriding-terminal-local-map)))) ; loop armed
+
+(ert-deftest test-ui-navigation-window-resize-sticky-meta-arrow-resizes ()
+ "Normal: with more than one window, M-<arrow> dispatches the matching
+`windsize' command, same as the bare arrow -- the Meta modifier is stripped
+before the resize-map lookup."
+ (dolist (case '((M-left . windsize-left)
+ (M-right . windsize-right)
+ (M-up . windsize-up)
+ (M-down . windsize-down)))
+ (let ((ran nil)
+ (overriding-terminal-local-map nil)
+ (pre-command-hook nil))
+ (cl-letf (((symbol-function 'one-window-p) (lambda (&rest _) nil))
+ ((symbol-function (cdr case))
+ (lambda (&rest _) (interactive) (setq ran t))))
+ (let ((last-command-event (car case)))
+ (cj/window-resize-sticky)))
+ (should ran)
+ (should overriding-terminal-local-map))))
+
+(ert-deftest test-ui-navigation-window-resize-bound-under-meta-arrow ()
+ "Normal: each global `M-<arrow>' reaches the sticky-resize command."
+ (dolist (arrow '("M-<left>" "M-<right>" "M-<up>" "M-<down>"))
+ (should (eq (keymap-lookup (current-global-map) arrow)
+ #'cj/window-resize-sticky))))
+
(provide 'test-ui-navigation--window-resize)
;;; test-ui-navigation--window-resize.el ends here
diff --git a/tests/test-undead-buffers--buffer-undead-p.el b/tests/test-undead-buffers--buffer-undead-p.el
new file mode 100644
index 000000000..e196e41a9
--- /dev/null
+++ b/tests/test-undead-buffers--buffer-undead-p.el
@@ -0,0 +1,52 @@
+;;; test-undead-buffers--buffer-undead-p.el --- undead predicate (name + regexp) -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--buffer-undead-p' decides whether a buffer name is buried instead of
+;; killed. A name is undead when it is in `cj/undead-buffer-list' (exact) or
+;; matches any regexp in `cj/undead-buffer-regexps' (dynamic families like the
+;; ai-term agent buffers, named "agent [<project>]"). `cj/make-buffer-pattern-undead'
+;; registers a regexp.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'undead-buffers)
+
+(ert-deftest test-undead-buffer-undead-p-name-list ()
+ "Normal: a name in the exact list is undead; others are not."
+ (let ((cj/undead-buffer-list '("*scratch*" "*Messages*"))
+ (cj/undead-buffer-regexps nil))
+ (should (cj/--buffer-undead-p "*scratch*"))
+ (should (cj/--buffer-undead-p "*Messages*"))
+ (should-not (cj/--buffer-undead-p "other"))))
+
+(ert-deftest test-undead-buffer-undead-p-regexp ()
+ "Normal: a name matching a regexp is undead; the agent pattern is anchored."
+ (let ((cj/undead-buffer-list nil)
+ (cj/undead-buffer-regexps '("\\`agent \\[")))
+ (should (cj/--buffer-undead-p "agent [rulesets]"))
+ (should (cj/--buffer-undead-p "agent [.emacs.d]"))
+ (should-not (cj/--buffer-undead-p "not an agent"))
+ (should-not (cj/--buffer-undead-p "my agent [x]")))) ; anchored: must start with "agent ["
+
+(ert-deftest test-undead-buffer-undead-p-neither ()
+ "Boundary/Error: a name in neither, an empty string, and a non-string are not undead."
+ (let ((cj/undead-buffer-list '("*scratch*"))
+ (cj/undead-buffer-regexps '("\\`agent \\[")))
+ (should-not (cj/--buffer-undead-p "random"))
+ (should-not (cj/--buffer-undead-p ""))
+ (should-not (cj/--buffer-undead-p nil))))
+
+(ert-deftest test-undead-make-buffer-pattern-undead-adds-and-rejects ()
+ "Normal/Error: registering a regexp makes matching names undead; a blank or
+non-string regexp signals."
+ (let ((cj/undead-buffer-regexps nil))
+ (cj/make-buffer-pattern-undead "\\`agent \\[")
+ (should (member "\\`agent \\[" cj/undead-buffer-regexps))
+ (should (cj/--buffer-undead-p "agent [x]"))
+ (should-error (cj/make-buffer-pattern-undead ""))
+ (should-error (cj/make-buffer-pattern-undead 42))))
+
+(provide 'test-undead-buffers--buffer-undead-p)
+;;; test-undead-buffers--buffer-undead-p.el ends here
diff --git a/tests/test-undead-buffers.el b/tests/test-undead-buffers.el
index d08649b7c..cd2a4176e 100644
--- a/tests/test-undead-buffers.el
+++ b/tests/test-undead-buffers.el
@@ -1,4 +1,4 @@
-;;; test-undead-buffers.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; test-undead-buffers.el --- Tests for undead buffer kill/bury behavior -*- coding: utf-8; lexical-binding: t; -*-
;;; Commentary:
;; ERT tests for undead-buffers.el.
diff --git a/tests/testutil-general.el b/tests/testutil-general.el
index 52b8a8eae..81743cad8 100644
--- a/tests/testutil-general.el
+++ b/tests/testutil-general.el
@@ -1,4 +1,4 @@
-;;; testutil-general.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; testutil-general.el --- Shared filesystem helpers for ERT tests -*- coding: utf-8; lexical-binding: t; -*-
;;
;; Author: Craig Jennings <c@cjennings.net>
;;
diff --git a/tests/testutil-ghostel-buffers.el b/tests/testutil-terminal-buffers.el
index 3c8d75d00..c2a43a3c7 100644
--- a/tests/testutil-ghostel-buffers.el
+++ b/tests/testutil-terminal-buffers.el
@@ -1,10 +1,8 @@
-;;; testutil-ghostel-buffers.el --- Shared helpers for ghostel/agent buffer tests -*- lexical-binding: t; -*-
+;;; testutil-terminal-buffers.el --- Shared helpers for terminal/agent buffer tests -*- lexical-binding: t; -*-
;;; Commentary:
-;; Cleanup helpers and a fake-ghostel constructor used across the
-;; ai-term and term-toggle test files. Replaces the older
-;; testutil-vterm-buffers helpers when the terminal engine moved from
-;; vterm to ghostel.
+;; Cleanup helpers and fake-terminal-buffer constructors (eat, eshell) used
+;; across the ai-term and term-toggle test files.
;;; Code:
@@ -13,10 +11,10 @@
(defun cj/test--call-as-gui (fn)
"Call FN, stubbing `env-terminal-p' to return nil (a GUI frame).
-The terminal refuse-guard was dropped when ghostel replaced vterm (ghostel
-renders in TTY frames too), so this no longer gates behavior; it is kept as a
-thin passthrough so window-behavior tests written against the old guard keep
-working unchanged."
+The terminal refuse-guard was dropped when the terminal engine moved off vterm
+(EAT and eshell render in TTY frames too), so this no longer gates behavior; it
+is kept as a thin passthrough so window-behavior tests written against the old
+guard keep working unchanged."
(cl-letf (((symbol-function 'env-terminal-p) (lambda () nil)))
(funcall fn)))
@@ -34,17 +32,6 @@ working unchanged."
"Kill all live buffers whose name starts with \"*test-term\"."
(cj/test--kill-buffers-matching-prefix "*test-term"))
-(defun cj/test--make-fake-ghostel-buffer (name)
- "Return a buffer named NAME with `major-mode' set to `ghostel-mode'.
-
-Avoids actually launching a ghostel process by setting the mode
-buffer-locally. Used by tests that need a buffer satisfying the
-ghostel-mode predicate without the side-effects of `(ghostel)'."
- (let ((buf (get-buffer-create name)))
- (with-current-buffer buf
- (setq-local major-mode 'ghostel-mode))
- buf))
-
(defun cj/test--make-fake-eat-buffer (name)
"Return a buffer named NAME with `major-mode' set to `eat-mode'.
@@ -56,5 +43,15 @@ predicate without the side-effects of `(eat)'."
(setq-local major-mode 'eat-mode))
buf))
-(provide 'testutil-ghostel-buffers)
-;;; testutil-ghostel-buffers.el ends here
+(defun cj/test--make-fake-eshell-buffer (name)
+ "Return a buffer named NAME with `major-mode' set to `eshell-mode'.
+
+Avoids starting a real eshell by setting the mode buffer-locally. Used by the
+F12 toggle tests that need a buffer satisfying the eshell-mode predicate."
+ (let ((buf (get-buffer-create name)))
+ (with-current-buffer buf
+ (setq-local major-mode 'eshell-mode))
+ buf))
+
+(provide 'testutil-terminal-buffers)
+;;; testutil-terminal-buffers.el ends here