aboutsummaryrefslogtreecommitdiff
path: root/tests/test-pearl-sync-hooks.el
blob: 05864e73407f518df43d8fbe9a1a912b5a7689a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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