aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/mu4e-org-contacts-integration.el7
-rw-r--r--modules/org-roam-config.el22
-rw-r--r--modules/org-webclipper.el31
-rw-r--r--tests/test-org-roam-config-copy-todo-to-today.el15
-rw-r--r--tests/test-org-webclipper-commands.el16
-rw-r--r--todo.org8
6 files changed, 70 insertions, 29 deletions
diff --git a/modules/mu4e-org-contacts-integration.el b/modules/mu4e-org-contacts-integration.el
index 8ffdccd2..68f83dd2 100644
--- a/modules/mu4e-org-contacts-integration.el
+++ b/modules/mu4e-org-contacts-integration.el
@@ -13,6 +13,13 @@
(require 'mu4e nil t)
(require 'org-contacts nil t)
+;; Cross-module symbols this file references. Declared so byte-compile in
+;; isolation doesn't warn about free variables / undefined functions; the
+;; actual definitions live where named.
+(eval-when-compile (defvar contacts-file)) ; user-constants.el
+(declare-function cj/get-all-contact-emails ; org-contacts-config.el
+ "org-contacts-config" ())
+
;; ---------------------- Completion at Point Function -------------------------
(defun cj/org-contacts-completion-at-point ()
diff --git a/modules/org-roam-config.el b/modules/org-roam-config.el
index d0851ee4..306c7fe2 100644
--- a/modules/org-roam-config.el
+++ b/modules/org-roam-config.el
@@ -77,14 +77,26 @@
(global-set-key (kbd "C-c n d") org-roam-dailies-map)
(org-roam-db-autosync-mode))
-;; Move closed tasks to today's journal when marked done
+;; Move closed tasks to today's journal when marked done.
+
+(defun cj/--org-roam-should-copy-completed-task-p ()
+ "Return non-nil when the current org-state change should copy to today's journal.
+Skips:
+- Fileless buffers (org-capture, indirect buffers, temp Org buffers)
+- The gcal.org buffer (synced from Google Calendar)
+- State changes that didn't actually enter a done state
+- State changes where the previous state was already done (avoid re-copy)"
+ (let ((file (buffer-file-name)))
+ (and file
+ (bound-and-true-p org-state)
+ (member org-state org-done-keywords)
+ (not (member org-last-state org-done-keywords))
+ (not (string= file (expand-file-name gcal-file))))))
+
(with-eval-after-load 'org
(add-to-list 'org-after-todo-state-change-hook
(lambda ()
- (when (and (member org-state org-done-keywords)
- (not (member org-last-state org-done-keywords))
- ;; Don't run for gcal.org - it's synced from Google Calendar
- (not (string= (buffer-file-name) (expand-file-name gcal-file))))
+ (when (cj/--org-roam-should-copy-completed-task-p)
(cj/org-roam-copy-todo-to-today)))))
;; ------------------------- Org Roam Insert Immediate -------------------------
diff --git a/modules/org-webclipper.el b/modules/org-webclipper.el
index be4c1cb6..cf4cdbf0 100644
--- a/modules/org-webclipper.el
+++ b/modules/org-webclipper.el
@@ -68,12 +68,12 @@
(require 'org-web-tools)
(require 'user-constants) ;; for webclipped-file
- ;; Register the org-protocol handler
- (add-to-list 'org-protocol-protocol-alist
- '("webclip"
- :protocol "webclip"
- :function cj/org-protocol-webclip
- :kill-client t))
+ ;; The org-protocol handler registration lives in the
+ ;; `with-eval-after-load 'org-protocol' block at the bottom of
+ ;; this module -- that's the more robust home (it survives
+ ;; org-protocol being loaded before or after this module). Two
+ ;; registration sites would silently drift if the alist entry
+ ;; shape ever changes.
;; Add capture templates if not already present
(unless (assoc "W" org-capture-templates)
@@ -119,14 +119,23 @@ Returns the processed content as a string with:
(defun cj/org-protocol-webclip (info)
"Process org-protocol webclip requests.
-INFO is a plist containing :url and :title from the org-protocol call."
+INFO is a plist containing :url and :title from the org-protocol call.
+Signals `user-error' when :url is missing, nil, empty, or non-string -- an
+unexpected plist shape used to silently set the globals to nil and fail
+downstream inside the capture handler with confusing messages."
(cj/webclipper-ensure-initialized)
(let ((url (plist-get info :url))
(title (plist-get info :title)))
- (when url
- ;; Store the URL and title for the capture template to use
- (setq cj/webclip-current-url url
- cj/webclip-current-title (or title "Untitled")))
+ (unless (and (stringp url) (not (string-empty-p url)))
+ (user-error
+ "org-protocol webclip: expected non-empty :url string, got %S" url))
+ (when (and title (not (stringp title)))
+ (user-error
+ "org-protocol webclip: :title must be a string when provided, got %S"
+ title))
+ ;; Store the URL and title for the capture template to use
+ (setq cj/webclip-current-url url
+ cj/webclip-current-title (or title "Untitled"))
;; Trigger the capture
(org-capture nil "W")
nil)) ; Return nil to indicate we handled it
diff --git a/tests/test-org-roam-config-copy-todo-to-today.el b/tests/test-org-roam-config-copy-todo-to-today.el
index bcac5a26..c642020d 100644
--- a/tests/test-org-roam-config-copy-todo-to-today.el
+++ b/tests/test-org-roam-config-copy-todo-to-today.el
@@ -51,7 +51,8 @@
(setq org-state "DONE") ; Dynamic variable used by org-mode hooks
(cl-letf (((symbol-function 'cj/org-roam-copy-todo-to-today)
(lambda () (setq copy-function-called t))))
- (run-hooks 'org-after-todo-state-change-hook)
+ (let ((buffer-file-name "/tmp/test-org-roam-fake.org"))
+ (run-hooks 'org-after-todo-state-change-hook))
(should copy-function-called)))
(test-org-roam-todo-hook-teardown)))
@@ -67,7 +68,8 @@
(setq org-state "CANCELLED") ; Dynamic variable used by org-mode hooks
(cl-letf (((symbol-function 'cj/org-roam-copy-todo-to-today)
(lambda () (setq copy-function-called t))))
- (run-hooks 'org-after-todo-state-change-hook)
+ (let ((buffer-file-name "/tmp/test-org-roam-fake.org"))
+ (run-hooks 'org-after-todo-state-change-hook))
(should copy-function-called)))
(test-org-roam-todo-hook-teardown)))
@@ -96,7 +98,8 @@
(setq org-state "DONE") ; New state is DONE
(cl-letf (((symbol-function 'cj/org-roam-copy-todo-to-today)
(lambda () (setq copy-function-called t))))
- (run-hooks 'org-after-todo-state-change-hook)
+ (let ((buffer-file-name "/tmp/test-org-roam-fake.org"))
+ (run-hooks 'org-after-todo-state-change-hook))
(should copy-function-called)))
(test-org-roam-todo-hook-teardown)))
@@ -112,7 +115,8 @@
(setq org-state "DONE") ; New state is DONE
(cl-letf (((symbol-function 'cj/org-roam-copy-todo-to-today)
(lambda () (setq copy-function-called t))))
- (run-hooks 'org-after-todo-state-change-hook)
+ (let ((buffer-file-name "/tmp/test-org-roam-fake.org"))
+ (run-hooks 'org-after-todo-state-change-hook))
(should copy-function-called)))
(test-org-roam-todo-hook-teardown)))
@@ -128,7 +132,8 @@
(setq org-state "CANCELLED") ; New state is CANCELLED
(cl-letf (((symbol-function 'cj/org-roam-copy-todo-to-today)
(lambda () (setq copy-function-called t))))
- (run-hooks 'org-after-todo-state-change-hook)
+ (let ((buffer-file-name "/tmp/test-org-roam-fake.org"))
+ (run-hooks 'org-after-todo-state-change-hook))
(should copy-function-called)))
(test-org-roam-todo-hook-teardown)))
diff --git a/tests/test-org-webclipper-commands.el b/tests/test-org-webclipper-commands.el
index 3871774c..57c7b5fc 100644
--- a/tests/test-org-webclipper-commands.el
+++ b/tests/test-org-webclipper-commands.el
@@ -29,19 +29,27 @@
;;; cj/webclipper-ensure-initialized
-(ert-deftest test-webclipper-ensure-initialized-registers-protocol-and-templates ()
- "Normal: first call sets up the protocol entry + W and w capture templates,
-and flips the initialized flag."
+(ert-deftest test-webclipper-ensure-initialized-registers-templates ()
+ "Normal: first call sets up the W and w capture templates and flips the
+initialized flag. Protocol registration lives in the
+`with-eval-after-load 'org-protocol' block at the bottom of the module --
+asserted separately below in `test-webclipper-protocol-registered-via-after-load'."
(let ((cj/webclipper-initialized nil)
(org-protocol-protocol-alist nil)
(org-capture-templates nil))
(cl-letf (((symbol-function 'require) (lambda (&rest _) t)))
(cj/webclipper-ensure-initialized))
(should cj/webclipper-initialized)
- (should (assoc "webclip" org-protocol-protocol-alist))
(should (assoc "W" org-capture-templates))
(should (assoc "w" org-capture-templates))))
+(ert-deftest test-webclipper-protocol-registered-via-after-load ()
+ "Loading org-webclipper installs a `with-eval-after-load 'org-protocol' block
+that registers the webclip entry. Providing `'org-protocol' fires the block."
+ (let ((org-protocol-protocol-alist nil))
+ (provide 'org-protocol)
+ (should (assoc "webclip" org-protocol-protocol-alist))))
+
(ert-deftest test-webclipper-ensure-initialized-is-idempotent ()
"Boundary: second call doesn't re-register or duplicate templates."
(let ((cj/webclipper-initialized nil)
diff --git a/todo.org b/todo.org
index 3e219489..1bcab75b 100644
--- a/todo.org
+++ b/todo.org
@@ -1594,7 +1594,7 @@ Expected outcome:
- Keep startup quiet; only check expensive/external requirements when the
relevant command runs.
-**** TODO [#B] Guard the org-roam completed-task hook around non-file buffers :bug:refactor:
+**** 2026-05-16 Sat @ 03:44:45 -0500 Guarded org-roam completed-task hook with cj/--org-roam-should-copy-completed-task-p
=org-roam-config.el= adds a global =org-after-todo-state-change-hook= that
copies newly completed tasks to today's org-roam journal. The hook assumes the
@@ -1721,7 +1721,7 @@ Recommended improvement:
- Link this note from the Org workflow review task and the broader load-graph
refactor.
-**** TODO [#C] De-duplicate =org-protocol-protocol-alist= registration in =org-webclipper= :cleanup:
+**** 2026-05-16 Sat @ 03:44:45 -0500 Removed duplicate org-protocol-protocol-alist registration in cj/webclipper-ensure-initialized
The =webclip= protocol handler is registered twice:
=modules/org-webclipper.el:72-76= inside =cj/webclipper-ensure-initialized=
@@ -1733,7 +1733,7 @@ the alist entry shape ever changes. Pick one site -- the
=with-eval-after-load= block is the more robust location -- and
remove the other.
-**** TODO [#C] Validate =:url= and =:title= in =cj/org-protocol-webclip= :safety:
+**** 2026-05-16 Sat @ 03:44:45 -0500 Validated :url and :title in cj/org-protocol-webclip before stashing
=modules/org-webclipper.el:124-125= extracts =:url= and =:title= from
the incoming protocol plist with no type or nil check. An unexpected
@@ -1752,7 +1752,7 @@ corrupt each other's state. Pass the data through a plist on
=org-capture-plist=, a per-invocation closure, or a queue, instead of
global mutation.
-**** TODO [#C] Declare cross-module free variables in =mu4e-org-contacts-integration.el= :cleanup:quick:
+**** 2026-05-16 Sat @ 03:44:45 -0500 Declared cross-module free vars in mu4e-org-contacts-integration.el
The module reads =contacts-file= (defined in =user-constants.el=) and
calls =cj/get-all-contact-emails= (defined in =org-contacts-config.el=)