flake-update-20260505
  1;;; batch-functions-test.el --- Tests for org-mode batch operations -*- lexical-binding: t; no-byte-compile: t -*-
  2
  3;; Copyright (C) 2025 Vincent Demeester
  4
  5;; Author: Vincent Demeester <vincent@sbr.pm>
  6;; Keywords: org, batch, automation, testing
  7
  8;;; Commentary:
  9
 10;; ERT tests for batch-functions.el
 11;; Run with: emacs --batch -L .. -l batch-functions.el -l batch-functions-test.el -f ert-run-tests-batch-and-exit
 12
 13;;; Code:
 14
 15(require 'ert)
 16(require 'org)
 17(require 'org-element)
 18
 19;; Load batch-functions from parent directory
 20(eval-and-compile
 21  (let* ((current-file (or load-file-name buffer-file-name))
 22         (parent-dir (when current-file
 23                       (file-name-directory (directory-file-name (file-name-directory current-file))))))
 24    (when parent-dir
 25      (add-to-list 'load-path parent-dir)))
 26  (require 'batch-functions))
 27
 28;;; Test Fixtures
 29
 30(defvar batch-test-fixture-dir
 31  (expand-file-name "fixtures" (file-name-directory load-file-name))
 32  "Directory containing test fixture files.")
 33
 34(defvar batch-test-fixture-file
 35  (expand-file-name "test-todos.org" batch-test-fixture-dir)
 36  "Main test fixture file.")
 37
 38;;; Helper Functions
 39
 40(defun batch-test--count-items (items)
 41  "Count number of items in ITEMS list."
 42  (length items))
 43
 44(defun batch-test--find-item-by-heading (items heading)
 45  "Find item in ITEMS with matching HEADING."
 46  (seq-find (lambda (item)
 47              (string= (alist-get 'heading item) heading))
 48            items))
 49
 50(defun batch-test--with-temp-org-file (content fn)
 51  "Create temp org file with CONTENT, call FN with filepath, then cleanup."
 52  (let ((temp-file (make-temp-file "org-test-" nil ".org")))
 53    (unwind-protect
 54        (progn
 55          (with-temp-file temp-file
 56            (insert content))
 57          (funcall fn temp-file))
 58      (when (file-exists-p temp-file)
 59        (delete-file temp-file)))))
 60
 61;;; Tests for Read Operations
 62
 63(ert-deftest test-org-batch-list-todos ()
 64  "Test listing all TODOs from file."
 65  (let ((todos (org-batch-list-todos batch-test-fixture-file)))
 66    (should (> (batch-test--count-items todos) 0))
 67    ;; Should find "Review PR #123"
 68    (should (batch-test--find-item-by-heading todos "Review PR #123"))
 69    ;; Should find "Buy groceries"
 70    (should (batch-test--find-item-by-heading todos "Buy groceries"))))
 71
 72(ert-deftest test-org-batch-list-todos-filter-state ()
 73  "Test filtering TODOs by state."
 74  (let ((next-todos (org-batch-list-todos batch-test-fixture-file "NEXT")))
 75    (should (> (batch-test--count-items next-todos) 0))
 76    ;; All items should have NEXT state
 77    (dolist (todo next-todos)
 78      (should (string= (alist-get 'todo todo) "NEXT")))
 79    ;; Should find "Implement authentication"
 80    (should (batch-test--find-item-by-heading next-todos "Implement authentication"))))
 81
 82(ert-deftest test-org-batch-list-todos-filter-priority ()
 83  "Test filtering TODOs by priority."
 84  (let ((high-priority (org-batch-list-todos batch-test-fixture-file nil 1)))
 85    (should (> (batch-test--count-items high-priority) 0))
 86    ;; All items should have priority 1
 87    (dolist (todo high-priority)
 88      (should (= (alist-get 'priority todo) 1)))
 89    ;; Should find "Buy groceries"
 90    (should (batch-test--find-item-by-heading high-priority "Buy groceries"))))
 91
 92(ert-deftest test-org-batch-list-todos-filter-tags ()
 93  "Test filtering TODOs by tags."
 94  (let ((work-todos (org-batch-list-todos batch-test-fixture-file nil nil '("work"))))
 95    (should (> (batch-test--count-items work-todos) 0))
 96    ;; All items should have :work: tag
 97    (dolist (todo work-todos)
 98      (should (member "work" (alist-get 'tags todo))))
 99    ;; Should find "Review PR #123"
100    (should (batch-test--find-item-by-heading work-todos "Review PR #123"))))
101
102(ert-deftest test-org-batch-scheduled-today ()
103  "Test getting scheduled items for a specific date."
104  (let ((scheduled (org-batch-scheduled-today batch-test-fixture-file "2025-12-23")))
105    (should (>= (batch-test--count-items scheduled) 1))
106    ;; Should include "Review PR #123" and "Buy groceries"
107    (should (batch-test--find-item-by-heading scheduled "Review PR #123"))))
108
109(ert-deftest test-org-batch-by-section ()
110  "Test getting TODOs by section."
111  (let ((work-section (org-batch-by-section batch-test-fixture-file "Work")))
112    (should (> (batch-test--count-items work-section) 0))
113    ;; Should find work-related tasks
114    (should (batch-test--find-item-by-heading work-section "Review PR #123")))
115
116  (let ((personal-section (org-batch-by-section batch-test-fixture-file "Personal")))
117    (should (> (batch-test--count-items personal-section) 0))
118    ;; Should find personal tasks
119    (should (batch-test--find-item-by-heading personal-section "Buy groceries"))))
120
121(ert-deftest test-org-batch-count-by-state ()
122  "Test counting TODOs by state."
123  (let ((counts (org-batch-count-by-state batch-test-fixture-file)))
124    (should (> (alist-get 'total counts) 0))
125    (should (> (alist-get 'TODO counts) 0))
126    (should (>= (alist-get 'NEXT counts) 1))
127    (should (>= (alist-get 'DONE counts) 1))
128    (should (>= (alist-get 'WAIT counts) 1))))
129
130(ert-deftest test-org-batch-search ()
131  "Test searching for content in TODOs."
132  (let ((matches (org-batch-search batch-test-fixture-file "OAuth2")))
133    (should (> (batch-test--count-items matches) 0))
134    ;; Should find "Implement authentication"
135    (should (batch-test--find-item-by-heading matches "Implement authentication"))))
136
137(ert-deftest test-org-batch-get-sections ()
138  "Test getting list of top-level sections."
139  (let ((sections (org-batch-get-sections batch-test-fixture-file)))
140    (should (>= (length sections) 3))
141    (should (member "Work" sections))
142    (should (member "Personal" sections))
143    (should (member "Archive" sections))))
144
145(ert-deftest test-org-batch-get-children ()
146  "Test getting direct children of a heading."
147  (let ((work-children (org-batch-get-children batch-test-fixture-file "Work")))
148    (should (>= (batch-test--count-items work-children) 2))
149    ;; Should find direct children only
150    (should (batch-test--find-item-by-heading work-children "Review PR #123"))
151    (should (batch-test--find-item-by-heading work-children "Implement authentication"))))
152
153;;; Tests for Write Operations
154
155(ert-deftest test-org-batch-add-todo ()
156  "Test adding a new TODO item."
157  (batch-test--with-temp-org-file
158   "* Work\n"
159   (lambda (temp-file)
160     (let ((result (org-batch-add-todo temp-file "Work" "New Task"
161                                       "2025-12-25" 2 '("test" "task"))))
162       (should result)
163       ;; Verify it was added
164       (let ((todos (org-batch-list-todos temp-file)))
165         (let ((new-task (batch-test--find-item-by-heading todos "New Task")))
166           (should new-task)
167           (should (string= (alist-get 'todo new-task) "TODO"))
168           (should (= (alist-get 'priority new-task) 2))
169           (should (member "test" (alist-get 'tags new-task)))
170           (should (member "task" (alist-get 'tags new-task)))))))))
171
172(ert-deftest test-org-batch-update-state ()
173  "Test updating TODO state."
174  (batch-test--with-temp-org-file
175   "* Work\n** TODO Test Task\n"
176   (lambda (temp-file)
177     (let ((result (org-batch-update-state temp-file "Test Task" "DONE")))
178       (should result)
179       ;; Verify state was updated
180       (let ((todos (org-batch-list-todos temp-file)))
181         (let ((task (batch-test--find-item-by-heading todos "Test Task")))
182           (should task)
183           (should (string= (alist-get 'todo task) "DONE"))))))))
184
185(ert-deftest test-org-batch-schedule-task ()
186  "Test scheduling a task."
187  (batch-test--with-temp-org-file
188   "* Work\n** TODO Test Task\n"
189   (lambda (temp-file)
190     (let ((result (org-batch-schedule-task temp-file "Test Task" "2025-12-25")))
191       (should result)
192       ;; Verify schedule was set
193       (let ((scheduled (org-batch-scheduled-today temp-file "2025-12-25")))
194         (should (batch-test--find-item-by-heading scheduled "Test Task")))))))
195
196(ert-deftest test-org-batch-set-priority ()
197  "Test setting task priority."
198  (batch-test--with-temp-org-file
199   "* Work\n** TODO Test Task\n"
200   (lambda (temp-file)
201     (let ((result (org-batch-set-priority temp-file "Test Task" 1)))
202       (should result)
203       ;; Verify priority was set
204       (let ((todos (org-batch-list-todos temp-file)))
205         (let ((task (batch-test--find-item-by-heading todos "Test Task")))
206           (should task)
207           (should (= (alist-get 'priority task) 1))))))))
208
209;;; Tests for Utility Functions
210
211(ert-deftest test-org-batch--priority-conversion ()
212  "Test priority number/character conversion.
213Priority mapping: '1'=1, '2'=2, '3'=3, '4'=4, '5'=5."
214  (should (= (org-batch--priority-to-number ?1) 1))
215  (should (= (org-batch--priority-to-number ?5) 5))
216  (should (= (org-batch--number-to-priority 1) ?1))
217  (should (= (org-batch--number-to-priority 5) ?5)))
218
219;;; Placeholder Tests for New Features
220
221(ert-deftest test-org-batch-get-todo-content ()
222  "Test getting full TODO content with metadata and body."
223  (let ((result (org-batch-get-todo-content batch-test-fixture-file "Review PR #123")))
224    (should result)
225    ;; Check basic metadata
226    (should (string= (alist-get 'heading result) "Review PR #123"))
227    (should (string= (alist-get 'todo result) "TODO"))
228    (should (member "work" (alist-get 'tags result)))
229    (should (member "code" (alist-get 'tags result)))
230    ;; Check properties
231    (let ((props (alist-get 'properties result)))
232      (should props)
233      (should (assoc "CREATED" props))
234      (should (assoc "PR_URL" props))
235      (should (string-match "github.com" (cdr (assoc "PR_URL" props)))))
236    ;; Check content
237    (let ((content (alist-get 'content result)))
238      (should content)
239      (should (string-match "Check for security" content))
240      (should (string-match "error handling" content))))
241
242  ;; Test non-existent heading
243  (let ((result (org-batch-get-todo-content batch-test-fixture-file "NonExistent Task")))
244    (should-not result)))
245
246(ert-deftest test-org-batch-add-tags ()
247  "Test adding tags to existing TODO."
248  (batch-test--with-temp-org-file
249   "* Work\n** TODO Test Task :existing:\n"
250   (lambda (temp-file)
251     (let ((result (org-batch-add-tags temp-file "Test Task" '("new" "tags"))))
252       (should result)
253       ;; Verify tags were added
254       (let ((todos (org-batch-list-todos temp-file)))
255         (let ((task (batch-test--find-item-by-heading todos "Test Task")))
256           (should task)
257           (should (member "existing" (alist-get 'tags task)))
258           (should (member "new" (alist-get 'tags task)))
259           (should (member "tags" (alist-get 'tags task)))))))))
260
261(ert-deftest test-org-batch-remove-tags ()
262  "Test removing tags from TODO."
263  (batch-test--with-temp-org-file
264   "* Work\n** TODO Test Task :tag1:tag2:tag3:\n"
265   (lambda (temp-file)
266     (let ((result (org-batch-remove-tags temp-file "Test Task" '("tag2"))))
267       (should result)
268       ;; Verify tag was removed
269       (let ((todos (org-batch-list-todos temp-file)))
270         (let ((task (batch-test--find-item-by-heading todos "Test Task")))
271           (should task)
272           (should (member "tag1" (alist-get 'tags task)))
273           (should-not (member "tag2" (alist-get 'tags task)))
274           (should (member "tag3" (alist-get 'tags task)))))))))
275
276(ert-deftest test-org-batch-list-all-tags ()
277  "Test listing all unique tags."
278  (let ((tags (org-batch-list-all-tags batch-test-fixture-file)))
279    (should (> (length tags) 0))
280    (should (member "work" tags))
281    (should (member "code" tags))
282    (should (member "personal" tags))
283    ;; Tags should be sorted and unique
284    (should (equal tags (sort (delete-dups tags) #'string<)))))
285
286(ert-deftest test-org-batch-get-overdue ()
287  "Test getting overdue tasks."
288  ;; Create a file with overdue task
289  (batch-test--with-temp-org-file
290   "* Work\n** TODO Overdue Task\nDEADLINE: <2020-01-01>\n"
291   (lambda (temp-file)
292     (let ((overdue (org-batch-get-overdue temp-file)))
293       (should (> (batch-test--count-items overdue) 0))
294       (should (batch-test--find-item-by-heading overdue "Overdue Task"))))))
295
296(ert-deftest test-org-batch-get-upcoming ()
297  "Test getting upcoming tasks."
298  ;; Create a file with upcoming task
299  (let ((future-date (format-time-string "%Y-%m-%d" (time-add (current-time) (days-to-time 3)))))
300    (batch-test--with-temp-org-file
301     (format "* Work\n** TODO Upcoming Task\nSCHEDULED: <%s>\n" future-date)
302     (lambda (temp-file)
303       (let ((upcoming (org-batch-get-upcoming temp-file 7)))
304         (should (> (batch-test--count-items upcoming) 0))
305         (should (batch-test--find-item-by-heading upcoming "Upcoming Task")))))))
306
307(ert-deftest test-org-batch-get-property ()
308  "Test getting property value."
309  (let ((prop-value (org-batch-get-property batch-test-fixture-file "Review PR #123" "PR_URL")))
310    (should prop-value)
311    (should (string-match "github.com" prop-value))))
312
313(ert-deftest test-org-batch-set-property ()
314  "Test setting property value."
315  (batch-test--with-temp-org-file
316   "* Work\n** TODO Test Task\n:PROPERTIES:\n:CREATED: [2025-12-23]\n:END:\n"
317   (lambda (temp-file)
318     (let ((result (org-batch-set-property temp-file "Test Task" "CUSTOM_PROP" "test-value")))
319       (should result)
320       ;; Verify property was set
321       (let ((value (org-batch-get-property temp-file "Test Task" "CUSTOM_PROP")))
322         (should value)
323         (should (string= value "test-value")))))))
324
325;;; Tests for Bulk Operations
326
327(ert-deftest test-org-batch-bulk-update-state ()
328  "Test bulk updating state of multiple tasks."
329  (batch-test--with-temp-org-file
330   "* Work\n** TODO Task 1\n** TODO Task 2 :urgent:\n** NEXT Task 3\n"
331   (lambda (temp-file)
332     (let ((count (org-batch-bulk-update-state temp-file "TODO" "DONE")))
333       (should (= count 2))
334       ;; Verify states were updated
335       (let ((todos (org-batch-list-todos temp-file)))
336         (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
337               (task2 (batch-test--find-item-by-heading todos "Task 2")))
338           (should (string= (alist-get 'todo task1) "DONE"))
339           (should (string= (alist-get 'todo task2) "DONE"))))))))
340
341(ert-deftest test-org-batch-bulk-update-state-with-tag-filter ()
342  "Test bulk updating state with tag filter."
343  (batch-test--with-temp-org-file
344   "* Work\n** TODO Task 1 :urgent:\n** TODO Task 2\n** TODO Task 3 :urgent:\n"
345   (lambda (temp-file)
346     (let ((count (org-batch-bulk-update-state temp-file "TODO" "NEXT" '("urgent"))))
347       (should (= count 2))
348       ;; Verify only urgent tasks were updated
349       (let ((todos (org-batch-list-todos temp-file)))
350         (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
351               (task2 (batch-test--find-item-by-heading todos "Task 2"))
352               (task3 (batch-test--find-item-by-heading todos "Task 3")))
353           (should (string= (alist-get 'todo task1) "NEXT"))
354           (should (string= (alist-get 'todo task2) "TODO"))
355           (should (string= (alist-get 'todo task3) "NEXT"))))))))
356
357(ert-deftest test-org-batch-bulk-add-tags ()
358  "Test bulk adding tags to multiple tasks."
359  (batch-test--with-temp-org-file
360   "* Work\n** TODO Task 1\n** TODO Task 2\n** NEXT Task 3\n"
361   (lambda (temp-file)
362     (let ((count (org-batch-bulk-add-tags temp-file "TODO" '("review" "urgent"))))
363       (should (= count 2))
364       ;; Verify tags were added
365       (let ((todos (org-batch-list-todos temp-file)))
366         (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
367               (task2 (batch-test--find-item-by-heading todos "Task 2")))
368           (should (member "review" (alist-get 'tags task1)))
369           (should (member "urgent" (alist-get 'tags task1)))
370           (should (member "review" (alist-get 'tags task2)))
371           (should (member "urgent" (alist-get 'tags task2)))))))))
372
373(ert-deftest test-org-batch-bulk-set-priority ()
374  "Test bulk setting priority for multiple tasks."
375  (batch-test--with-temp-org-file
376   "* Work\n** TODO Task 1\n** TODO Task 2\n** NEXT Task 3\n"
377   (lambda (temp-file)
378     (let ((count (org-batch-bulk-set-priority temp-file "TODO" 1)))
379       (should (= count 2))
380       ;; Verify priorities were set
381       (let ((todos (org-batch-list-todos temp-file)))
382         (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
383               (task2 (batch-test--find-item-by-heading todos "Task 2"))
384               (task3 (batch-test--find-item-by-heading todos "Task 3")))
385           (should (= (alist-get 'priority task1) 1))
386           (should (= (alist-get 'priority task2) 1))
387           (should-not (alist-get 'priority task3))))))))
388
389;;; Tests for Time Tracking
390
391(ert-deftest test-org-batch-clock-in ()
392  "Test clocking in to a task."
393  (batch-test--with-temp-org-file
394   "* Work\n** TODO Test Task\n"
395   (lambda (temp-file)
396     (let ((result (org-batch-clock-in temp-file "Test Task")))
397       (should result)
398       ;; Verify clock entry was added
399       (with-temp-buffer
400         (insert-file-contents temp-file)
401         (should (string-match-p "CLOCK: \\[" (buffer-string))))))))
402
403(ert-deftest test-org-batch-clock-out ()
404  "Test clocking out of a task."
405  (batch-test--with-temp-org-file
406   "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]\n:END:\n"
407   (lambda (temp-file)
408     (let ((result (org-batch-clock-out temp-file)))
409       (should result)
410       ;; Verify clock was closed with end time
411       (with-temp-buffer
412         (insert-file-contents temp-file)
413         (should (string-match-p "CLOCK: \\[.*?\\]--\\[.*?\\] =>" (buffer-string))))))))
414
415(ert-deftest test-org-batch-get-active-clock ()
416  "Test getting active clock."
417  (batch-test--with-temp-org-file
418   "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]\n:END:\n"
419   (lambda (temp-file)
420     (let ((result (org-batch-get-active-clock temp-file)))
421       (should result)
422       (should (string= (alist-get 'heading result) "Test Task"))
423       (should (alist-get 'clock_start result))))))
424
425(ert-deftest test-org-batch-get-active-clock-none ()
426  "Test getting active clock when none exists."
427  (batch-test--with-temp-org-file
428   "* Work\n** TODO Test Task\n"
429   (lambda (temp-file)
430     (let ((result (org-batch-get-active-clock temp-file)))
431       (should-not result)))))
432
433(ert-deftest test-org-batch-get-clocked-time ()
434  "Test getting total clocked time for a task."
435  (batch-test--with-temp-org-file
436   "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]--[2025-12-23 Mon 11:30] =>  1:30\n:END:\n"
437   (lambda (temp-file)
438     (let ((minutes (org-batch-get-clocked-time temp-file "Test Task")))
439       (should (> minutes 0))
440       (should (= minutes 90))))))  ; 1:30 = 90 minutes
441
442;;; Tests for Statistics & Analytics
443
444(ert-deftest test-org-batch-get-statistics ()
445  "Test getting comprehensive statistics."
446  (let ((stats (org-batch-get-statistics batch-test-fixture-file)))
447    (should stats)
448    (should (> (alist-get 'total stats) 0))
449    (should (alist-get 'by_state stats))
450    (should (alist-get 'by_priority stats))
451    (should (alist-get 'by_tag stats))
452    ;; Verify we have state counts
453    (let ((by-state (alist-get 'by_state stats)))
454      (should (assoc 'TODO by-state))
455      (should (assoc 'NEXT by-state)))))
456
457(ert-deftest test-org-batch-get-priority-distribution ()
458  "Test getting priority distribution."
459  (let ((distribution (org-batch-get-priority-distribution batch-test-fixture-file)))
460    (should distribution)
461    ;; Should have entries for priorities 1-5
462    (should (assoc 1 distribution))
463    (should (assoc 2 distribution))
464    (should (assoc 3 distribution))
465    (should (assoc 4 distribution))
466    (should (assoc 5 distribution))))
467
468(ert-deftest test-org-batch-get-tag-statistics ()
469  "Test getting tag statistics."
470  (let ((tag-stats (org-batch-get-tag-statistics batch-test-fixture-file)))
471    (should tag-stats)
472    (should (> (length tag-stats) 0))
473    ;; Should be sorted by count (descending)
474    (when (>= (length tag-stats) 2)
475      (should (>= (cdr (nth 0 tag-stats)) (cdr (nth 1 tag-stats)))))))
476
477;;; Tests for Export & Reporting
478
479(ert-deftest test-org-batch-export-csv ()
480  "Test exporting TODOs to CSV."
481  (let ((output-file (make-temp-file "org-export-" nil ".csv")))
482    (unwind-protect
483        (progn
484          (should (org-batch-export-csv batch-test-fixture-file output-file))
485          ;; Verify file was created
486          (should (file-exists-p output-file))
487          ;; Verify it has content
488          (with-temp-buffer
489            (insert-file-contents output-file)
490            (should (> (buffer-size) 0))
491            ;; Should have CSV header
492            (should (string-match-p "heading,state,priority" (buffer-string)))))
493      (when (file-exists-p output-file)
494        (delete-file output-file)))))
495
496(ert-deftest test-org-batch-export-json ()
497  "Test exporting TODOs to JSON."
498  (let ((output-file (make-temp-file "org-export-" nil ".json")))
499    (unwind-protect
500        (progn
501          (should (org-batch-export-json batch-test-fixture-file output-file))
502          ;; Verify file was created
503          (should (file-exists-p output-file))
504          ;; Verify it has valid JSON
505          (with-temp-buffer
506            (insert-file-contents output-file)
507            (should (> (buffer-size) 0))
508            ;; Should be valid JSON (starts with [)
509            (should (string-match-p "^\\[" (buffer-string)))))
510      (when (file-exists-p output-file)
511        (delete-file output-file)))))
512
513(provide 'batch-functions-test)
514;;; batch-functions-test.el ends here