;;; test-pearl-sync-hooks.el --- Tests for pearl org sync hooks -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings ;; Author: Craig Jennings ;; 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 . ;;; 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