diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-22 15:01:14 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-22 15:01:14 -0500 |
| commit | 082037ad75e21405655adb492d0c85e342f4f771 (patch) | |
| tree | 67b239762729034b12304c71d945206a587c65e0 /tests | |
| parent | 030bb13ba5742f331dd30a55bbce5a13917d4d63 (diff) | |
| download | dotemacs-082037ad75e21405655adb492d0c85e342f4f771.tar.gz dotemacs-082037ad75e21405655adb492d0c85e342f4f771.zip | |
feat(org-config): add cj/org-finalize-task with tests
I added a command on C-; O d that finalizes the task at point. It prompts for a finalized keyword from org-done-keywords, so the picker tracks org-todo-keywords automatically. Marking the task done fires the org-roam journal-copy hook, so the completed task lands in today's daily.
Then the heading is reshaped by depth. A sub-task (level 3 or deeper, or a VERIFY at any depth) becomes a dated log entry: the keyword and priority cookie are stripped, a sortable timestamp is prepended, and the tags are kept. A top-level task keeps its keyword and gains a date-only CLOSED line.
The command binds org-inhibit-logging around the org-todo call so it owns the CLOSED line rather than depending on org-log-done, which is set inconsistently across two modules. The journal hook keys off org-state, not org-log-done, so the copy still fires.
Tests run in org temp-buffers with the journal hook bound to nil, exercise the real org primitives, and inject a fixed time so the stamp shape is deterministic.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-org-config-finalize-task.el | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/tests/test-org-config-finalize-task.el b/tests/test-org-config-finalize-task.el new file mode 100644 index 00000000..a0932919 --- /dev/null +++ b/tests/test-org-config-finalize-task.el @@ -0,0 +1,140 @@ +;;; test-org-config-finalize-task.el --- Tests for cj/org-finalize-task -*- lexical-binding: t; -*- + +;;; Commentary: +;; Covers `cj/org-finalize-task' and its helpers: the dated-rewrite vs +;; close-in-place dispatch, the two heading transforms, and the guard. +;; +;; The journal-copy hook (`org-after-todo-state-change-hook') is bound to +;; nil in every buffer test so the org-roam daily side effect never fires -- +;; that hook is the external boundary, mocked out here. `org-todo-keywords' +;; is set to the project's full sequence so DOING / VERIFY / CANCELLED resolve +;; as keywords inside the temp buffers regardless of load state. + +;;; Code: + +(require 'ert) +(require 'org) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'org-config) + +;; Fixed instant so the stamp is deterministic in shape. The exact value and +;; tz offset are still system-dependent, so assertions match the FORMAT, not a +;; literal string (per testing.md: assert behavior, not exact text). +(defconst test-finalize--time (encode-time 24 18 14 22 5 2026) + "2026-05-22 14:18:24, local time, for deterministic stamp shape.") + +(defmacro test-finalize--with-heading (text &rest body) + "Insert TEXT in a temp Org buffer, point on the first heading, run BODY. +Returns the resulting buffer string. Binds the project keyword set, nils +the todo-state-change hook (no journal side effect), and inhibits logging." + (declare (indent 1)) + `(with-temp-buffer + (let ((org-todo-keywords + '((sequence "TODO" "PROJECT" "DOING" "WAITING" "VERIFY" + "STALLED" "DELEGATED" "|" "FAILED" "DONE" "CANCELLED"))) + (org-mode-hook nil) ; isolate from unrelated hooks (org-tidy etc.) + (org-after-todo-state-change-hook nil) + (org-inhibit-logging t)) + (insert ,text) + (org-mode) + (goto-char (point-min)) + ,@body + (buffer-string)))) + +;; ---------------------------- predicate -------------------------------------- + +(ert-deftest test-org-finalize-dated-p-level3-true () + "Normal: a level-3 sub-task takes the dated rewrite." + (should (cj/--org-finalize-dated-p 3 "TODO"))) + +(ert-deftest test-org-finalize-dated-p-level2-false () + "Normal: a level-2 task stays task-shaped (close in place)." + (should-not (cj/--org-finalize-dated-p 2 "TODO"))) + +(ert-deftest test-org-finalize-dated-p-verify-true-at-level2 () + "Boundary: VERIFY flips to dated at all depths (todo-format exception)." + (should (cj/--org-finalize-dated-p 2 "VERIFY"))) + +(ert-deftest test-org-finalize-dated-p-level1-false () + "Boundary: a level-1 heading is not dated." + (should-not (cj/--org-finalize-dated-p 1 "TODO"))) + +;; ------------------------- dated rewrite ------------------------------------- + +(ert-deftest test-org-finalize-rewrite-dated-strips-and-prepends () + "Normal: keyword and cookie gone, tag kept, stamp prepended in exact format." + (let ((out (test-finalize--with-heading "*** TODO [#A] Buy milk :shop:\nbody\n" + (cj/--org-finalize-rewrite-dated test-finalize--time)))) + (should (string-match-p + "^\\*\\*\\* [0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} [A-Z][a-z]\\{2\\} @ [0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\} [-+][0-9]\\{4\\} Buy milk[ \t]+:shop:" + out)) + (should-not (string-match-p "TODO" out)) + (should-not (string-match-p "\\[#A\\]" out)))) + +(ert-deftest test-org-finalize-rewrite-dated-no-priority () + "Boundary: a heading without a priority cookie still rewrites." + (let ((out (test-finalize--with-heading "*** DOING Refactor thing\n" + (cj/--org-finalize-rewrite-dated test-finalize--time)))) + (should (string-match-p "Refactor thing" out)) + (should-not (string-match-p "DOING" out)))) + +(ert-deftest test-org-finalize-rewrite-dated-no-tags () + "Boundary: a heading without tags rewrites cleanly." + (let ((out (test-finalize--with-heading "*** TODO Plain task\n" + (cj/--org-finalize-rewrite-dated test-finalize--time)))) + (should (string-match-p "Plain task" out)) + (should-not (string-match-p "TODO" out)))) + +;; ------------------------- close in place ------------------------------------ + +(ert-deftest test-org-finalize-close-in-place-adds-date-only-closed () + "Normal: a date-only CLOSED line is added; keyword retained, no time part." + (let ((out (test-finalize--with-heading "** DONE [#A] Ship it :rel:\n" + (cj/--org-finalize-close-in-place test-finalize--time)))) + (should (string-match-p + "CLOSED: \\[[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} [A-Z][a-z]\\{2\\}\\]" out)) + (should (string-match-p "DONE" out)) + (should-not (string-match-p "CLOSED: \\[[0-9-]+ [A-Z][a-z]+ [0-9]+:[0-9]+" out)))) + +;; ------------------------- command / guard ----------------------------------- + +(ert-deftest test-org-finalize-task-errors-outside-org () + "Error: the command refuses to run outside Org." + (with-temp-buffer + (fundamental-mode) + (should-error (cj/org-finalize-task "DONE") :type 'user-error))) + +(ert-deftest test-org-finalize-task-errors-on-non-task-heading () + "Error: a heading with no actionable keyword cannot be finalized." + (with-temp-buffer + (let ((org-mode-hook nil) + (org-after-todo-state-change-hook nil)) + (insert "* Just a section\n") + (org-mode) + (goto-char (point-min)) + (should-error (cj/org-finalize-task "DONE") :type 'user-error)))) + +(ert-deftest test-org-finalize-task-level3-produces-dated-entry () + "Integration: finalizing a level-3 task yields a dated entry, no CLOSED cruft." + (let ((out (test-finalize--with-heading "*** TODO [#B] Wire the thing :feat:\n" + (cj/org-finalize-task "DONE" test-finalize--time)))) + (should (string-match-p "^\\*\\*\\* [0-9]\\{4\\}-.*Wire the thing" out)) + (should-not (string-match-p "TODO\\|\\[#B\\]\\|CLOSED" out)))) + +(ert-deftest test-org-finalize-task-level2-keeps-keyword-adds-closed () + "Integration: finalizing a level-2 task keeps the keyword and adds CLOSED." + (let ((out (test-finalize--with-heading "** TODO [#A] Big task :proj:\n" + (cj/org-finalize-task "DONE" test-finalize--time)))) + (should (string-match-p "DONE" out)) + (should (string-match-p "CLOSED: \\[" out)) + (should-not (string-match-p "TODO" out)))) + +;; ------------------------- keybinding wiring --------------------------------- + +(ert-deftest test-org-finalize-task-bound-on-org-map () + "Normal: the command is bound to `d' under the org prefix (C-; O d)." + (should (eq (keymap-lookup cj/org-map "d") #'cj/org-finalize-task))) + +(provide 'test-org-config-finalize-task) +;;; test-org-config-finalize-task.el ends here |
