aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/calendar-sync-source.el69
-rw-r--r--modules/flyspell-and-abbrev.el7
-rw-r--r--modules/keyboard-macros.el12
-rw-r--r--modules/lorem-optimum.el18
-rw-r--r--modules/org-babel-config.el3
-rw-r--r--modules/org-contacts-config.el37
-rw-r--r--modules/org-drill-config.el55
-rw-r--r--modules/org-export-config.el16
-rw-r--r--modules/org-reveal-config.el33
-rw-r--r--modules/org-webclipper.el20
-rw-r--r--modules/text-config.el6
-rw-r--r--tests/test-calendar-sync-source-fetch-sentinel.el72
-rw-r--r--tests/test-flyspell-and-abbrev.el22
-rw-r--r--tests/test-lorem-optimum.el21
-rw-r--r--tests/test-org-contacts-config-find.el72
-rw-r--r--tests/test-org-drill-config-source.el31
-rw-r--r--tests/test-org-reveal-config-keymap.el38
-rw-r--r--tests/test-text-config.el13
18 files changed, 402 insertions, 143 deletions
diff --git a/modules/calendar-sync-source.el b/modules/calendar-sync-source.el
index 15c91c59..10dea66e 100644
--- a/modules/calendar-sync-source.el
+++ b/modules/calendar-sync-source.el
@@ -138,39 +138,25 @@ Checks `cj/debug-modules' for symbol `calendar-sync' or t (all)."
;;; .ics Fetch
-(defun calendar-sync--fetch-ics (url callback)
- "Fetch .ics file from URL asynchronously using curl.
-Calls CALLBACK with the .ics content as string (normalized to Unix line endings)
-or nil on error. CALLBACK signature: (lambda (content) ...).
-
-The fetch happens asynchronously and doesn't block Emacs. The callback is
-invoked when the fetch completes, either successfully or with an error."
- (condition-case err
- (let ((buffer (generate-new-buffer " *calendar-sync-curl*")))
- (make-process
- :name "calendar-sync-curl"
- :buffer buffer
- :command (list "curl" "-s" "-L" "--fail"
- "--connect-timeout" "10"
- "--max-time" (number-to-string calendar-sync-fetch-timeout)
- url)
- :sentinel
- (lambda (process event)
- (when (memq (process-status process) '(exit signal))
- (let ((buf (process-buffer process)))
- (when (buffer-live-p buf)
- (let ((content
- (with-current-buffer buf
- (if (and (eq (process-status process) 'exit)
- (= (process-exit-status process) 0))
- (calendar-sync--normalize-line-endings (buffer-string))
- (calendar-sync--log-silently "calendar-sync: Fetch error: curl failed: %s" (string-trim event))
- nil))))
- (kill-buffer buf)
- (funcall callback content))))))))
- (error
- (calendar-sync--log-silently "calendar-sync: Fetch error: %s" (error-message-string err))
- (funcall callback nil))))
+(defun calendar-sync--fetch-sentinel-finish (success event temp-file buffer callback)
+ "Finish an async .ics fetch.
+SUCCESS is non-nil when curl exited cleanly, EVENT the process event
+string, TEMP-FILE the curl output path, BUFFER the process buffer, and
+CALLBACK the continuation. On success CALLBACK receives TEMP-FILE (the
+caller owns deleting it); on failure the error is logged, TEMP-FILE is
+removed, and CALLBACK receives nil. Extracted from the sentinel so the
+success, failure, and cleanup branches are unit-testable without a live
+curl process."
+ (when (buffer-live-p buffer)
+ (unless success
+ (calendar-sync--log-silently "calendar-sync: Fetch error: curl failed: %s"
+ (string-trim event)))
+ (kill-buffer buffer))
+ (if success
+ (funcall callback temp-file)
+ (when (file-exists-p temp-file)
+ (delete-file temp-file))
+ (funcall callback nil)))
(defun calendar-sync--fetch-ics-file (url callback)
"Fetch .ics from URL to a temp file asynchronously.
@@ -190,19 +176,10 @@ owns deleting the temp file after a successful callback."
:sentinel
(lambda (process event)
(when (memq (process-status process) '(exit signal))
- (let ((buf (process-buffer process))
- (success (and (eq (process-status process) 'exit)
- (= (process-exit-status process) 0))))
- (when (buffer-live-p buf)
- (unless success
- (calendar-sync--log-silently "calendar-sync: Fetch error: curl failed: %s"
- (string-trim event)))
- (kill-buffer buf))
- (if success
- (funcall callback temp-file)
- (when (file-exists-p temp-file)
- (delete-file temp-file))
- (funcall callback nil)))))))
+ (calendar-sync--fetch-sentinel-finish
+ (and (eq (process-status process) 'exit)
+ (= (process-exit-status process) 0))
+ event temp-file (process-buffer process) callback)))))
(error
(calendar-sync--log-silently "calendar-sync: Fetch error: %s" (error-message-string err))
(funcall callback nil))))
diff --git a/modules/flyspell-and-abbrev.el b/modules/flyspell-and-abbrev.el
index b73bfdf3..ebe4898e 100644
--- a/modules/flyspell-and-abbrev.el
+++ b/modules/flyspell-and-abbrev.el
@@ -197,9 +197,10 @@ Without prefix argument, it's created in the global abbrev table.
Press C-' repeatedly to step through misspellings one at a time."
(interactive "P")
(cj/--require-spell-checker)
- ;; Run flyspell-buffer only if buffer hasn't been checked yet
- (unless (bound-and-true-p flyspell-mode)
- (flyspell-buffer))
+ ;; Enable Flyspell for the buffer type so the mode sticks and the buffer is
+ ;; scanned once. A bare flyspell-buffer here never turned the mode on, so
+ ;; the guard never tripped and every C-' press re-scanned the whole buffer.
+ (cj/flyspell-on-for-buffer-type)
(let ((misspelled-word (cj/flyspell-goto-previous-misspelling (point))))
(if (not misspelled-word)
diff --git a/modules/keyboard-macros.el b/modules/keyboard-macros.el
index 4e801096..bca36eed 100644
--- a/modules/keyboard-macros.el
+++ b/modules/keyboard-macros.el
@@ -43,7 +43,7 @@
;;; Code:
(require 'subr-x) ;; for string-trim
-(eval-when-compile (require 'user-constants))
+(require 'user-constants) ;; for macros-file, read at runtime
(defvar cj/macros-loaded nil
"Whether saved keyboard macros have been loaded from file.")
@@ -130,15 +130,7 @@ With prefix arg, open the macros file for editing after saving."
(keymap-global-set "C-<f3>" #'cj/kbd-macro-start-or-end)
(keymap-global-set "<f3>" #'call-last-kbd-macro)
(keymap-global-set "M-<f3>" #'cj/save-maybe-edit-macro)
- (keymap-global-set "s-<f3>" #'cj/open-macros-file)
- (add-hook 'kill-emacs-hook #'cj/save-last-kbd-macro-on-exit))
-
-;; Add hook to save any unnamed macros on exit if desired
-(defun cj/save-last-kbd-macro-on-exit ()
- "Save the last keyboard macro before exiting Emacs if it's not saved."
- (when last-kbd-macro
- (when (y-or-n-p "Save last keyboard macro before exiting? ")
- (call-interactively #'cj/save-maybe-edit-macro))))
+ (keymap-global-set "s-<f3>" #'cj/open-macros-file))
;; Auto-call setup after init
(if after-init-time
diff --git a/modules/lorem-optimum.el b/modules/lorem-optimum.el
index 8aa96345..14f1d666 100644
--- a/modules/lorem-optimum.el
+++ b/modules/lorem-optimum.el
@@ -219,8 +219,22 @@ Builds and caches the keys list lazily if not already cached."
(message "Lorem-optimum learned from file: %s" file))
(defun cj/lipsum (n)
- "Return N words of lorem ipsum."
- (cj/markov-generate cj/lipsum-chain n '("Lorem" "ipsum")))
+ "Return N words of lorem ipsum.
+Interactively, prompt for N and echo the generated words.
+
+Signal a `user-error' when the Markov chain is empty (for example when the
+training file `cj/lipsum-default-file' is missing). Without this, callers
+such as `cj/lipsum-insert' would insert nil and raise a cryptic wrong-type
+error far from the cause. Train the chain with `cj/lipsum-learn-file',
+`cj/lipsum-learn-buffer', or `cj/lipsum-learn-region', or restore the file."
+ (interactive "nNumber of words: ")
+ (let ((text (cj/markov-generate cj/lipsum-chain n '("Lorem" "ipsum"))))
+ (unless (and (stringp text) (not (string-empty-p text)))
+ (user-error "Lorem-optimum chain is empty; train it with cj/lipsum-learn-file or restore %s"
+ cj/lipsum-default-file))
+ (when (called-interactively-p 'any)
+ (message "%s" text))
+ text))
(defun cj/lipsum-insert (n)
"Insert N words of lorem ipsum at point."
diff --git a/modules/org-babel-config.el b/modules/org-babel-config.el
index 79661013..51919da1 100644
--- a/modules/org-babel-config.el
+++ b/modules/org-babel-config.el
@@ -173,8 +173,5 @@ session when working in trusted files, and back on when done."
;; requires ob-racket, not yet in repositories
;; (add-to-list 'org-structure-template-alist '("sicp" . "src racket :lang sicp"))
-;; drop Org’s default footnote list at the end
-(setq org-html-footnote-separator "")
-
(provide 'org-babel-config)
;;; org-babel-config.el ends here.
diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el
index 944d75c1..39ff9910 100644
--- a/modules/org-contacts-config.el
+++ b/modules/org-contacts-config.el
@@ -170,29 +170,40 @@ Added: %U"
(require 'system-lib)
+(defun cj/--org-contacts-collect (buffer)
+ "Return an alist of (NAME POSITION INFO) for the contact headings in BUFFER.
+NAME is the heading text, POSITION its buffer position, and INFO the
+EMAIL or PHONE property value (or nil)."
+ (with-current-buffer buffer
+ (org-map-entries
+ (lambda ()
+ (list (nth 4 (org-heading-components))
+ (point)
+ (or (org-entry-get nil "EMAIL")
+ (org-entry-get nil "PHONE"))))
+ nil nil)))
+
(defun cj/org-contacts-find ()
- "Find and open a contact."
+ "Find a contact and jump to its heading.
+Collect the contact headings before prompting, so cancelling the prompt
+leaves point where it was, and jump to the selected heading's stored
+position instead of a text search that could land inside another entry."
(interactive)
- (find-file contacts-file)
- (goto-char (point-min))
- (let* ((alist (org-map-entries
- (lambda ()
- (cons (nth 4 (org-heading-components))
- (or (org-entry-get nil "EMAIL")
- (org-entry-get nil "PHONE"))))
- nil (list contacts-file)))
+ (let* ((buf (find-file-noselect contacts-file))
+ (alist (cj/--org-contacts-collect buf))
(contact (completing-read
"Find contact: "
(cj/completion-table-annotated
'contact
(lambda (cand)
- (let ((info (cdr (assoc cand alist))))
+ (let ((info (nth 2 (assoc cand alist))))
(when (and info (> (length info) 0))
(concat " " (propertize info 'face
'completions-annotations)))))
- alist))))
- (goto-char (point-min))
- (search-forward contact)
+ alist)
+ nil t)))
+ (switch-to-buffer buf)
+ (goto-char (nth 1 (assoc contact alist)))
(org-fold-show-entry)
(org-reveal)))
diff --git a/modules/org-drill-config.el b/modules/org-drill-config.el
index 29f6130a..f53f36b9 100644
--- a/modules/org-drill-config.el
+++ b/modules/org-drill-config.el
@@ -134,25 +134,42 @@ With a prefix arg OTHER-DIR, prompt for the directory instead of `drill-dir'."
;; --------------------------------- Org Drill ---------------------------------
-(use-package org-drill
- ;; :vc (:url "git@cjennings.net:org-drill.git"
- ;; :branch "main"
- ;; :rev :newest)
- :load-path "~/code/org-drill" ;; local dev checkout — switch back to :vc above when done
- :after (org org-capture)
- :demand t
- :commands (org-drill org-drill-resume)
- :custom
- (org-drill-leech-failure-threshold 50 "leech cards = 50 wrong answers")
- (org-drill-leech-method 'warn "leech cards show warnings")
- (org-drill-use-visible-cloze-face-p t "cloze text shows up in a different font")
- (org-drill-hide-item-headings-p t "don't show heading text")
- (org-drill-maximum-items-per-session 100 "drill sessions end after 100 cards")
- (org-drill-maximum-duration 30 "each drill session can last up to 30 mins")
- (org-drill-add-random-noise-to-intervals-p t "vary the days to repetition slightly")
- (org-drill-text-size-during-session 24 "24-point font for comfortable reading")
- (org-drill-use-variable-pitch t "variable-pitch font for readability")
- (org-drill-hide-modeline-during-session t "hide the modeline for a cleaner display"))
+(defconst cj/org-drill-dev-checkout (expand-file-name "org-drill" "~/code/")
+ "Local org-drill development checkout, preferred when it exists.")
+
+(defun cj/--org-drill-source-keywords (&optional checkout)
+ "Return the use-package source keywords for org-drill.
+With CHECKOUT (default `cj/org-drill-dev-checkout') an existing directory,
+load from it via :load-path. Otherwise install from upstream via :vc, so
+drill still loads on a machine without the dev checkout (bare :load-path +
+:demand t would fail to load there)."
+ (let ((dir (or checkout cj/org-drill-dev-checkout)))
+ (if (file-directory-p dir)
+ (list :load-path dir)
+ (list :vc '(:url "git@cjennings.net:org-drill.git"
+ :branch "main"
+ :rev :newest)))))
+
+;; `use-package' keywords must be literals at macro-expansion, so the
+;; source keyword is spliced in through `eval' at load time (same idiom as
+;; the computed flycheck checker path elsewhere in the config).
+(eval
+ `(use-package org-drill
+ ,@(cj/--org-drill-source-keywords)
+ :after (org org-capture)
+ :demand t
+ :commands (org-drill org-drill-resume)
+ :custom
+ (org-drill-leech-failure-threshold 50 "leech cards = 50 wrong answers")
+ (org-drill-leech-method 'warn "leech cards show warnings")
+ (org-drill-use-visible-cloze-face-p t "cloze text shows up in a different font")
+ (org-drill-hide-item-headings-p t "don't show heading text")
+ (org-drill-maximum-items-per-session 100 "drill sessions end after 100 cards")
+ (org-drill-maximum-duration 30 "each drill session can last up to 30 mins")
+ (org-drill-add-random-noise-to-intervals-p t "vary the days to repetition slightly")
+ (org-drill-text-size-during-session 24 "24-point font for comfortable reading")
+ (org-drill-use-variable-pitch t "variable-pitch font for readability")
+ (org-drill-hide-modeline-during-session t "hide the modeline for a cleaner display")))
(provide 'org-drill-config)
;;; org-drill-config.el ends here.
diff --git a/modules/org-export-config.el b/modules/org-export-config.el
index 5a6f09fc..c3d3294c 100644
--- a/modules/org-export-config.el
+++ b/modules/org-export-config.el
@@ -20,7 +20,6 @@
;; - HTML: Web publishing with HTML5 support
;; - Markdown: README files and web content
;; - ODT: Office documents for LibreOffice/MS Word
-;; - Texinfo: GNU documentation and Info files
;;
;; Extended via Pandoc:
;; - Additional formats: DOCX, self-contained HTML5
@@ -28,7 +27,7 @@
;;
;; Key features:
;; - UTF-8 encoding enforced across all backends
-;; - Subtree export as default scope
+;; - Buffer export as default scope
;;
;; Note: reveal.js presentations are handled by org-reveal-config.el (C-; p)
;;
@@ -68,17 +67,8 @@
:config
(setq org-html-postamble nil)
(setq org-html-html5-fancy t)
- (setq org-html-head-include-default-style nil))
-
-
-(use-package ox-texinfo
- :ensure nil ; Built into Org
- :defer t
- :after ox
- :config
- (setq org-texinfo-coding-system 'utf-8)
- (setq org-texinfo-default-class "info")
- (add-to-list 'org-export-backends 'texinfo))
+ (setq org-html-head-include-default-style nil)
+ (setq org-html-footnote-separator "")) ;; no separator between adjacent footnote refs
(use-package ox-pandoc
:defer t
diff --git a/modules/org-reveal-config.el b/modules/org-reveal-config.el
index be702bf7..f842680b 100644
--- a/modules/org-reveal-config.el
+++ b/modules/org-reveal-config.el
@@ -8,9 +8,10 @@
;; Load shape: eager.
;; Eager reason: none; presentation export is a command-loaded deferral
;; candidate for Phase 4.
-;; Top-level side effects: package configuration via use-package.
-;; Runtime requires: none (configures packages via use-package).
-;; Direct test load: yes.
+;; Top-level side effects: registers a presentation prefix keymap under
+;; cj/custom-keymap; package configuration via use-package.
+;; Runtime requires: keybindings.
+;; Direct test load: yes (requires keybindings explicitly).
;;
;; Integrates ox-reveal for creating reveal.js presentations from Org files.
;;
@@ -28,6 +29,8 @@
;;; Code:
+(require 'keybindings) ;; cj/register-prefix-map, cj/custom-keymap
+
;; Forward declarations for byte-compiler (ox-reveal loaded via use-package)
(defvar org-reveal-root)
(defvar org-reveal-single-file)
@@ -238,17 +241,25 @@ reveal.js headers pre-filled."
;; -------------------------------- Keybindings --------------------------------
-(global-set-key (kbd "C-; p SPC") #'cj/reveal-present)
-(global-set-key (kbd "C-; p e") #'cj/reveal-export)
-(global-set-key (kbd "C-; p p") #'cj/reveal-preview-start)
-(global-set-key (kbd "C-; p s") #'cj/reveal-preview-stop)
-(global-set-key (kbd "C-; p h") #'cj/reveal-insert-header)
-(global-set-key (kbd "C-; p H") #'cj/reveal-remove-headers)
-(global-set-key (kbd "C-; p n") #'cj/reveal-new)
+;; A registered prefix keymap, not raw `global-set-key' chains: binding
+;; "C-; p ..." directly depends on keybindings.el having already made "C-;"
+;; a live prefix (otherwise "non-prefix key" errors), while
+;; `cj/register-prefix-map' binds into `cj/custom-keymap' with no load-order
+;; dependency beyond requiring keybindings.
+(defvar-keymap cj/reveal-map
+ :doc "Keymap for reveal.js presentation commands."
+ "SPC" #'cj/reveal-present
+ "e" #'cj/reveal-export
+ "p" #'cj/reveal-preview-start
+ "s" #'cj/reveal-preview-stop
+ "h" #'cj/reveal-insert-header
+ "H" #'cj/reveal-remove-headers
+ "n" #'cj/reveal-new)
+
+(cj/register-prefix-map "p" cj/reveal-map "presentations")
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
- "C-; p" "presentations"
"C-; p SPC" "present current buffer"
"C-; p e" "export & open"
"C-; p p" "start live preview"
diff --git a/modules/org-webclipper.el b/modules/org-webclipper.el
index 40ceada7..217aecfa 100644
--- a/modules/org-webclipper.el
+++ b/modules/org-webclipper.el
@@ -186,22 +186,6 @@ Return the yanked content as a string so templates can insert it."
;; extract the webpage content from the kill ring
(car kill-ring)))
-;; ----------------------------- Webclipper Keymap -----------------------------
-
-;; keymaps shouldn't be required for webclipper
-;; Setup keymaps
-;;
-;; (defun cj/webclipper-setup-keymaps ()
-;; "Setup webclipper keymaps."
-;; (define-prefix-command 'cj/webclipper-map nil
-;; "Keymap for weblipper operations.")
-;; (define-key cj/custom-keymap "c" 'cj/webclipper-map)
-;; (define-key cj/webclipper-map "n" 'cj/move-org-branch-to-roam))
-
-;; ;; Call keymap setup if cj/custom-keymap is already defined
-;; (when (boundp 'cj/custom-keymap)
-;; (cj/webclipper-setup-keymaps))
-
;; Register protocol handler early for external calls
(with-eval-after-load 'org-protocol
(unless (assoc "webclip" org-protocol-protocol-alist)
@@ -211,9 +195,5 @@ Return the yanked content as a string so templates can insert it."
:function cj/org-protocol-webclip
:kill-client t))))
-;; (with-eval-after-load 'cj/custom-keymap
-;; (require 'org-webclipper)
-;; (cj/webclipper-setup-keymaps))
-
(provide 'org-webclipper)
;;; org-webclipper.el ends here
diff --git a/modules/text-config.el b/modules/text-config.el
index dd7bd3ca..a65002a8 100644
--- a/modules/text-config.el
+++ b/modules/text-config.el
@@ -69,7 +69,7 @@
;; edit selection in new buffer, C-c to finish; replaces with modifications
(use-package edit-indirect
- :bind ("M-S-i" . edit-indirect-region)) ;; was M-I
+ :bind ("M-I" . edit-indirect-region))
;; ------------------------------ Prettify Symbols -----------------------------
;; replacing the word l-a-m-b-d-a with a symbol, just because
@@ -118,8 +118,8 @@ everything else, such as `lambda', use the standard boundary check."
;; an easy way to enter diacritical marks
(use-package accent
- :commands accent-company
- :bind ("C-`" . accent-company))
+ :commands accent-menu
+ :bind ("C-`" . accent-menu))
(provide 'text-config)
;;; text-config.el ends here
diff --git a/tests/test-calendar-sync-source-fetch-sentinel.el b/tests/test-calendar-sync-source-fetch-sentinel.el
new file mode 100644
index 00000000..0b7ba1cf
--- /dev/null
+++ b/tests/test-calendar-sync-source-fetch-sentinel.el
@@ -0,0 +1,72 @@
+;;; test-calendar-sync-source-fetch-sentinel.el --- Tests for the fetch sentinel -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; calendar-sync--fetch-sentinel-finish is the extracted tail of the async
+;; .ics fetch sentinel. The async-worker tests stub the whole fetch, so its
+;; success, failure, and temp-file-cleanup branches were never exercised.
+;; These tests drive the helper directly with fake success/failure inputs,
+;; no live curl process.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'calendar-sync)
+
+(ert-deftest test-calendar-sync-fetch-sentinel-success-passes-temp-file ()
+ "Normal: on success the callback gets the temp file and it is not deleted."
+ (let ((temp-file (make-temp-file "calendar-sync-sentinel-" nil ".ics"))
+ (buffer (generate-new-buffer " *cs-test*"))
+ (got 'unset))
+ (unwind-protect
+ (progn
+ (calendar-sync--fetch-sentinel-finish
+ t "finished\n" temp-file buffer (lambda (r) (setq got r)))
+ (should (equal got temp-file))
+ (should (file-exists-p temp-file))
+ (should-not (buffer-live-p buffer)))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (when (buffer-live-p buffer) (kill-buffer buffer)))))
+
+(ert-deftest test-calendar-sync-fetch-sentinel-failure-deletes-and-passes-nil ()
+ "Error: on failure the temp file is deleted and the callback gets nil."
+ (let ((temp-file (make-temp-file "calendar-sync-sentinel-" nil ".ics"))
+ (buffer (generate-new-buffer " *cs-test*"))
+ (got 'unset)
+ (logged nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'calendar-sync--log-silently)
+ (lambda (&rest _) (setq logged t))))
+ (calendar-sync--fetch-sentinel-finish
+ nil "exited abnormally with code 22\n" temp-file buffer
+ (lambda (r) (setq got r)))
+ (should (null got))
+ (should-not (file-exists-p temp-file))
+ (should logged)
+ (should-not (buffer-live-p buffer)))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (when (buffer-live-p buffer) (kill-buffer buffer)))))
+
+(ert-deftest test-calendar-sync-fetch-sentinel-failure-tolerates-missing-temp-file ()
+ "Boundary: failure with the temp file already gone does not error."
+ (let ((temp-file (make-temp-file "calendar-sync-sentinel-" nil ".ics"))
+ (got 'unset))
+ (delete-file temp-file)
+ (cl-letf (((symbol-function 'calendar-sync--log-silently) #'ignore))
+ (calendar-sync--fetch-sentinel-finish
+ nil "failed\n" temp-file nil (lambda (r) (setq got r)))
+ (should (null got)))))
+
+(ert-deftest test-calendar-sync-fetch-sentinel-tolerates-dead-buffer ()
+ "Boundary: a already-dead process buffer is not touched on success."
+ (let ((temp-file (make-temp-file "calendar-sync-sentinel-" nil ".ics"))
+ (got 'unset))
+ (unwind-protect
+ (progn
+ (calendar-sync--fetch-sentinel-finish
+ t "finished\n" temp-file nil (lambda (r) (setq got r)))
+ (should (equal got temp-file)))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(provide 'test-calendar-sync-source-fetch-sentinel)
+;;; test-calendar-sync-source-fetch-sentinel.el ends here
diff --git a/tests/test-flyspell-and-abbrev.el b/tests/test-flyspell-and-abbrev.el
index ef8cc637..3b494d5e 100644
--- a/tests/test-flyspell-and-abbrev.el
+++ b/tests/test-flyspell-and-abbrev.el
@@ -97,5 +97,27 @@
(cj/flyspell-on-for-buffer-type)))
(should mode-called)))
+;; --------------------------- cj/flyspell-then-abbrev -------------------------
+
+(ert-deftest test-flyspell-then-abbrev-enables-mode-not-bare-rescan ()
+ "Regression: cj/flyspell-then-abbrev routes the initial scan through
+cj/flyspell-on-for-buffer-type, which enables flyspell-mode so it sticks.
+The bare flyspell-buffer it replaced never turned the mode on, so the guard
+never tripped and every C-' press re-scanned the whole buffer (O(buffer) per
+keypress in large files)."
+ (let (on-called scan-called)
+ (cl-letf (((symbol-function 'cj/--require-spell-checker) #'ignore)
+ ((symbol-function 'cj/flyspell-on-for-buffer-type)
+ (lambda () (setq on-called t)))
+ ((symbol-function 'flyspell-buffer)
+ (lambda (&rest _) (setq scan-called t)))
+ ((symbol-function 'cj/flyspell-goto-previous-misspelling)
+ (lambda (&rest _) nil)))
+ (with-temp-buffer
+ (text-mode)
+ (cj/flyspell-then-abbrev nil)))
+ (should on-called)
+ (should-not scan-called)))
+
(provide 'test-flyspell-and-abbrev)
;;; test-flyspell-and-abbrev.el ends here
diff --git a/tests/test-lorem-optimum.el b/tests/test-lorem-optimum.el
index f928c972..b7d97a8e 100644
--- a/tests/test-lorem-optimum.el
+++ b/tests/test-lorem-optimum.el
@@ -253,5 +253,26 @@ an empty string, not an error."
(let ((cj/lipsum-chain (cj/markov-chain-create)))
(should (equal "" (cj/lipsum-title)))))
+;;; cj/lipsum entry point
+
+(ert-deftest test-lipsum-returns-string-with-populated-chain ()
+ "Normal: cj/lipsum returns a non-empty string when the chain is trained."
+ (let ((cj/lipsum-chain
+ (test-learn "Lorem ipsum dolor sit amet consectetur adipiscing elit")))
+ (let ((result (cj/lipsum 5)))
+ (should (stringp result))
+ (should (> (length result) 0)))))
+
+(ert-deftest test-lipsum-empty-chain-signals-user-error ()
+ "Error: cj/lipsum on an empty chain signals a user-error naming the fix,
+rather than returning nil and letting cj/lipsum-insert do (insert nil),
+which raises a cryptic wrong-type error far from the cause."
+ (let ((cj/lipsum-chain (cj/markov-chain-create)))
+ (should-error (cj/lipsum 5) :type 'user-error)))
+
+(ert-deftest test-lipsum-is-interactive-command ()
+ "Normal: cj/lipsum is a command, as its Commentary (M-x cj/lipsum) advertises."
+ (should (commandp 'cj/lipsum)))
+
(provide 'test-lorem-optimum)
;;; test-lorem-optimum.el ends here
diff --git a/tests/test-org-contacts-config-find.el b/tests/test-org-contacts-config-find.el
new file mode 100644
index 00000000..60a5e713
--- /dev/null
+++ b/tests/test-org-contacts-config-find.el
@@ -0,0 +1,72 @@
+;;; test-org-contacts-config-find.el --- Tests for cj/org-contacts-find -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; cj/org-contacts-find collects contact headings (name, position, and the
+;; EMAIL/PHONE annotation) before prompting, then jumps to the selected
+;; heading's stored position. These tests pin the collector helper and a
+;; command-level smoke test: the jump must land on the heading, never on a
+;; body line that merely mentions the name.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'org)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-contacts-config)
+
+;;; cj/--org-contacts-collect
+
+(ert-deftest test-org-contacts-collect-returns-name-position-info ()
+ "Normal: collector returns heading name, heading position, and EMAIL/PHONE."
+ (with-temp-buffer
+ (org-mode)
+ (insert "* Alice\n:PROPERTIES:\n:EMAIL: alice@example.com\n:END:\n"
+ "some body text mentioning Bob\n"
+ "* Bob\n:PROPERTIES:\n:PHONE: 555-1234\n:END:\n")
+ (let* ((alist (cj/--org-contacts-collect (current-buffer)))
+ (alice (assoc "Alice" alist))
+ (bob (assoc "Bob" alist)))
+ (should (equal (length alist) 2))
+ (should (equal (nth 2 alice) "alice@example.com"))
+ (should (equal (nth 2 bob) "555-1234"))
+ ;; positions land on the headings, not the body line that mentions Bob
+ (should (save-excursion (goto-char (nth 1 alice)) (looking-at "\\* Alice")))
+ (should (save-excursion (goto-char (nth 1 bob)) (looking-at "\\* Bob"))))))
+
+(ert-deftest test-org-contacts-collect-empty-buffer-returns-nil ()
+ "Boundary: a buffer with no headings yields an empty alist."
+ (with-temp-buffer
+ (org-mode)
+ (should (null (cj/--org-contacts-collect (current-buffer))))))
+
+(ert-deftest test-org-contacts-collect-heading-without-props-has-nil-info ()
+ "Error: a heading with no EMAIL or PHONE yields nil info."
+ (with-temp-buffer
+ (org-mode)
+ (insert "* Carol\n")
+ (let ((alist (cj/--org-contacts-collect (current-buffer))))
+ (should (equal (nth 2 (assoc "Carol" alist)) nil)))))
+
+;;; cj/org-contacts-find
+
+(ert-deftest test-org-contacts-find-jumps-to-heading-not-body ()
+ "Normal: selecting a contact jumps to its heading, not a body mention."
+ (let ((contacts-file (make-temp-file "org-contacts-test-" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file contacts-file
+ (insert "* Alice\n:PROPERTIES:\n:EMAIL: alice@example.com\n:END:\n"
+ "note: call Bob about Alice\n"
+ "* Bob\n:PROPERTIES:\n:PHONE: 555-1234\n:END:\n"))
+ (cl-letf (((symbol-function 'completing-read)
+ (lambda (&rest _) "Bob"))
+ ((symbol-function 'org-fold-show-entry) #'ignore)
+ ((symbol-function 'org-reveal) #'ignore))
+ (cj/org-contacts-find)
+ (should (looking-at "\\* Bob"))))
+ (delete-file contacts-file))))
+
+(provide 'test-org-contacts-config-find)
+;;; test-org-contacts-config-find.el ends here
diff --git a/tests/test-org-drill-config-source.el b/tests/test-org-drill-config-source.el
new file mode 100644
index 00000000..ccda99ef
--- /dev/null
+++ b/tests/test-org-drill-config-source.el
@@ -0,0 +1,31 @@
+;;; test-org-drill-config-source.el --- Tests for org-drill source selection -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; cj/--org-drill-source-keywords decides how use-package obtains org-drill:
+;; a local dev checkout via :load-path when it exists, otherwise the upstream
+;; repo via :vc. Without this, a machine lacking the checkout hits :demand t
+;; against a nonexistent :load-path and drill fails to load entirely.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-drill-config)
+
+(ert-deftest test-org-drill-source-keywords-uses-load-path-when-checkout-exists ()
+ "Normal: an existing checkout directory selects :load-path."
+ (let ((dir (make-temp-file "org-drill-checkout-" t)))
+ (unwind-protect
+ (should (equal (cj/--org-drill-source-keywords dir)
+ (list :load-path dir)))
+ (delete-directory dir t))))
+
+(ert-deftest test-org-drill-source-keywords-falls-back-to-vc-when-absent ()
+ "Error: a missing checkout falls back to a :vc install spec."
+ (let ((kws (cj/--org-drill-source-keywords "/nonexistent/org-drill-xyz")))
+ (should (eq (car kws) :vc))
+ (should (plist-get (nth 1 kws) :url))))
+
+(provide 'test-org-drill-config-source)
+;;; test-org-drill-config-source.el ends here
diff --git a/tests/test-org-reveal-config-keymap.el b/tests/test-org-reveal-config-keymap.el
new file mode 100644
index 00000000..17a21b19
--- /dev/null
+++ b/tests/test-org-reveal-config-keymap.el
@@ -0,0 +1,38 @@
+;;; test-org-reveal-config-keymap.el --- Tests for org-reveal-config prefix keymap -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; The presentation commands are reached through `cj/reveal-map', a prefix
+;; keymap registered under "C-; p" via `cj/register-prefix-map'. These
+;; tests pin that structure so the module can't silently regress to raw
+;; `global-set-key' calls (which carry a hidden load-order dependency on
+;; keybindings.el establishing "C-;" as a prefix first).
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'keybindings)
+(require 'org-reveal-config)
+
+(ert-deftest test-org-reveal-config-reveal-map-is-a-keymap ()
+ "Normal: `cj/reveal-map' is a keymap."
+ (should (keymapp cj/reveal-map)))
+
+(ert-deftest test-org-reveal-config-reveal-map-bindings ()
+ "Normal: each presentation command is reachable under `cj/reveal-map'."
+ (dolist (pair '(("SPC" . cj/reveal-present)
+ ("e" . cj/reveal-export)
+ ("p" . cj/reveal-preview-start)
+ ("s" . cj/reveal-preview-stop)
+ ("h" . cj/reveal-insert-header)
+ ("H" . cj/reveal-remove-headers)
+ ("n" . cj/reveal-new)))
+ (should (eq (keymap-lookup cj/reveal-map (car pair)) (cdr pair)))))
+
+(ert-deftest test-org-reveal-config-reveal-map-registered-under-p ()
+ "Normal: `cj/reveal-map' is registered under \"p\" in `cj/custom-keymap'."
+ (should (eq (keymap-lookup cj/custom-keymap "p") cj/reveal-map)))
+
+(provide 'test-org-reveal-config-keymap)
+;;; test-org-reveal-config-keymap.el ends here
diff --git a/tests/test-text-config.el b/tests/test-text-config.el
index 96935e1b..82dfe05e 100644
--- a/tests/test-text-config.el
+++ b/tests/test-text-config.el
@@ -37,5 +37,18 @@ standard boundary check."
(should (eq (cj/prettify-compose-block-markers-p start end "lambda")
(prettify-symbols-default-compose-p start end "lambda"))))))
+(ert-deftest test-text-config-edit-indirect-bound-on-reachable-key ()
+ "Error/regression: edit-indirect-region is bound on M-I -- the event
+Meta+Shift+i actually produces -- not the unreachable M-S-i, which no
+keypress generates so it silently fell through to M-i tab-to-tab-stop."
+ (should (eq (key-binding (kbd "M-I")) #'edit-indirect-region))
+ (should-not (eq (key-binding (kbd "M-S-i")) #'edit-indirect-region)))
+
+(ert-deftest test-text-config-accent-uses-completion-agnostic-backend ()
+ "Regression: C-` invokes accent-menu, which reads through the minibuffer
+and so survives a Company->Corfu migration, rather than accent-company,
+whose company backend would break silently once Company is gone."
+ (should (eq (key-binding (kbd "C-`")) #'accent-menu)))
+
(provide 'test-text-config)
;;; test-text-config.el ends here