summaryrefslogtreecommitdiff
path: root/tests/test-org-agenda-build-list.el
blob: 6b42420045ab3431f80f28303564f27bed8ce75e (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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
;;; test-org-agenda-build-list.el --- Tests for cj/build-org-agenda-list -*- lexical-binding: t; -*-

;;; Commentary:
;; Unit tests for cj/build-org-agenda-list caching logic.
;; Tests cache behavior, TTL expiration, force rebuild, and async build flag.

;;; Code:

(require 'ert)

;; Add modules to load path
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))

;; Stub dependencies before loading the module
(defvar inbox-file "/tmp/test-inbox.org")
(defvar schedule-file "/tmp/test-schedule.org")
(defvar gcal-file "/tmp/test-gcal.org")
(defvar projects-dir "/tmp/test-projects/")

;; Now load the actual production module
(require 'org-agenda-config)

;;; Setup and Teardown

(defun test-org-agenda-setup ()
  "Reset cache and state before each test."
  (setq cj/org-agenda-files-cache nil)
  (setq cj/org-agenda-files-cache-time nil)
  (setq cj/org-agenda-files-building nil)
  (setq org-agenda-files nil))

(defun test-org-agenda-teardown ()
  "Clean up after each test."
  (setq cj/org-agenda-files-cache nil)
  (setq cj/org-agenda-files-cache-time nil)
  (setq cj/org-agenda-files-building nil)
  (setq org-agenda-files nil))

;;; Normal Cases

(ert-deftest test-org-agenda-build-list-normal-first-call-builds-cache ()
  "Test that first call builds cache from scratch.

When cache is empty, function should:
1. Scan directory for todo.org files
2. Build agenda files list
3. Populate cache
4. Set cache timestamp"
  (test-org-agenda-setup)
  (unwind-protect
      (cl-letf (((symbol-function 'directory-files-recursively)
                 (lambda (_dir _pattern &optional _include-dirs) '("/tmp/project/todo.org"))))

        ;; Before call: cache empty
        (should (null cj/org-agenda-files-cache))
        (should (null cj/org-agenda-files-cache-time))

        ;; Build agenda files
        (cj/build-org-agenda-list)

        ;; After call: cache populated
        (should cj/org-agenda-files-cache)
        (should cj/org-agenda-files-cache-time)
        (should org-agenda-files)

        ;; Cache matches org-agenda-files
        (should (equal cj/org-agenda-files-cache org-agenda-files))

        ;; Contains base files (inbox, schedule, gcal) plus project files
        (should (>= (length org-agenda-files) 3)))
    (test-org-agenda-teardown)))

(ert-deftest test-org-agenda-build-list-normal-second-call-uses-cache ()
  "Test that second call uses cache instead of rebuilding.

When cache is valid (not expired):
1. Should NOT scan directories again
2. Should restore files from cache
3. Should NOT update cache timestamp"
  (test-org-agenda-setup)
  (unwind-protect
      (let ((scan-count 0))
        (cl-letf (((symbol-function 'directory-files-recursively)
                   (lambda (_dir _pattern &optional _include-dirs)
                     (setq scan-count (1+ scan-count))
                     '("/tmp/project/todo.org"))))

          ;; First call: builds cache
          (cj/build-org-agenda-list)
          (should (= scan-count 1))  ; 1 directory scanned

          (let ((cached-time cj/org-agenda-files-cache-time)
                (cached-files cj/org-agenda-files-cache))

            ;; Second call: uses cache
            (cj/build-org-agenda-list)

            ;; Scan count unchanged (cache hit)
            (should (= scan-count 1))

            ;; Cache unchanged
            (should (equal cj/org-agenda-files-cache-time cached-time))
            (should (equal cj/org-agenda-files-cache cached-files)))))
    (test-org-agenda-teardown)))

(ert-deftest test-org-agenda-build-list-normal-force-rebuild-bypasses-cache ()
  "Test that force-rebuild parameter bypasses cache.

When force-rebuild is non-nil:
1. Should ignore valid cache
2. Should rebuild from scratch
3. Should update cache with new data"
  (test-org-agenda-setup)
  (unwind-protect
      (let ((scan-count 0))
        (cl-letf (((symbol-function 'directory-files-recursively)
                   (lambda (_dir _pattern &optional _include-dirs)
                     (setq scan-count (1+ scan-count))
                     (if (> scan-count 1)
                         '("/tmp/project/todo.org" "/tmp/project2/todo.org")  ; New file on rebuild
                       '("/tmp/project/todo.org")))))

          ;; First call: builds cache
          (cj/build-org-agenda-list)
          (let ((initial-count (length org-agenda-files)))

            ;; Force rebuild
            (cj/build-org-agenda-list 'force)

            ;; Scanned again
            (should (= scan-count 2))

            ;; New files include additional project
            (should (> (length org-agenda-files) initial-count)))))
    (test-org-agenda-teardown)))

;;; Boundary Cases

(ert-deftest test-org-agenda-build-list-boundary-cache-expires-after-ttl ()
  "Test that cache expires after TTL period.

When cache timestamp exceeds TTL:
1. Should rebuild files list
2. Should update cache timestamp
3. Should rescan directory"
  (test-org-agenda-setup)
  (unwind-protect
      (let ((scan-count 0))
        (cl-letf (((symbol-function 'directory-files-recursively)
                   (lambda (_dir _pattern &optional _include-dirs)
                     (setq scan-count (1+ scan-count))
                     '("/tmp/project/todo.org"))))

          ;; First call: builds cache
          (cj/build-org-agenda-list)
          (should (= scan-count 1))

          ;; Simulate cache expiration (set time to 2 hours ago)
          (setq cj/org-agenda-files-cache-time
                (- (float-time) (* 2 3600)))

          ;; Second call: cache expired, rebuild
          (cj/build-org-agenda-list)

          ;; Scanned again (cache was expired)
          (should (= scan-count 2))

          ;; Cache timestamp updated to current time
          (should (< (- (float-time) cj/org-agenda-files-cache-time) 1))))
    (test-org-agenda-teardown)))

(ert-deftest test-org-agenda-build-list-boundary-empty-directory-creates-minimal-list ()
  "Test behavior when directory contains no todo.org files.

When directory scan returns empty:
1. Should still create base files (inbox, schedule)
2. Should not fail or error
3. Should cache the minimal result"
  (test-org-agenda-setup)
  (unwind-protect
      (cl-letf (((symbol-function 'directory-files-recursively)
                 (lambda (_dir _pattern &optional _include-dirs) nil)))  ; No files found

        (cj/build-org-agenda-list)

        ;; Should have base files only (inbox, schedule, gcal)
        (should (= (length org-agenda-files) 3))

        ;; Cache should contain base files
        (should cj/org-agenda-files-cache)
        (should (= (length cj/org-agenda-files-cache) 3)))
    (test-org-agenda-teardown)))

(ert-deftest test-org-agenda-build-list-boundary-building-flag-set-during-build ()
  "Test that building flag is set during build and cleared after.

During build:
1. Flag should be set to prevent concurrent builds
2. Flag should clear even if build fails
3. Flag state should be consistent"
  (test-org-agenda-setup)
  (unwind-protect
      (let ((flag-during-build nil))
        (cl-letf (((symbol-function 'directory-files-recursively)
                   (lambda (_dir _pattern &optional _include-dirs)
                     ;; Capture flag state during directory scan
                     (setq flag-during-build cj/org-agenda-files-building)
                     '("/tmp/project/todo.org"))))

          ;; Before build
          (should (null cj/org-agenda-files-building))

          ;; Build
          (cj/build-org-agenda-list)

          ;; Flag was set during build
          (should flag-during-build)

          ;; Flag cleared after build
          (should (null cj/org-agenda-files-building))))
    (test-org-agenda-teardown)))

(ert-deftest test-org-agenda-build-list-boundary-building-flag-clears-on-error ()
  "Test that building flag clears even if build errors.

When build encounters error:
1. Flag should still be cleared (unwind-protect)
2. Prevents permanently locked state
3. Next build can proceed"
  (test-org-agenda-setup)
  (unwind-protect
      (cl-letf (((symbol-function 'directory-files-recursively)
                 (lambda (_dir _pattern &optional _include-dirs)
                   (error "Simulated scan failure"))))

        ;; Build will error
        (should-error (cj/build-org-agenda-list))

        ;; Flag cleared despite error (unwind-protect)
        (should (null cj/org-agenda-files-building)))
    (test-org-agenda-teardown)))

;;; Error Cases

(ert-deftest test-org-agenda-build-list-error-nil-cache-with-old-timestamp ()
  "Test handling of inconsistent state (nil cache but timestamp set).

When cache is nil but timestamp exists:
1. Should recognize cache as invalid
2. Should rebuild files list
3. Should set both cache and timestamp"
  (test-org-agenda-setup)
  (unwind-protect
      (cl-letf (((symbol-function 'directory-files-recursively)
                 (lambda (_dir _pattern &optional _include-dirs) '("/tmp/project/todo.org"))))

        ;; Set inconsistent state
        (setq cj/org-agenda-files-cache nil)
        (setq cj/org-agenda-files-cache-time (float-time))

        ;; Build should recognize invalid state
        (cj/build-org-agenda-list)

        ;; Cache now populated
        (should cj/org-agenda-files-cache)
        (should cj/org-agenda-files-cache-time)
        (should org-agenda-files))
    (test-org-agenda-teardown)))

(ert-deftest test-org-agenda-build-list-error-directory-scan-failure-propagates ()
  "Test that directory scan failures propagate as errors.

When directory-files-recursively errors:
1. Error should propagate to caller
2. Cache should not be corrupted
3. Building flag should clear"
  (test-org-agenda-setup)
  (unwind-protect
      (cl-letf (((symbol-function 'directory-files-recursively)
                 (lambda (_dir _pattern &optional _include-dirs)
                   (error "Permission denied"))))

        ;; Should propagate error
        (should-error (cj/build-org-agenda-list))

        ;; Cache not corrupted (still nil)
        (should (null cj/org-agenda-files-cache))

        ;; Building flag cleared
        (should (null cj/org-agenda-files-building)))
    (test-org-agenda-teardown)))

(provide 'test-org-agenda-build-list)
;;; test-org-agenda-build-list.el ends here