aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-custom-buffer-file-keymap-bindings.el30
-rw-r--r--tests/test-dashboard-config-recentf-exclude.el33
-rw-r--r--tests/test-dwim-shell-config-command-fixes.el33
-rw-r--r--tests/test-help-config.el32
-rw-r--r--tests/test-mail-config-refile-folder.el40
-rw-r--r--tests/test-markdown-config.el10
-rw-r--r--tests/test-org-capture-config-popup-window.el281
-rw-r--r--tests/test-org-drill-config-commands.el45
-rw-r--r--tests/test-org-roam-config-dailies-head.el29
-rw-r--r--tests/test-prog-general--electric-pair-angle.el54
-rw-r--r--tests/test-reconcile--find-git-repos.el9
-rw-r--r--tests/test-selection-framework--consult-line-or-repeat.el6
-rw-r--r--tests/test-system-lib-confirm-strong.el37
-rw-r--r--tests/test-ui-navigation-split-follow-undo-kill.el29
14 files changed, 651 insertions, 17 deletions
diff --git a/tests/test-custom-buffer-file-keymap-bindings.el b/tests/test-custom-buffer-file-keymap-bindings.el
new file mode 100644
index 00000000..ea9ceb26
--- /dev/null
+++ b/tests/test-custom-buffer-file-keymap-bindings.el
@@ -0,0 +1,30 @@
+;;; test-custom-buffer-file-keymap-bindings.el --- d/D bindings in the buffer-and-file keymap -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/buffer-and-file-map' should put the destructive op on the capital key and
+;; the frequently-used op on the easy lowercase key: D = delete-buffer-and-file,
+;; d = diff-buffer-with-file. Guards the swap against silently reverting.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module (mirrors the sibling tests).
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+(provide 'ps-print)
+
+(require 'custom-buffer-file)
+
+(ert-deftest test-custom-buffer-file-keymap-diff-on-lowercase-d ()
+ "Normal: lowercase d runs diff -- the frequently-used, non-destructive op."
+ (should (eq (keymap-lookup cj/buffer-and-file-map "d") #'cj/diff-buffer-with-file)))
+
+(ert-deftest test-custom-buffer-file-keymap-delete-on-capital-d ()
+ "Normal: capital D runs delete -- the destructive op on the capital key."
+ (should (eq (keymap-lookup cj/buffer-and-file-map "D") #'cj/delete-buffer-and-file)))
+
+(provide 'test-custom-buffer-file-keymap-bindings)
+;;; test-custom-buffer-file-keymap-bindings.el ends here
diff --git a/tests/test-dashboard-config-recentf-exclude.el b/tests/test-dashboard-config-recentf-exclude.el
new file mode 100644
index 00000000..f35b3eda
--- /dev/null
+++ b/tests/test-dashboard-config-recentf-exclude.el
@@ -0,0 +1,33 @@
+;;; test-dashboard-config-recentf-exclude.el --- recentf-exclude is not clobbered -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--dashboard-exclude-emms-from-recentf' adds the EMMS history pattern
+;; to `recentf-exclude'. It must ADD to the list, not replace it, or it
+;; wipes the exclusions system-defaults.el set earlier in init order
+;; (emacs_bookmarks, elpa, recentf, ElfeedDB, airootfs).
+
+;;; Code:
+
+(require 'ert)
+(require 'recentf) ; makes `recentf-exclude' special so the let below is dynamic
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'testutil-general)
+(require 'dashboard-config)
+
+(ert-deftest test-dashboard-config-exclude-emms-preserves-existing-entries ()
+ "Error: excluding the EMMS history preserves prior recentf-exclude entries."
+ (let ((recentf-exclude (list "emacs_bookmarks" "airootfs")))
+ (cj/--dashboard-exclude-emms-from-recentf)
+ (should (member "/emms/history" recentf-exclude))
+ (should (member "emacs_bookmarks" recentf-exclude))
+ (should (member "airootfs" recentf-exclude))))
+
+(ert-deftest test-dashboard-config-exclude-emms-adds-the-pattern ()
+ "Normal: the EMMS history pattern is present after the call."
+ (let ((recentf-exclude nil))
+ (cj/--dashboard-exclude-emms-from-recentf)
+ (should (member "/emms/history" recentf-exclude))))
+
+(provide 'test-dashboard-config-recentf-exclude)
+;;; test-dashboard-config-recentf-exclude.el ends here
diff --git a/tests/test-dwim-shell-config-command-fixes.el b/tests/test-dwim-shell-config-command-fixes.el
new file mode 100644
index 00000000..2f49a868
--- /dev/null
+++ b/tests/test-dwim-shell-config-command-fixes.el
@@ -0,0 +1,33 @@
+;;; test-dwim-shell-config-command-fixes.el --- zip/backup command builders -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Two audit fixes, extracted into top-level command-string builders so they're
+;; testable without loading the dwim-shell-command package (the command defuns
+;; that call them live inside its use-package :config, which the batch test
+;; harness doesn't instantiate):
+;; - cj/dwim-shell--zip-single-file-command names the archive <fne>.zip
+;; - cj/dwim-shell--dated-backup-command carries a real timestamp, not "$(date)"
+;; The third fix (dired menu key M-S-d -> M-D) is a keybinding inside the same
+;; :config block; it's verified in the live daemon, not here.
+
+;;; Code:
+
+(require 'ert)
+(require 'dwim-shell-config)
+
+(ert-deftest test-dwim-zip-single-file-command-names-archive-dot-zip ()
+ "Normal: the single-file zip template names the archive <fne>.zip, with no
+leftover <<e>> that would rebuild the input filename."
+ (let ((cmd (cj/dwim-shell--zip-single-file-command)))
+ (should (string-match-p "'<<fne>>\\.zip'" cmd))
+ (should-not (string-match-p "<<e>>" cmd))))
+
+(ert-deftest test-dwim-dated-backup-command-carries-real-timestamp ()
+ "Normal: the dated-backup template interpolates a real YYYYMMDD_HHMMSS stamp,
+so the substitution can't sit dead inside single quotes."
+ (let ((cmd (cj/dwim-shell--dated-backup-command)))
+ (should (string-match-p "\\.[0-9]\\{8\\}_[0-9]\\{6\\}\\.bak'" cmd))
+ (should-not (string-match-p "\\$(date" cmd))))
+
+(provide 'test-dwim-shell-config-command-fixes)
+;;; test-dwim-shell-config-command-fixes.el ends here
diff --git a/tests/test-help-config.el b/tests/test-help-config.el
new file mode 100644
index 00000000..0ba95c41
--- /dev/null
+++ b/tests/test-help-config.el
@@ -0,0 +1,32 @@
+;;; test-help-config.el --- Tests for the Info-open decision logic -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; cj/open-with-info-mode opens the current .info buffer in Info, prompting to
+;; save first if the buffer is modified. The save/cancel/open decision is
+;; factored into the pure helper `cj/--info-open-plan' so it's testable without
+;; driving find-file, Info, or the save prompt. Declining the prompt must yield
+;; `cancel' -- the original cl-return-from inside a plain defun signalled
+;; "No catch for tag" instead of cancelling.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'help-config)
+
+(ert-deftest test-info-open-plan-unmodified-opens ()
+ "Normal: an unmodified buffer opens in Info directly."
+ (should (eq (cj/--info-open-plan nil nil) 'open)))
+
+(ert-deftest test-info-open-plan-modified-confirmed-saves-then-opens ()
+ "Normal: a modified buffer whose save is confirmed saves, then opens."
+ (should (eq (cj/--info-open-plan t t) 'save-then-open)))
+
+(ert-deftest test-info-open-plan-modified-declined-cancels ()
+ "Error/edge: a modified buffer whose save is declined cancels -- the path that
+used to signal \"No catch for tag\" via cl-return-from in a plain defun."
+ (should (eq (cj/--info-open-plan t nil) 'cancel)))
+
+(provide 'test-help-config)
+;;; test-help-config.el ends here
diff --git a/tests/test-mail-config-refile-folder.el b/tests/test-mail-config-refile-folder.el
new file mode 100644
index 00000000..e2d224eb
--- /dev/null
+++ b/tests/test-mail-config-refile-folder.el
@@ -0,0 +1,40 @@
+;;; test-mail-config-refile-folder.el --- Tests for refile-folder dispatch -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; ERT tests for `cj/mu4e--refile-folder-for-maildir', the per-message refile
+;; (archive) target dispatch. cmail has a real synced Archive folder; the
+;; Gmail-backed accounts (gmail, dmail) have none, so refiling them must signal
+;; rather than move mail into an unsynced, phantom folder (silent mail loss).
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'mail-config)
+
+(ert-deftest test-mail-config-refile-cmail-returns-archive ()
+ "Normal: a cmail message refiles into the synced /cmail/Archive folder."
+ (should (string= (cj/mu4e--refile-folder-for-maildir "/cmail/INBOX")
+ "/cmail/Archive"))
+ (should (string= (cj/mu4e--refile-folder-for-maildir "/cmail/Sent")
+ "/cmail/Archive")))
+
+(ert-deftest test-mail-config-refile-gmail-signals ()
+ "Error: gmail has no synced archive folder, so refile signals rather than
+moving mail into a phantom folder."
+ (should-error (cj/mu4e--refile-folder-for-maildir "/gmail/INBOX")
+ :type 'user-error))
+
+(ert-deftest test-mail-config-refile-dmail-signals ()
+ "Error: dmail (Gmail-backed) has no synced archive folder; refile signals."
+ (should-error (cj/mu4e--refile-folder-for-maildir "/dmail/INBOX")
+ :type 'user-error))
+
+(ert-deftest test-mail-config-refile-nil-maildir-signals ()
+ "Boundary: a message with no maildir cannot be refiled; signal."
+ (should-error (cj/mu4e--refile-folder-for-maildir nil)
+ :type 'user-error))
+
+(provide 'test-mail-config-refile-folder)
+;;; test-mail-config-refile-folder.el ends here
diff --git a/tests/test-markdown-config.el b/tests/test-markdown-config.el
index 45e1a601..edb20d35 100644
--- a/tests/test-markdown-config.el
+++ b/tests/test-markdown-config.el
@@ -9,6 +9,7 @@
;;; Code:
(require 'ert)
+(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
@@ -50,5 +51,14 @@
(should (string-match-p "<xmp" (buffer-string))))
(kill-buffer src))))
+;;; cj/markdown-preview (guard: refuse when the httpd listener is down)
+
+(ert-deftest test-markdown-preview-errors-when-server-down ()
+ "Error: `cj/markdown-preview' signals a user-error when the simple-httpd
+listener is not running, rather than opening a preview against a dead server.
+Also pins the rename off the bare `markdown-preview' that markdown-mode shadows."
+ (cl-letf (((symbol-function 'httpd-running-p) (lambda () nil)))
+ (should-error (cj/markdown-preview) :type 'user-error)))
+
(provide 'test-markdown-config)
;;; test-markdown-config.el ends here
diff --git a/tests/test-org-capture-config-popup-window.el b/tests/test-org-capture-config-popup-window.el
new file mode 100644
index 00000000..34f67b36
--- /dev/null
+++ b/tests/test-org-capture-config-popup-window.el
@@ -0,0 +1,281 @@
+;;; test-org-capture-config-popup-window.el --- Quick-capture popup single-window tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the pure predicate behind the quick-capture popup single-window
+;; fix. The Hyprland Super+Shift+N popup opens an emacsclient frame named
+;; "org-capture"; in that frame the *Org Select* template menu and the
+;; CAPTURE-* buffer must fill the frame's sole window instead of splitting it.
+;; `cj/org-capture--popup-sole-window-p' is the frame+buffer decision; the
+;; display-buffer action that acts on it is exercised by hand (window ops),
+;; not here.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'org)
+(require 'org-capture) ; makes `org-capture-templates' a real special var
+(require 'user-constants)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-capture-config)
+
+(defconst test-org-capture-popup--sample-templates
+ '(("t" "Task" entry (function cj/--org-capture-project-location)
+ "* TODO %?" :prepend t)
+ ("b" "Bug" entry (function cj/--org-capture-project-location)
+ "* TODO [#C] %?" :prepend t)
+ ("e" "Event" entry (file+headline schedule-file "Scheduled Events")
+ "* %?" :prepend t :prepare-finalize cj/org-capture-format-event-headline)
+ ("m" "Mu4e Email" entry (file+headline inbox-file "Inbox") "* TODO %?" :prepend t)
+ ("L" "Link" entry (file+headline inbox-file "Inbox") "* %?" :immediate-finish t)
+ ("d" "Drill Question" entry (file ignore) "* Item :drill:\n%?" :prepend t))
+ "A representative org-capture-templates list for popup-subset tests.")
+
+;;; cj/org-capture--popup-sole-window-p
+
+(ert-deftest test-org-capture-config-popup-sole-window-p-select-menu ()
+ "Normal: the *Org Select* menu in the popup frame wants the sole window."
+ (should (cj/org-capture--popup-sole-window-p "org-capture" "*Org Select*")))
+
+(ert-deftest test-org-capture-config-popup-sole-window-p-capture-buffer ()
+ "Normal: a CAPTURE-* buffer in the popup frame wants the sole window."
+ (should (cj/org-capture--popup-sole-window-p "org-capture" "CAPTURE-todo.org")))
+
+(ert-deftest test-org-capture-config-popup-sole-window-p-capture-prefix-only ()
+ "Boundary: the bare \"CAPTURE-\" prefix still matches."
+ (should (cj/org-capture--popup-sole-window-p "org-capture" "CAPTURE-")))
+
+(ert-deftest test-org-capture-config-popup-sole-window-p-other-frame ()
+ "Boundary: the same menu in a normal frame is left alone."
+ (should-not (cj/org-capture--popup-sole-window-p "emacs" "*Org Select*"))
+ (should-not (cj/org-capture--popup-sole-window-p nil "CAPTURE-todo.org")))
+
+(ert-deftest test-org-capture-config-popup-sole-window-p-other-buffer ()
+ "Boundary: an unrelated buffer in the popup frame is left alone."
+ (should-not (cj/org-capture--popup-sole-window-p "org-capture" "todo.org"))
+ (should-not (cj/org-capture--popup-sole-window-p "org-capture" "*scratch*")))
+
+(ert-deftest test-org-capture-config-popup-sole-window-p-nil-buffer ()
+ "Error: a nil or non-string buffer name returns nil without raising."
+ (should-not (cj/org-capture--popup-sole-window-p "org-capture" nil))
+ (should-not (cj/org-capture--popup-sole-window-p "org-capture" 42)))
+
+;;; Integration: the display-buffer-alist entry routes to a sole window
+
+(ert-deftest test-integration-org-capture-popup-display-sole-window ()
+ "Integration: in an \"org-capture\"-named frame, displaying a CAPTURE-*
+buffer fills the frame's sole window via the registered display-buffer-alist
+entry, instead of splitting.
+
+Components integrated:
+- cj/org-capture--popup-display-condition (real)
+- cj/org-capture--display-sole-window (real)
+- display-buffer / display-buffer-alist (real)
+
+Validates the popup frame ends with one window showing the CAPTURE buffer."
+ ;; The batch frame is auto-named (\"F1\"), which cannot be restored by name
+ ;; (\"F<num> usurped by Emacs\"); reset to nil to return it to auto-naming,
+ ;; keeping the test independent of execution order.
+ (let ((buf (get-buffer-create "CAPTURE-itest")))
+ (unwind-protect
+ (progn
+ (set-frame-parameter nil 'name "org-capture")
+ (delete-other-windows)
+ (display-buffer buf)
+ (should (= (length (window-list)) 1))
+ (should (eq (window-buffer (selected-window)) buf)))
+ (set-frame-parameter nil 'name nil)
+ (when (buffer-live-p buf) (kill-buffer buf)))))
+
+;;; cj/--org-capture-popup-templates (pure subset/retarget)
+
+(ert-deftest test-org-capture-config-popup-templates-keeps-tbe ()
+ "Normal: only Task, Bug, Event survive, preserving order."
+ (should (equal (mapcar #'car (cj/--org-capture-popup-templates
+ test-org-capture-popup--sample-templates "/inbox.org"))
+ '("t" "b" "e"))))
+
+(ert-deftest test-org-capture-config-popup-templates-retargets-task-bug ()
+ "Normal: Task and Bug retarget to the inbox \"Inbox\" headline; body + props kept."
+ (let* ((result (cj/--org-capture-popup-templates
+ test-org-capture-popup--sample-templates "/inbox.org"))
+ (task (assoc "t" result))
+ (bug (assoc "b" result)))
+ (should (equal (nth 3 task) '(file+headline "/inbox.org" "Inbox")))
+ (should (equal (nth 3 bug) '(file+headline "/inbox.org" "Inbox")))
+ (should (equal (nth 4 task) "* TODO %?"))
+ (should (equal (nth 4 bug) "* TODO [#C] %?"))
+ (should (memq :prepend task))))
+
+(ert-deftest test-org-capture-config-popup-templates-event-unchanged ()
+ "Boundary: Event passes through untouched, schedule-file target and props intact."
+ (let ((event (assoc "e" (cj/--org-capture-popup-templates
+ test-org-capture-popup--sample-templates "/inbox.org"))))
+ (should (equal (nth 3 event) '(file+headline schedule-file "Scheduled Events")))
+ (should (memq :prepare-finalize event))))
+
+(ert-deftest test-org-capture-config-popup-templates-drops-context-templates ()
+ "Boundary: context-dependent templates (mu4e, link, drill) are dropped."
+ (let ((result (cj/--org-capture-popup-templates
+ test-org-capture-popup--sample-templates "/inbox.org")))
+ (should-not (assoc "m" result))
+ (should-not (assoc "L" result))
+ (should-not (assoc "d" result))))
+
+(ert-deftest test-org-capture-config-popup-templates-empty ()
+ "Error/Boundary: empty or all-dropped input yields nil without raising."
+ (should-not (cj/--org-capture-popup-templates nil "/inbox.org"))
+ (should-not (cj/--org-capture-popup-templates
+ '(("L" "Link" entry (file+headline f "Inbox") "* %?")) "/inbox.org")))
+
+;;; cj/quick-capture (binds the subset; integration with a stubbed org-capture)
+
+(ert-deftest test-integration-org-capture-quick-capture-binds-subset ()
+ "Integration: cj/quick-capture runs org-capture with only Task/Bug/Event,
+Task and Bug retargeted to the inbox.
+
+Components integrated:
+- cj/quick-capture (real)
+- cj/--org-capture-popup-templates (real)
+- org-capture (MOCKED — records the dynamically-bound templates)"
+ (let ((org-capture-templates test-org-capture-popup--sample-templates)
+ captured)
+ (cl-letf (((symbol-function 'org-capture)
+ (lambda (&rest _) (setq captured org-capture-templates))))
+ (cj/quick-capture))
+ (should (equal (mapcar #'car captured) '("t" "b" "e")))
+ (should (equal (nth 3 (assoc "t" captured)) (list 'file+headline inbox-file "Inbox")))
+ (should (equal (nth 3 (assoc "b" captured)) (list 'file+headline inbox-file "Inbox")))))
+
+(ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-abort ()
+ "Integration: when selection aborts (org-capture signals), cj/quick-capture
+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)"
+ (let ((org-capture-templates test-org-capture-popup--sample-templates)
+ (deleted 0))
+ (cl-letf (((symbol-function 'org-capture)
+ (lambda (&rest _) (user-error "Abort")))
+ ((symbol-function 'cj/org-capture--delete-popup-frame)
+ (lambda () (cl-incf deleted))))
+ (cj/quick-capture))
+ (should (= deleted 1))))
+
+(ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-quit ()
+ "Integration: a C-g (quit) during capture also closes the popup frame."
+ (let ((org-capture-templates test-org-capture-popup--sample-templates)
+ (deleted 0))
+ (cl-letf (((symbol-function 'org-capture)
+ (lambda (&rest _) (signal 'quit nil)))
+ ((symbol-function 'cj/org-capture--delete-popup-frame)
+ (lambda () (cl-incf deleted))))
+ (cj/quick-capture))
+ (should (= deleted 1))))
+
+(ert-deftest test-integration-org-capture-quick-capture-keeps-frame-on-success ()
+ "Integration: a successful capture (no signal) does NOT delete the frame —
+the finalize hook owns that."
+ (let ((org-capture-templates test-org-capture-popup--sample-templates)
+ (deleted 0))
+ (cl-letf (((symbol-function 'org-capture) (lambda (&rest _) nil))
+ ((symbol-function 'cj/org-capture--delete-popup-frame)
+ (lambda () (cl-incf deleted))))
+ (cj/quick-capture))
+ (should (= deleted 0))))
+
+;;; cj/--org-capture-popup-strip-specials (drop the Customize menu entry)
+
+(ert-deftest test-org-capture-config-popup-strip-specials-removes-customize ()
+ "Normal: the \"C\" Customize entry is removed, \"q\" Abort kept, order intact."
+ (should (equal (cj/--org-capture-popup-strip-specials
+ '(("C" "Customize org-capture-templates") ("q" "Abort")))
+ '(("q" "Abort")))))
+
+(ert-deftest test-org-capture-config-popup-strip-specials-no-customize ()
+ "Boundary: specials without a \"C\" entry pass through unchanged."
+ (should (equal (cj/--org-capture-popup-strip-specials '(("q" "Abort")))
+ '(("q" "Abort")))))
+
+(ert-deftest test-org-capture-config-popup-strip-specials-empty ()
+ "Error/Boundary: nil specials yields nil without raising."
+ (should-not (cj/--org-capture-popup-strip-specials nil)))
+
+;;; cj/org-capture--popup-frame-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))))
+
+;;; cj/org-capture--popup-mks-advice (frame-gated specials stripping)
+
+(ert-deftest test-org-capture-config-popup-mks-advice-strips-in-popup ()
+ "Integration: in the popup frame, org-mks receives specials without \"C\"."
+ (let (seen)
+ (cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () t)))
+ (cj/org-capture--popup-mks-advice
+ (lambda (_table _title _prompt specials) (setq seen specials))
+ nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort"))))
+ (should (equal seen '(("q" "Abort"))))))
+
+(ert-deftest test-org-capture-config-popup-mks-advice-keeps-elsewhere ()
+ "Integration: in a normal frame, org-mks receives the specials untouched."
+ (let (seen)
+ (cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () nil)))
+ (cj/org-capture--popup-mks-advice
+ (lambda (_table _title _prompt specials) (setq seen specials))
+ nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort"))))
+ (should (equal seen '(("C" "Customize org-capture-templates") ("q" "Abort"))))))
+
+;;; cj/org-capture--popup-frame (find the popup frame by name)
+
+(ert-deftest test-org-capture-config-popup-frame-found ()
+ "Normal: returns the live frame whose name is \"org-capture\"."
+ (cl-letf (((symbol-function 'frame-list) (lambda () '(fa fb fc)))
+ ((symbol-function 'frame-live-p) (lambda (_f) t))
+ ((symbol-function 'frame-parameter)
+ (lambda (f _p) (if (eq f 'fb) "org-capture" "other"))))
+ (should (eq (cj/org-capture--popup-frame) 'fb))))
+
+(ert-deftest test-org-capture-config-popup-frame-none ()
+ "Boundary: no popup frame present yields nil."
+ (cl-letf (((symbol-function 'frame-list) (lambda () '(fa fc)))
+ ((symbol-function 'frame-live-p) (lambda (_f) t))
+ ((symbol-function 'frame-parameter) (lambda (_f _p) "other")))
+ (should-not (cj/org-capture--popup-frame))))
+
+;;; cj/quick-capture targets the popup frame
+
+(ert-deftest test-integration-org-capture-quick-capture-selects-named-frame ()
+ "Integration: cj/quick-capture selects the \"org-capture\" frame found by name,
+not whatever frame happens to be selected (the emacsclient -c focus race)."
+ (let ((org-capture-templates test-org-capture-popup--sample-templates)
+ (focused nil))
+ (cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () 'popup-frame))
+ ((symbol-function 'select-frame-set-input-focus)
+ (lambda (f) (setq focused f)))
+ ((symbol-function 'org-capture) (lambda (&rest _) nil)))
+ (cj/quick-capture))
+ (should (eq focused 'popup-frame))))
+
+(ert-deftest test-integration-org-capture-quick-capture-no-frame-still-captures ()
+ "Integration: when no popup frame is found, cj/quick-capture skips the focus
+call and still runs the capture (no error)."
+ (let ((org-capture-templates test-org-capture-popup--sample-templates)
+ (focused 'unset)
+ (captured nil))
+ (cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () nil))
+ ((symbol-function 'select-frame-set-input-focus)
+ (lambda (f) (setq focused f)))
+ ((symbol-function 'org-capture) (lambda (&rest _) (setq captured t))))
+ (cj/quick-capture))
+ (should (eq focused 'unset))
+ (should captured)))
+
+(provide 'test-org-capture-config-popup-window)
+;;; test-org-capture-config-popup-window.el ends here
diff --git a/tests/test-org-drill-config-commands.el b/tests/test-org-drill-config-commands.el
index 7d197616..c35bd6cd 100644
--- a/tests/test-org-drill-config-commands.el
+++ b/tests/test-org-drill-config-commands.el
@@ -71,21 +71,50 @@
;;; cj/drill-refile
-(ert-deftest test-org-drill-refile-sets-targets-and-delegates ()
- "Normal: drill-refile narrows `org-refile-targets' to current buffer +
-`drill-dir', then dispatches to `org-refile' via `call-interactively'."
- (let (seen-targets called-fn)
- (cl-letf (((symbol-function 'call-interactively)
+(ert-deftest test-org-drill-refile-targets-from-validated-helper ()
+ "Normal: drill-refile builds its drill targets from the shared
+`cj/--drill-files-or-error' helper, expanded against `drill-dir' — not from
+a raw `directory-files' call (so it inherits the helper's dot-file exclusion
+and validation)."
+ (let ((drill-dir "/tmp/cj-drill/")
+ seen-targets called-fn)
+ (cl-letf (((symbol-function 'cj/--drill-files-or-error)
+ (lambda (_dir) '("a.org" "b.org")))
+ ;; If the old raw path were still in use it would call
+ ;; `directory-files'; a sentinel here keeps it from masquerading.
+ ((symbol-function 'directory-files)
+ (lambda (&rest _) '("/WRONG/raw.org")))
+ ((symbol-function 'call-interactively)
(lambda (fn)
(setq called-fn fn
seen-targets org-refile-targets))))
(cj/drill-refile))
(should (eq called-fn 'org-refile))
- (should seen-targets)
- ;; Two entries: (nil :maxlevel . 1) and (drill-dir :maxlevel . 1).
(should (= 2 (length seen-targets)))
(should (assoc nil seen-targets))
- (should (assoc 'drill-dir seen-targets))))
+ (should (equal (car (nth 1 seen-targets))
+ '("/tmp/cj-drill/a.org" "/tmp/cj-drill/b.org")))))
+
+(ert-deftest test-org-drill-refile-does-not-clobber-global-targets ()
+ "Error: drill-refile let-binds `org-refile-targets'; the session-wide value
+survives the call instead of being permanently replaced."
+ (let ((drill-dir "/tmp/cj-drill/")
+ (org-refile-targets '((sentinel :maxlevel . 9))))
+ (cl-letf (((symbol-function 'cj/--drill-files-or-error) (lambda (_dir) '("a.org")))
+ ((symbol-function 'call-interactively) (lambda (_fn) nil)))
+ (cj/drill-refile))
+ (should (equal org-refile-targets '((sentinel :maxlevel . 9))))))
+
+(ert-deftest test-org-drill-refile-errors-on-missing-drill-dir ()
+ "Error: a missing or unreadable drill dir signals a clear `user-error' via
+the shared validated helper, instead of a low-level error, and never reaches
+`org-refile'."
+ (let ((drill-dir (expand-file-name "cj-drill-nonexistent-XYZ/"
+ temporary-file-directory))
+ (called nil))
+ (cl-letf (((symbol-function 'call-interactively) (lambda (_fn) (setq called t))))
+ (should-error (cj/drill-refile) :type 'user-error))
+ (should-not called)))
(provide 'test-org-drill-config-commands)
;;; test-org-drill-config-commands.el ends here
diff --git a/tests/test-org-roam-config-dailies-head.el b/tests/test-org-roam-config-dailies-head.el
new file mode 100644
index 00000000..631f017c
--- /dev/null
+++ b/tests/test-org-roam-config-dailies-head.el
@@ -0,0 +1,29 @@
+;;; test-org-roam-config-dailies-head.el --- Tests for the dailies template head -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--org-roam-dailies-head' is the head inserted into a new org-roam
+;; daily file. #+FILETAGS and #+TITLE must sit on separate lines, or Org
+;; never parses the #+TITLE keyword and the FILETAGS value swallows the
+;; rest of the line.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-roam-config)
+
+(ert-deftest test-org-roam-config-dailies-head-separates-filetags-and-title ()
+ "Boundary: #+FILETAGS and #+TITLE sit on separate lines."
+ (should (string-match-p "#\\+FILETAGS: Journal\n#\\+TITLE:"
+ cj/--org-roam-dailies-head))
+ ;; And never run together on one line.
+ (should-not (string-match-p "Journal #\\+TITLE:" cj/--org-roam-dailies-head)))
+
+(ert-deftest test-org-roam-config-dailies-head-ends-with-newline ()
+ "Boundary: the head ends with a newline so the capture body starts clean."
+ (should (string-suffix-p "\n" cj/--org-roam-dailies-head)))
+
+(provide 'test-org-roam-config-dailies-head)
+;;; test-org-roam-config-dailies-head.el ends here
diff --git a/tests/test-prog-general--electric-pair-angle.el b/tests/test-prog-general--electric-pair-angle.el
new file mode 100644
index 00000000..cb33725a
--- /dev/null
+++ b/tests/test-prog-general--electric-pair-angle.el
@@ -0,0 +1,54 @@
+;;; test-prog-general--electric-pair-angle.el --- Angle-bracket pairing inhibit -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--electric-pair-inhibit-angle, which stops electric-pair from
+;; pairing "<" into "<>". Craig's yasnippet keys start with "<" (e.g. <cj);
+;; auto-pairing the "<" strands a ">" after the expanded snippet, which broke
+;; the cj-comment close fence into "#+end_src>".
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'elec-pair)
+(require 'org)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'prog-general)
+
+;;; cj/--electric-pair-inhibit-angle
+
+(ert-deftest test-prog-general-electric-pair-inhibit-angle-open ()
+ "Normal: the open angle bracket is inhibited."
+ (should (cj/--electric-pair-inhibit-angle ?<)))
+
+(ert-deftest test-prog-general-electric-pair-inhibit-angle-delegates ()
+ "Boundary: any other character defers to electric-pair-default-inhibit."
+ (cl-letf (((symbol-function 'electric-pair-default-inhibit)
+ (lambda (_c) 'delegated)))
+ (should (eq (cj/--electric-pair-inhibit-angle ?a) 'delegated))
+ (should (eq (cj/--electric-pair-inhibit-angle ?\() 'delegated))))
+
+(ert-deftest test-prog-general-electric-pair-predicate-installed ()
+ "Normal: prog-general installs the predicate as the global value."
+ (should (eq electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle)))
+
+;;; Integration — the actual pairing behavior
+
+(ert-deftest test-integration-prog-general-angle-not-paired-in-org ()
+ "Integration: in an org buffer (where < has paren syntax), typing < with the
+inhibit predicate active inserts just <, not <>.
+
+Components integrated:
+- cj/--electric-pair-inhibit-angle (real)
+- electric-pair-local-mode / self-insert-command (real)
+- org-mode syntax table (real — gives < paren syntax)"
+ (with-temp-buffer
+ (org-mode)
+ (electric-pair-local-mode 1)
+ (setq-local electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle)
+ (let ((last-command-event ?<))
+ (call-interactively #'self-insert-command))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max)) "<"))))
+
+(provide 'test-prog-general--electric-pair-angle)
+;;; test-prog-general--electric-pair-angle.el ends here
diff --git a/tests/test-reconcile--find-git-repos.el b/tests/test-reconcile--find-git-repos.el
index e065fca9..c6a190a1 100644
--- a/tests/test-reconcile--find-git-repos.el
+++ b/tests/test-reconcile--find-git-repos.el
@@ -81,6 +81,15 @@
(should (= (length repos) 1))
(should (string-suffix-p "visible-repo" (car repos))))))
+(ert-deftest test-find-git-repos-boundary-dotted-repo-name-found ()
+ "Boundary: a repo whose directory name contains a dot (e.g. mcp.el) is
+discovered. Regression for the `^[^.]+$' filter that matched only dot-free
+names and silently skipped dotted repos like mcp.el / capture.el."
+ (reconcile-test-with-temp-dirs
+ ("mcp.el/.git/" "capture.el/.git/" "plain-repo/.git/")
+ (let ((repos (cj/find-git-repos test-root)))
+ (should (= (length repos) 3)))))
+
(ert-deftest test-find-git-repos-boundary-prunes-heavy-directories ()
"Skips generated/heavy directories while discovering repos."
(reconcile-test-with-temp-dirs
diff --git a/tests/test-selection-framework--consult-line-or-repeat.el b/tests/test-selection-framework--consult-line-or-repeat.el
index fcaddcfd..66f5b172 100644
--- a/tests/test-selection-framework--consult-line-or-repeat.el
+++ b/tests/test-selection-framework--consult-line-or-repeat.el
@@ -64,5 +64,11 @@
"Normal: `cj/consult-line-or-repeat' is an interactive command."
(should (commandp #'cj/consult-line-or-repeat)))
+(ert-deftest test-selection-framework-vertico-repeat-save-on-minibuffer-setup ()
+ "Normal: loading the module registers `vertico-repeat-save' on
+`minibuffer-setup-hook'. Without it `vertico-repeat' has no saved session
+and the second C-s signals \"No Vertico session\"."
+ (should (memq 'vertico-repeat-save minibuffer-setup-hook)))
+
(provide 'test-selection-framework--consult-line-or-repeat)
;;; test-selection-framework--consult-line-or-repeat.el ends here
diff --git a/tests/test-system-lib-confirm-strong.el b/tests/test-system-lib-confirm-strong.el
new file mode 100644
index 00000000..26c00822
--- /dev/null
+++ b/tests/test-system-lib-confirm-strong.el
@@ -0,0 +1,37 @@
+;;; test-system-lib-confirm-strong.el --- Tests for cj/confirm-strong -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; ERT tests for `cj/confirm-strong', the typed-"yes" confirmation used for
+;; irreversible actions. The behavior under test is the long-form guarantee:
+;; the prompt demands a typed yes/no even when the global single-key default
+;; (`use-short-answers') is in effect.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'system-lib)
+
+(ert-deftest test-system-lib-confirm-strong-returns-t-on-yes ()
+ "Normal: passes a t answer through from `yes-or-no-p'."
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)))
+ (should (eq (cj/confirm-strong "Really? ") t))))
+
+(ert-deftest test-system-lib-confirm-strong-returns-nil-on-no ()
+ "Normal: passes a nil answer through from `yes-or-no-p'."
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) nil)))
+ (should (eq (cj/confirm-strong "Really? ") nil))))
+
+(ert-deftest test-system-lib-confirm-strong-forces-long-form ()
+ "Boundary: binds `use-short-answers' to nil for the call even when it is
+globally t, so the irreversible prompt requires a typed yes/no regardless of
+the single-key default."
+ (let ((use-short-answers t)
+ (seen 'unset))
+ (cl-letf (((symbol-function 'yes-or-no-p)
+ (lambda (&rest _) (setq seen use-short-answers) t)))
+ (cj/confirm-strong "Really? ")
+ (should (eq seen nil)))))
+
+(provide 'test-system-lib-confirm-strong)
+;;; test-system-lib-confirm-strong.el ends here
diff --git a/tests/test-ui-navigation-split-follow-undo-kill.el b/tests/test-ui-navigation-split-follow-undo-kill.el
index 74c1e2fc..8e390074 100644
--- a/tests/test-ui-navigation-split-follow-undo-kill.el
+++ b/tests/test-ui-navigation-split-follow-undo-kill.el
@@ -54,8 +54,9 @@
;;; cj/undo-kill-buffer
-(ert-deftest test-ui-navigation-undo-kill-buffer-opens-most-recent ()
- "Normal: with no arg, opens the head of recentf-list that isn't currently visited."
+(ert-deftest test-ui-navigation-undo-kill-buffer-no-prefix-opens-most-recent ()
+ "Normal: no prefix (arg=1, the value `\"p\"' yields) opens the most-recent
+non-visited entry, not the second."
(let ((opened nil)
(recentf-mode t)
(recentf-list '("/tmp/dead.org" "/tmp/alive.txt")))
@@ -71,12 +72,12 @@
((symbol-function 'find-file)
(lambda (f) (setq opened f))))
(unwind-protect
- (cj/undo-kill-buffer 0)
+ (cj/undo-kill-buffer 1)
(when (get-buffer "*test-alive*") (kill-buffer "*test-alive*"))))
(should (equal opened "/tmp/dead.org"))))
-(ert-deftest test-ui-navigation-undo-kill-buffer-honors-numeric-arg ()
- "Normal: with N=1, opens the second non-visited entry from recentf-list."
+(ert-deftest test-ui-navigation-undo-kill-buffer-numeric-arg-is-one-based ()
+ "Normal: a numeric prefix is 1-based — N=2 opens the second non-visited entry."
(let ((opened nil)
(recentf-mode t)
(recentf-list '("/tmp/a.org" "/tmp/b.org" "/tmp/c.org")))
@@ -85,10 +86,7 @@
((symbol-function 'buffer-list) (lambda (&rest _) nil))
((symbol-function 'find-file)
(lambda (f) (setq opened f))))
- ;; cj/undo-kill-buffer takes a prefix `arg' and indexes into the list
- ;; with `(nth arg ...)` when arg is non-nil. Passing 1 grabs the 2nd
- ;; entry.
- (cj/undo-kill-buffer 1))
+ (cj/undo-kill-buffer 2))
(should (equal opened "/tmp/b.org"))))
(ert-deftest test-ui-navigation-undo-kill-buffer-no-op-when-list-empty ()
@@ -104,5 +102,18 @@
(cj/undo-kill-buffer 0))
(should-not opened)))
+(ert-deftest test-ui-navigation-undo-kill-buffer-out-of-range-arg-errors ()
+ "Error: a prefix larger than the killed-file list signals a clear user-error,
+not a wrong-type-argument from find-file on nil."
+ (let ((opened nil)
+ (recentf-mode t)
+ (recentf-list '("/tmp/a.org")))
+ (cl-letf (((symbol-function 'require) (lambda (&rest _) t))
+ ((symbol-function 'recentf-mode) (lambda (&rest _) t))
+ ((symbol-function 'buffer-list) (lambda (&rest _) nil))
+ ((symbol-function 'find-file) (lambda (f) (setq opened f))))
+ (should-error (cj/undo-kill-buffer 5) :type 'user-error))
+ (should-not opened)))
+
(provide 'test-ui-navigation-split-follow-undo-kill)
;;; test-ui-navigation-split-follow-undo-kill.el ends here