summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-15 21:14:18 -0600
committerCraig Jennings <c@cjennings.net>2026-02-15 21:14:18 -0600
commitedfb11b1a161d39a3f88279bc7b395aa8e6f68b7 (patch)
tree10805d200687e3477b2c250871050ccc0b144a5d /tests
parent61bfb79f2c55983697f87b9ba3961a9fb46de2fe (diff)
test: add 38 tests for org-agenda-config and org-refile-config
- testutil-org.el: shared dynamic timestamp helpers (days-ago, days-ahead, today) - org-agenda-config: 31 tests across 2 files (skip functions + add-files) - org-refile-config: 7 tests for ensure-org-mode validation - Remove prog-shell from checklist (fully covered by existing 9 tests)
Diffstat (limited to 'tests')
-rw-r--r--tests/test-org-agenda-config-add-files.el133
-rw-r--r--tests/test-org-agenda-config-skip-functions.el216
-rw-r--r--tests/test-org-refile-config-ensure-org-mode.el123
-rw-r--r--tests/testutil-org.el32
4 files changed, 504 insertions, 0 deletions
diff --git a/tests/test-org-agenda-config-add-files.el b/tests/test-org-agenda-config-add-files.el
new file mode 100644
index 00000000..e977cc68
--- /dev/null
+++ b/tests/test-org-agenda-config-add-files.el
@@ -0,0 +1,133 @@
+;;; test-org-agenda-config-add-files.el --- Tests for cj/add-files-to-org-agenda-files-list -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/add-files-to-org-agenda-files-list from org-agenda-config.el.
+;; Uses testutil-general.el for filesystem setup/teardown.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(require 'testutil-general)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-agenda-config)
+
+;;; Normal Cases
+
+(ert-deftest test-org-agenda-config-add-files-normal-finds-todo-in-subdirs ()
+ "Subdirectories containing todo.org should be added to org-agenda-files."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project-a/todo.org" "* TODO Task A\n")
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project-b/todo.org" "* TODO Task B\n")
+ (let ((org-agenda-files nil)
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ (should (= 2 (length org-agenda-files)))
+ (should (cl-every #'file-exists-p org-agenda-files))
+ (should (cl-every (lambda (f) (string-suffix-p "todo.org" f)) org-agenda-files))))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-agenda-config-add-files-normal-ignores-subdirs-without-todo ()
+ "Subdirectories without todo.org should not add anything."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project-a/readme.org" "Notes\n")
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project-b/notes.txt" "Stuff\n")
+ (let ((org-agenda-files nil)
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ (should (= 0 (length org-agenda-files)))))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-agenda-config-add-files-normal-mixed-subdirs ()
+ "Only subdirectories with todo.org should be added."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/has-todo/todo.org" "* TODO Yes\n")
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/no-todo/readme.org" "No tasks\n")
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/also-has/todo.org" "* TODO Also\n")
+ (let ((org-agenda-files nil)
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ (should (= 2 (length org-agenda-files)))))
+ (cj/delete-test-base-dir)))
+
+;;; Boundary Cases
+
+(ert-deftest test-org-agenda-config-add-files-boundary-empty-directory ()
+ "Empty directory should add nothing."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/")
+ (let ((org-agenda-files nil)
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ (should (= 0 (length org-agenda-files)))))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-agenda-config-add-files-boundary-hidden-dirs-excluded ()
+ "Hidden subdirectories (starting with .) should be excluded."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/.hidden-project/todo.org" "* TODO Hidden\n")
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/visible-project/todo.org" "* TODO Visible\n")
+ (let ((org-agenda-files nil)
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ (should (= 1 (length org-agenda-files)))
+ (should (string-match-p "visible-project" (car org-agenda-files)))))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-agenda-config-add-files-boundary-files-at-top-level-ignored ()
+ "Regular files (not directories) at the top level should be ignored."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/todo.org" "* TODO Top-level\n")
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project/todo.org" "* TODO In subdir\n")
+ (let ((org-agenda-files nil)
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ ;; Only the subdir's todo.org, not the top-level one
+ (should (= 1 (length org-agenda-files)))
+ (should (string-match-p "project" (car org-agenda-files)))))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-agenda-config-add-files-boundary-no-deep-recursion ()
+ "Should not recurse into nested subdirectories."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project/todo.org" "* TODO Shallow\n")
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project/subdir/todo.org" "* TODO Deep\n")
+ (let ((org-agenda-files nil)
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ ;; Only immediate subdir's todo.org
+ (should (= 1 (length org-agenda-files)))
+ (should-not (string-match-p "subdir" (car org-agenda-files)))))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-agenda-config-add-files-boundary-appends-to-existing ()
+ "Should append to existing org-agenda-files, not replace."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (cj/create-directory-or-file-ensuring-parents "agenda-test/project/todo.org" "* TODO New\n")
+ (let ((org-agenda-files '("/existing/file.org"))
+ (test-dir (expand-file-name "agenda-test" cj/test-base-dir)))
+ (cj/add-files-to-org-agenda-files-list test-dir)
+ (should (= 2 (length org-agenda-files)))
+ (should (member "/existing/file.org" org-agenda-files))))
+ (cj/delete-test-base-dir)))
+
+(provide 'test-org-agenda-config-add-files)
+;;; test-org-agenda-config-add-files.el ends here
diff --git a/tests/test-org-agenda-config-skip-functions.el b/tests/test-org-agenda-config-skip-functions.el
new file mode 100644
index 00000000..24eeb7e4
--- /dev/null
+++ b/tests/test-org-agenda-config-skip-functions.el
@@ -0,0 +1,216 @@
+;;; test-org-agenda-config-skip-functions.el --- Tests for org-agenda skip functions -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the agenda skip functions in org-agenda-config.el:
+;; - cj/org-skip-subtree-if-habit
+;; - cj/org-skip-subtree-if-priority
+;; - cj/org-skip-subtree-if-keyword
+;; - cj/org-agenda-skip-subtree-if-not-overdue
+;;
+;; Uses dynamic timestamp generation (no hardcoded dates) via testutil-org.
+
+;;; Code:
+
+(require 'ert)
+(require 'org)
+
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(require 'testutil-org)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-agenda-config)
+
+;; Suppress org-mode hooks that load packages unavailable in batch mode
+(defmacro test-org-agenda--with-org-buffer (content &rest body)
+ "Execute BODY in a temp org buffer with CONTENT, point at first heading.
+Suppresses org-mode hooks to avoid loading packages not available in batch."
+ (declare (indent 1))
+ `(with-temp-buffer
+ (let ((org-mode-hook nil)
+ (text-mode-hook nil))
+ (org-mode))
+ (insert ,content)
+ (goto-char (point-min))
+ ,@body))
+
+;;; ---------- cj/org-skip-subtree-if-habit ----------
+
+;;; Normal Cases
+
+(ert-deftest test-org-agenda-config-skip-habit-normal-habit-entry-skips ()
+ "Entry with STYLE=habit should return subtree-end (skip)."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Daily exercise\n"
+ ":PROPERTIES:\n"
+ ":STYLE: habit\n"
+ ":END:\n")
+ (should (integerp (cj/org-skip-subtree-if-habit)))))
+
+(ert-deftest test-org-agenda-config-skip-habit-normal-non-habit-style-keeps ()
+ "Entry with STYLE set to something other than habit should return nil (keep)."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Regular task\n"
+ ":PROPERTIES:\n"
+ ":STYLE: other\n"
+ ":END:\n")
+ (should (null (cj/org-skip-subtree-if-habit)))))
+
+(ert-deftest test-org-agenda-config-skip-habit-normal-no-style-property-keeps ()
+ "Entry with no STYLE property should return nil (keep)."
+ (test-org-agenda--with-org-buffer "* TODO Task without style\n"
+ (should (null (cj/org-skip-subtree-if-habit)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-org-agenda-config-skip-habit-boundary-returns-subtree-end-position ()
+ "Return value should be the position after the entire subtree."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Habit task\n"
+ ":PROPERTIES:\n"
+ ":STYLE: habit\n"
+ ":END:\n"
+ "** Sub-heading\n"
+ "Some content\n"
+ "* Next task\n")
+ (let ((skip-pos (cj/org-skip-subtree-if-habit)))
+ (should (integerp skip-pos))
+ ;; Skip position should be past the sub-heading content
+ (should (> skip-pos (point)))
+ ;; But not past the next top-level heading
+ (goto-char (point-min))
+ (re-search-forward "^\\* Next task")
+ (beginning-of-line)
+ (should (<= skip-pos (point))))))
+
+;;; ---------- cj/org-skip-subtree-if-priority ----------
+
+;;; Normal Cases
+
+(ert-deftest test-org-agenda-config-skip-priority-normal-matching-priority-skips ()
+ "Entry with priority A should be skipped when filtering for A."
+ (test-org-agenda--with-org-buffer "* TODO [#A] Important task\n"
+ (should (integerp (cj/org-skip-subtree-if-priority ?A)))))
+
+(ert-deftest test-org-agenda-config-skip-priority-normal-different-priority-keeps ()
+ "Entry with priority B should not be skipped when filtering for A."
+ (test-org-agenda--with-org-buffer "* TODO [#B] Normal task\n"
+ (should (null (cj/org-skip-subtree-if-priority ?A)))))
+
+(ert-deftest test-org-agenda-config-skip-priority-normal-no-priority-keeps ()
+ "Entry with no priority cookie should not be skipped."
+ (test-org-agenda--with-org-buffer "* TODO Plain task\n"
+ (should (null (cj/org-skip-subtree-if-priority ?A)))))
+
+(ert-deftest test-org-agenda-config-skip-priority-normal-filter-b-skips-b ()
+ "Entry with priority B should be skipped when filtering for B."
+ (test-org-agenda--with-org-buffer "* TODO [#B] Normal task\n"
+ (should (integerp (cj/org-skip-subtree-if-priority ?B)))))
+
+(ert-deftest test-org-agenda-config-skip-priority-normal-filter-c-skips-c ()
+ "Entry with priority C should be skipped when filtering for C."
+ (test-org-agenda--with-org-buffer "* TODO [#C] Low priority task\n"
+ (should (integerp (cj/org-skip-subtree-if-priority ?C)))))
+
+;;; ---------- cj/org-skip-subtree-if-keyword ----------
+
+;;; Normal Cases
+
+(ert-deftest test-org-agenda-config-skip-keyword-normal-matching-keyword-skips ()
+ "Entry with TODO keyword in list should return subtree-end (skip)."
+ (test-org-agenda--with-org-buffer "* TODO Some task\n"
+ (should (integerp (cj/org-skip-subtree-if-keyword '("TODO"))))))
+
+(ert-deftest test-org-agenda-config-skip-keyword-normal-done-in-list-skips ()
+ "Entry with DONE keyword in list should return subtree-end (skip)."
+ (test-org-agenda--with-org-buffer "* DONE Completed task\n"
+ (should (integerp (cj/org-skip-subtree-if-keyword '("DONE"))))))
+
+(ert-deftest test-org-agenda-config-skip-keyword-normal-keyword-not-in-list-keeps ()
+ "Entry with keyword not in filter list should return nil (keep)."
+ (test-org-agenda--with-org-buffer "* TODO Some task\n"
+ (should (null (cj/org-skip-subtree-if-keyword '("DONE" "CANCELLED"))))))
+
+(ert-deftest test-org-agenda-config-skip-keyword-normal-no-keyword-keeps ()
+ "Entry with no TODO keyword should return nil (keep)."
+ (test-org-agenda--with-org-buffer "* Just a heading\n"
+ (should (null (cj/org-skip-subtree-if-keyword '("TODO" "DONE"))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-org-agenda-config-skip-keyword-boundary-multiple-keywords-in-list ()
+ "Filter list with multiple keywords should match any of them."
+ (test-org-agenda--with-org-buffer "* DONE Finished task\n"
+ (should (integerp (cj/org-skip-subtree-if-keyword '("TODO" "DONE" "CANCELLED"))))))
+
+;;; ---------- cj/org-agenda-skip-subtree-if-not-overdue ----------
+
+;;; Normal Cases
+
+(ert-deftest test-org-agenda-config-skip-overdue-normal-past-scheduled-keeps ()
+ "Entry scheduled in the past with TODO keyword is overdue — keep it."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Overdue task\n"
+ "SCHEDULED: " (test-org-timestamp-days-ago 7) "\n")
+ (should (null (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(ert-deftest test-org-agenda-config-skip-overdue-normal-future-scheduled-skips ()
+ "Entry scheduled in the future is not overdue — skip it."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Future task\n"
+ "SCHEDULED: " (test-org-timestamp-days-ahead 7) "\n")
+ (should (integerp (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(ert-deftest test-org-agenda-config-skip-overdue-normal-past-deadline-keeps ()
+ "Entry with past deadline and TODO keyword is overdue — keep it."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Missed deadline\n"
+ "DEADLINE: " (test-org-timestamp-days-ago 3) "\n")
+ (should (null (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(ert-deftest test-org-agenda-config-skip-overdue-normal-done-task-skips ()
+ "Done task should be skipped even if overdue."
+ (test-org-agenda--with-org-buffer
+ (concat "* DONE Completed task\n"
+ "SCHEDULED: " (test-org-timestamp-days-ago 7) "\n")
+ (should (integerp (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(ert-deftest test-org-agenda-config-skip-overdue-normal-habit-skips ()
+ "Habit should be skipped even if overdue."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Daily habit\n"
+ "SCHEDULED: " (test-org-timestamp-days-ago 7) "\n"
+ ":PROPERTIES:\n"
+ ":STYLE: habit\n"
+ ":END:\n")
+ (should (integerp (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(ert-deftest test-org-agenda-config-skip-overdue-normal-no-todo-keyword-skips ()
+ "Entry without a TODO keyword should be skipped."
+ (test-org-agenda--with-org-buffer
+ (concat "* Just a heading\n"
+ "SCHEDULED: " (test-org-timestamp-days-ago 7) "\n")
+ (should (integerp (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-org-agenda-config-skip-overdue-boundary-today-scheduled-skips ()
+ "Entry scheduled today is NOT overdue (not strictly before today) — skip."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Today task\n"
+ "SCHEDULED: " (test-org-timestamp-today) "\n")
+ (should (integerp (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(ert-deftest test-org-agenda-config-skip-overdue-boundary-no-date-skips ()
+ "Entry with TODO but no scheduled/deadline date — not overdue, skip."
+ (test-org-agenda--with-org-buffer "* TODO Undated task\n"
+ (should (integerp (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(ert-deftest test-org-agenda-config-skip-overdue-boundary-future-deadline-skips ()
+ "Entry with future deadline is not overdue — skip."
+ (test-org-agenda--with-org-buffer
+ (concat "* TODO Future deadline\n"
+ "DEADLINE: " (test-org-timestamp-days-ahead 14) "\n")
+ (should (integerp (cj/org-agenda-skip-subtree-if-not-overdue)))))
+
+(provide 'test-org-agenda-config-skip-functions)
+;;; test-org-agenda-config-skip-functions.el ends here
diff --git a/tests/test-org-refile-config-ensure-org-mode.el b/tests/test-org-refile-config-ensure-org-mode.el
new file mode 100644
index 00000000..c86ad269
--- /dev/null
+++ b/tests/test-org-refile-config-ensure-org-mode.el
@@ -0,0 +1,123 @@
+;;; test-org-refile-config-ensure-org-mode.el --- Tests for cj/org-refile-ensure-org-mode -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/org-refile-ensure-org-mode from org-refile-config.el.
+;; Uses testutil-general.el for filesystem setup/teardown.
+
+;;; Code:
+
+(require 'ert)
+(require 'org)
+
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(require 'testutil-general)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-refile-config)
+
+;;; Normal Cases
+
+(ert-deftest test-org-refile-ensure-org-mode-normal-org-file-returns-buffer ()
+ "A .org file already in org-mode should return its buffer."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (let ((file (cj/create-directory-or-file-ensuring-parents
+ "refile-test/notes.org" "* Heading\n")))
+ (let ((buf (find-file-noselect file)))
+ (unwind-protect
+ (progn
+ (with-current-buffer buf
+ (let ((org-mode-hook nil) (text-mode-hook nil))
+ (org-mode)))
+ (let ((result (cj/org-refile-ensure-org-mode file)))
+ (should (bufferp result))
+ (with-current-buffer result
+ (should (derived-mode-p 'org-mode)))))
+ (kill-buffer buf)))))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-refile-ensure-org-mode-normal-switches-fundamental-to-org ()
+ "A .org file in fundamental-mode should be switched to org-mode."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (let ((file (cj/create-directory-or-file-ensuring-parents
+ "refile-test/stuck.org" "* TODO Task\n")))
+ (let ((buf (find-file-noselect file)))
+ (unwind-protect
+ (progn
+ (with-current-buffer buf
+ (fundamental-mode))
+ (should (with-current-buffer buf
+ (eq major-mode 'fundamental-mode)))
+ (let ((org-mode-hook nil) (text-mode-hook nil))
+ (cj/org-refile-ensure-org-mode file))
+ (with-current-buffer buf
+ (should (derived-mode-p 'org-mode))))
+ (kill-buffer buf)))))
+ (cj/delete-test-base-dir)))
+
+;;; Error Cases
+
+(ert-deftest test-org-refile-ensure-org-mode-error-non-org-extension ()
+ "A non-.org file should signal an error."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (let ((file (cj/create-directory-or-file-ensuring-parents
+ "refile-test/notes.txt" "Some text\n")))
+ (should-error (cj/org-refile-ensure-org-mode file)
+ :type 'error)))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-refile-ensure-org-mode-error-markdown-extension ()
+ "A .md file should signal an error."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (let ((file (cj/create-directory-or-file-ensuring-parents
+ "refile-test/notes.md" "# Heading\n")))
+ (should-error (cj/org-refile-ensure-org-mode file)
+ :type 'error)))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-refile-ensure-org-mode-error-no-extension ()
+ "A file without an extension should signal an error."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (let ((file (cj/create-directory-or-file-ensuring-parents
+ "refile-test/notes" "content\n")))
+ (should-error (cj/org-refile-ensure-org-mode file)
+ :type 'error)))
+ (cj/delete-test-base-dir)))
+
+;;; Boundary Cases
+
+(ert-deftest test-org-refile-ensure-org-mode-boundary-org-in-dirname ()
+ "A file with .org in the directory name but not the extension should error."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (let ((file (cj/create-directory-or-file-ensuring-parents
+ "refile-test/my.org.files/todo.txt" "* TODO\n")))
+ (should-error (cj/org-refile-ensure-org-mode file)
+ :type 'error)))
+ (cj/delete-test-base-dir)))
+
+(ert-deftest test-org-refile-ensure-org-mode-boundary-uppercase-org-extension ()
+ "A .ORG file (uppercase) should be accepted (case-insensitive match)."
+ (unwind-protect
+ (progn
+ (cj/create-test-base-dir)
+ (let ((file (cj/create-directory-or-file-ensuring-parents
+ "refile-test/notes.ORG" "* Heading\n")))
+ (let ((buf (cj/org-refile-ensure-org-mode file)))
+ (unwind-protect
+ (should (bufferp buf))
+ (when (buffer-live-p buf) (kill-buffer buf))))))
+ (cj/delete-test-base-dir)))
+
+(provide 'test-org-refile-config-ensure-org-mode)
+;;; test-org-refile-config-ensure-org-mode.el ends here
diff --git a/tests/testutil-org.el b/tests/testutil-org.el
new file mode 100644
index 00000000..c1949981
--- /dev/null
+++ b/tests/testutil-org.el
@@ -0,0 +1,32 @@
+;;; testutil-org.el --- Test utilities for org-mode tests -*- lexical-binding: t; -*-
+;;
+;; Author: Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; Utilities for testing org-mode related modules.
+;; Provides dynamic timestamp generation for deterministic date-sensitive tests.
+;; No hardcoded dates — all timestamps generated relative to current time.
+;;
+;; See also: testutil-calendar-sync.el for iCal-specific timestamp utilities.
+
+;;; Code:
+
+(defun test-org-timestamp-days-ago (days)
+ "Generate org timestamp string for DAYS ago.
+Returns string like \"<2026-02-10 Tue>\"."
+ (format-time-string "<%Y-%m-%d %a>"
+ (time-subtract (current-time) (* days 86400))))
+
+(defun test-org-timestamp-days-ahead (days)
+ "Generate org timestamp string for DAYS from now.
+Returns string like \"<2026-02-20 Fri>\"."
+ (format-time-string "<%Y-%m-%d %a>"
+ (time-add (current-time) (* days 86400))))
+
+(defun test-org-timestamp-today ()
+ "Generate org timestamp string for today.
+Returns string like \"<2026-02-15 Sun>\"."
+ (format-time-string "<%Y-%m-%d %a>"))
+
+(provide 'testutil-org)
+;;; testutil-org.el ends here