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
|
;;; test-pearl-saved.el --- Tests for saved queries + sort -*- 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 local saved queries and the sort layer: the client-side
;; `--sort-issues', the sort->orderBy mapping, and `pearl-run-saved-query'
;; threading the filter, source, and order through.
;;; Code:
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
(require 'cl-lib)
;;; --sort-issues
(defconst test-pearl--sort-sample
'((:title "banana" :priority 2 :updated-at "2026-05-23T10:00:00.000Z")
(:title "apple" :priority 1 :updated-at "2026-05-23T12:00:00.000Z")
(:title "cherry" :priority 3 :updated-at "2026-05-23T08:00:00.000Z"))
"Three issues for exercising the client-side sort.")
(ert-deftest test-pearl-sort-issues-nil-sort-unchanged ()
"With no sort key, the issues are returned in their original order."
(should (equal test-pearl--sort-sample
(pearl--sort-issues test-pearl--sort-sample nil nil))))
(ert-deftest test-pearl-sort-issues-title-asc ()
"Sorting by title ascending orders alphabetically."
(let ((out (pearl--sort-issues test-pearl--sort-sample 'title 'asc)))
(should (equal '("apple" "banana" "cherry")
(mapcar (lambda (i) (plist-get i :title)) out)))))
(ert-deftest test-pearl-sort-issues-updated-desc-default ()
"Sorting by updated, descending, puts the most recent first."
(let ((out (pearl--sort-issues test-pearl--sort-sample 'updated 'desc)))
(should (string= "apple" (plist-get (car out) :title)))
(should (string= "cherry" (plist-get (car (last out)) :title)))))
(ert-deftest test-pearl-sort-issues-priority-asc ()
"Sorting by priority ascending orders by the numeric value."
(let ((out (pearl--sort-issues test-pearl--sort-sample 'priority 'asc)))
(should (equal '(1 2 3) (mapcar (lambda (i) (plist-get i :priority)) out)))))
;;; --sort->order-by
(ert-deftest test-pearl-sort-to-order-by ()
"Updated/created map to the server orderBy; everything else defaults to updatedAt."
(should (eq 'updatedAt (pearl--sort->order-by 'updated)))
(should (eq 'createdAt (pearl--sort->order-by 'created)))
(should (eq 'updatedAt (pearl--sort->order-by 'title))))
;;; run-saved-query
(ert-deftest test-pearl-run-saved-query-threads-filter-and-source ()
"Running a saved query compiles its filter and renders with a sorted source."
(let ((pearl-saved-queries
'(("My bugs" :filter (:labels ("bug") :open t) :sort priority :order asc)))
(built nil) (rendered-source nil) (order nil))
(cl-letf (((symbol-function 'pearl--build-issue-filter)
(lambda (plist) (setq built plist) '((compiled . t))))
((symbol-function 'pearl--query-issues-async)
(lambda (_filter cb &optional ord) (setq order ord)
(funcall cb (pearl--make-query-result 'ok :issues nil))))
((symbol-function 'pearl--render-query-result)
(lambda (_result source) (setq rendered-source source))))
(pearl-run-saved-query "My bugs")
(should (equal '(:labels ("bug") :open t) built))
(should (eq 'priority (plist-get rendered-source :sort)))
(should (eq 'asc (plist-get rendered-source :order)))
(should (eq 'filter (plist-get rendered-source :type)))
(should (eq 'updatedAt order)))))
(ert-deftest test-pearl-run-saved-query-unknown-errors ()
"An unknown saved-query name signals a user error."
(let ((pearl-saved-queries '(("Known" :filter (:open t)))))
(should-error (pearl-run-saved-query "Missing") :type 'user-error)))
;;; render applies the source sort
(ert-deftest test-pearl-render-query-result-sorts-by-source ()
"The render boundary sorts issues by the source's sort/order before writing."
(let ((written nil)
(source '(:type filter :name "By title" :filter nil :sort title :order asc)))
(cl-letf (((symbol-function 'pearl--normalize-issue) #'identity)
((symbol-function 'pearl--update-org-from-issues)
(lambda (issues &optional _s _t) (setq written issues))))
(pearl--render-query-result
(pearl--make-query-result
'ok :issues '((:title "banana") (:title "apple")))
source)
(should (equal '("apple" "banana")
(mapcar (lambda (i) (plist-get i :title)) written))))))
;;; sort boundary cases
(ert-deftest test-pearl-sort-issues-title-nil-and-empty ()
"Title sort treats nil and empty titles as empty strings, ordered first ascending."
(let ((out (pearl--sort-issues
'((:title "banana") (:title nil) (:title "apple") (:title ""))
'title 'asc)))
;; both empties sort before the named titles; their relative order is the
;; stable input order (nil came before the empty string)
(should (equal '(nil "" "apple" "banana")
(mapcar (lambda (i) (plist-get i :title)) out)))))
(ert-deftest test-pearl-sort-issues-priority-nil-is-last-asc ()
"A nil (none) priority sorts last ascending and first descending."
(let ((issues '((:title "p2" :priority 2) (:title "none" :priority nil) (:title "p1" :priority 1))))
(should (equal '("p1" "p2" "none")
(mapcar (lambda (i) (plist-get i :title))
(pearl--sort-issues issues 'priority 'asc))))
(should (equal '("none" "p2" "p1")
(mapcar (lambda (i) (plist-get i :title))
(pearl--sort-issues issues 'priority 'desc))))))
(ert-deftest test-pearl-sort-issues-equal-keys-deterministic ()
"Equal keys keep input order ascending and reverse it descending (stable sort + nreverse)."
(let ((issues '((:title "a" :priority 2) (:title "b" :priority 2) (:title "c" :priority 2))))
(should (equal '("a" "b" "c")
(mapcar (lambda (i) (plist-get i :title))
(pearl--sort-issues issues 'priority 'asc))))
(should (equal '("c" "b" "a")
(mapcar (lambda (i) (plist-get i :title))
(pearl--sort-issues issues 'priority 'desc))))))
(ert-deftest test-pearl-render-query-result-sorts-with-missing-priority ()
"Render sorts by priority after normalization even when a node lacks the field."
(let ((written nil)
(source '(:type filter :name "By priority" :filter nil :sort priority :order asc)))
(cl-letf (((symbol-function 'pearl--normalize-issue) #'identity)
((symbol-function 'pearl--update-org-from-issues)
(lambda (issues &optional _s _t) (setq written issues))))
(pearl--render-query-result
(pearl--make-query-result
'ok :issues '((:title "hi" :priority 1) (:title "none") (:title "lo" :priority 3)))
source)
(should (equal '("hi" "lo" "none")
(mapcar (lambda (i) (plist-get i :title)) written))))))
(provide 'test-pearl-saved)
;;; test-pearl-saved.el ends here
|