diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-24 13:44:34 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-24 13:44:34 -0500 |
| commit | b081d62276378b3168c92c06153fd59db0589535 (patch) | |
| tree | 9be7f7d22e0c9b4a73432fe744c09bb456c671a9 /tests/test-pearl-sync-hooks.el | |
| download | pearl-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.el | 175 |
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 |
