aboutsummaryrefslogtreecommitdiff
path: root/tests/test-pearl-sync-hooks.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 13:44:34 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 13:44:34 -0500
commitb081d62276378b3168c92c06153fd59db0589535 (patch)
tree9be7f7d22e0c9b4a73432fe744c09bb456c671a9 /tests/test-pearl-sync-hooks.el
downloadpearl-b081d62276378b3168c92c06153fd59db0589535.tar.gz
pearl-b081d62276378b3168c92c06153fd59db0589535.zip
feat: pearl — manage Linear issues from org-mode
Pearl fetches Linear issues into an org file and syncs edits back. It covers list / custom views / saved queries, per-issue and bulk rendering with comments inline, conflict-aware sync of descriptions, titles, and comments, field commands for priority / state / assignee / labels, and a transient dispatch menu. The render folds to a scannable outline and nests issues under a sortable parent. Based on and inspired by Gael Blanchemain's linear-emacs.
Diffstat (limited to 'tests/test-pearl-sync-hooks.el')
-rw-r--r--tests/test-pearl-sync-hooks.el175
1 files changed, 175 insertions, 0 deletions
diff --git a/tests/test-pearl-sync-hooks.el b/tests/test-pearl-sync-hooks.el
new file mode 100644
index 0000000..05864e7
--- /dev/null
+++ b/tests/test-pearl-sync-hooks.el
@@ -0,0 +1,175 @@
+;;; test-pearl-sync-hooks.el --- Tests for pearl org sync hooks -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for the org sync hook wiring: enable/disable add and remove
+;; buffer-local hooks; the after-save hook only fires for linear.org buffers;
+;; and per-heading sync degrades gracefully when point is before any heading.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+;;; enable / disable
+
+(ert-deftest test-pearl-enable-org-sync-adds-buffer-local-hooks ()
+ "Enabling sync adds both hook functions buffer-locally."
+ (with-temp-buffer
+ (pearl-enable-org-sync)
+ (should (memq 'pearl-org-hook-function after-save-hook))
+ (should (memq 'pearl-sync-org-to-linear org-after-todo-state-change-hook))))
+
+(ert-deftest test-pearl-disable-org-sync-removes-hooks ()
+ "Disabling sync removes both hook functions."
+ (with-temp-buffer
+ (pearl-enable-org-sync)
+ (pearl-disable-org-sync)
+ (should-not (memq 'pearl-org-hook-function after-save-hook))
+ (should-not (memq 'pearl-sync-org-to-linear org-after-todo-state-change-hook))))
+
+;;; org-hook-function buffer guard
+
+(ert-deftest test-pearl-org-hook-function-skips-other-buffer ()
+ "The after-save hook does nothing in a buffer that isn't the configured file."
+ (let ((called nil)
+ (pearl-org-file-path "/tmp/linear.org"))
+ (cl-letf (((symbol-function 'pearl-sync-org-to-linear) (lambda () (setq called t))))
+ (with-temp-buffer
+ (setq buffer-file-name "/tmp/scratch.org")
+ (pearl-org-hook-function)
+ (should-not called)))))
+
+(ert-deftest test-pearl-org-hook-function-syncs-configured-buffer ()
+ "The after-save hook syncs when the buffer visits `pearl-org-file-path'."
+ (let ((called nil)
+ (pearl-org-file-path "/tmp/linear.org"))
+ (cl-letf (((symbol-function 'pearl-sync-org-to-linear) (lambda () (setq called t))))
+ (with-temp-buffer
+ (setq buffer-file-name "/tmp/linear.org")
+ (pearl-org-hook-function)
+ (should called)))))
+
+(ert-deftest test-pearl-org-hook-function-honors-custom-path ()
+ "A non-default `pearl-org-file-path' is what the hook matches on.
+Regression: the hook used to hardcode a \"linear.org$\" regex, so a buffer
+named linear.org fired even when the configured file was elsewhere, and a
+custom-named configured file never fired."
+ (let ((called nil)
+ (pearl-org-file-path "/tmp/my-linear-issues.org"))
+ (cl-letf (((symbol-function 'pearl-sync-org-to-linear) (lambda () (setq called t))))
+ ;; A buffer literally named linear.org must NOT fire when the configured
+ ;; file is something else.
+ (with-temp-buffer
+ (setq buffer-file-name "/tmp/linear.org")
+ (pearl-org-hook-function)
+ (should-not called))
+ ;; The configured custom-named file DOES fire.
+ (with-temp-buffer
+ (setq buffer-file-name "/tmp/my-linear-issues.org")
+ (pearl-org-hook-function)
+ (should called)))))
+
+(ert-deftest test-pearl-org-hook-function-matches-through-symlink ()
+ "A configured path and a visited symlink to the same file match via truename.
+The hook resolves both sides with `file-truename', so a symlink to the
+configured file still syncs -- this guards the choice of truename over a raw
+string compare."
+ (let ((real (make-temp-file "linear-real-" nil ".org"))
+ (link (make-temp-file "linear-link-" nil ".org"))
+ (called nil))
+ (unwind-protect
+ (progn
+ (delete-file link)
+ (make-symbolic-link real link)
+ (let ((pearl-org-file-path real))
+ (cl-letf (((symbol-function 'pearl-sync-org-to-linear)
+ (lambda () (setq called t))))
+ (with-temp-buffer
+ (setq buffer-file-name link)
+ (pearl-org-hook-function)
+ (should called)))))
+ (when (file-exists-p link) (delete-file link))
+ (when (file-exists-p real) (delete-file real)))))
+
+(ert-deftest test-pearl-org-hook-function-nil-path-no-op ()
+ "With `pearl-org-file-path' nil, the hook is a no-op and does not error."
+ (let ((called nil)
+ (pearl-org-file-path nil))
+ (cl-letf (((symbol-function 'pearl-sync-org-to-linear) (lambda () (setq called t))))
+ (with-temp-buffer
+ (setq buffer-file-name "/tmp/linear.org")
+ (should (progn (pearl-org-hook-function) t))
+ (should-not called)))))
+
+;;; sync-current-heading-to-linear
+
+(ert-deftest test-pearl-sync-current-heading-before-first-heading-no-error ()
+ "Syncing with point before the first heading degrades gracefully.
+
+Regression: `org-back-to-heading' signals \"before first heading\" in the
+preamble, which must not propagate out of the sync entry point."
+ (cl-letf (((symbol-function 'pearl--process-heading-at-point) (lambda () nil)))
+ (with-temp-buffer
+ (insert "#+TITLE: x\n\npreamble line\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (progn (pearl-sync-current-heading-to-linear) t)))))
+
+(ert-deftest test-pearl-sync-current-heading-processes-on-heading ()
+ "Syncing from within an entry processes that heading."
+ (let ((called nil))
+ (cl-letf (((symbol-function 'pearl--process-heading-at-point)
+ (lambda () (setq called t))))
+ (with-temp-buffer
+ (insert "* Top\n*** TODO x\n")
+ (org-mode)
+ (goto-char (point-max))
+ (pearl-sync-current-heading-to-linear)
+ (should called)))))
+
+;;; sync-org-to-linear dispatcher
+
+(ert-deftest test-pearl-sync-org-to-linear-org-todo-syncs-current-heading ()
+ "When invoked from `org-todo', only the current heading is synced."
+ (let ((called nil)
+ (this-command 'org-todo))
+ (cl-letf (((symbol-function 'pearl-sync-current-heading-to-linear)
+ (lambda () (setq called t))))
+ (pearl-sync-org-to-linear)
+ (should called))))
+
+(ert-deftest test-pearl-sync-org-to-linear-otherwise-scans-whole-file ()
+ "Outside `org-todo', every matching heading in the buffer is processed."
+ (let ((count 0)
+ (this-command 'some-other-command)
+ (pearl-state-to-todo-mapping '(("Todo" . "TODO") ("Done" . "DONE")))
+ (pearl-todo-states-pattern nil)
+ (pearl--todo-states-pattern-source nil))
+ (cl-letf (((symbol-function 'pearl--process-heading-at-point)
+ (lambda () (setq count (1+ count)))))
+ (with-temp-buffer
+ (insert "*** TODO a\n*** DONE b\n")
+ (org-mode)
+ (pearl-sync-org-to-linear)
+ (should (= 2 count))))))
+
+(provide 'test-pearl-sync-hooks)
+;;; test-pearl-sync-hooks.el ends here