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
|
;;; test-pearl-todo-keywords.el --- Tests for deriving TODO keywords from states -*- 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 pure derivation helpers behind the workspace-derived #+TODO
;; line: `pearl--state-name-to-keyword' (slugify a Linear state name to an Org
;; keyword) and `pearl--derive-todo-line' (an ordered state list to the
;; "ACTIVE... | DONE..." keyword string).
;;; Code:
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
;;; --state-name-to-keyword (slugify)
(ert-deftest test-pearl-state-name-to-keyword-spaces-and-case ()
"Spaces become hyphens and the name is upcased."
(should (string= "DEV-REVIEW" (pearl--state-name-to-keyword "Dev Review")))
(should (string= "IN-PROGRESS" (pearl--state-name-to-keyword "In Progress")))
(should (string= "TODO" (pearl--state-name-to-keyword "Todo"))))
(ert-deftest test-pearl-state-name-to-keyword-punctuation-runs ()
"A run of non-alphanumerics collapses to a single hyphen; edges trimmed."
(should (string= "PM-ACCEPTANCE" (pearl--state-name-to-keyword "PM Acceptance")))
(should (string= "BACKLOG-PRIORITIZED"
(pearl--state-name-to-keyword "Backlog (prioritized)"))))
(ert-deftest test-pearl-state-name-to-keyword-unicode-preserved ()
"Unicode letters are kept (alnum is Unicode-aware), not stripped."
(should (string= "ÅNGSTRÖM" (pearl--state-name-to-keyword "Ångström"))))
(ert-deftest test-pearl-state-name-to-keyword-empty-slug-falls-back ()
"An all-symbol name (empty slug) falls back to TODO, as does the empty string."
(should (string= "TODO" (pearl--state-name-to-keyword "!!!")))
(should (string= "TODO" (pearl--state-name-to-keyword ""))))
;;; --derive-todo-line
(ert-deftest test-pearl-derive-todo-line-partitions-by-type ()
"Completed/canceled/duplicate states go after the bar; the rest before it."
(should (string= "TODO IN-PROGRESS | DONE CANCELED"
(pearl--derive-todo-line
'((:name "Todo" :type "unstarted")
(:name "In Progress" :type "started")
(:name "Done" :type "completed")
(:name "Canceled" :type "canceled"))))))
(ert-deftest test-pearl-derive-todo-line-dedups-by-slug ()
"Two names slugifying to the same keyword list it once (first-seen)."
(should (string= "DEV-REVIEW |"
(pearl--derive-todo-line
'((:name "Dev Review" :type "started")
(:name "Dev-Review" :type "started"))))))
(ert-deftest test-pearl-derive-todo-line-preserves-input-order ()
"Within each side, the caller's order is preserved."
(should (string= "BACKLOG TODO IN-PROGRESS | DONE"
(pearl--derive-todo-line
'((:name "Backlog" :type "backlog")
(:name "Todo" :type "unstarted")
(:name "In Progress" :type "started")
(:name "Done" :type "completed"))))))
(ert-deftest test-pearl-derive-todo-line-empty-is-hardcoded-fallback ()
"An empty state list yields the hardcoded default so the file stays valid."
(should (string= "TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE"
(pearl--derive-todo-line '()))))
;;; --gather-header-states (teams first-seen, states by position)
(ert-deftest test-pearl-gather-header-states-orders-by-team-then-position ()
"Teams come in first-seen order; within a team, states sort by position."
(cl-letf (((symbol-function 'pearl--team-states)
(lambda (tid)
(pcase tid
("t1" '(((id . "s2") (name . "In Progress") (type . "started") (position . 2.0))
((id . "s1") (name . "Todo") (type . "unstarted") (position . 1.0))))
("t2" '(((id . "s3") (name . "Done") (type . "completed") (position . 9.0))))))))
(let* ((issues (list (list :team '(:id "t1") :state '(:name "Todo" :type "unstarted"))
(list :team '(:id "t2") :state '(:name "Done" :type "completed"))))
(line (pearl--derive-todo-line (pearl--gather-header-states issues))))
(should (string= "TODO IN-PROGRESS | DONE" line)))))
(ert-deftest test-pearl-gather-header-states-covers-failed-team ()
"A team whose fetch returns nil is skipped, but its issues' own states still appear."
(cl-letf (((symbol-function 'pearl--team-states) (lambda (_tid) nil)))
(let* ((issues (list (list :team '(:id "t9") :state '(:name "Grooming" :type "backlog"))))
(line (pearl--derive-todo-line (pearl--gather-header-states issues))))
(should (string-match-p "GROOMING" line)))))
;;; --build-org-content with a derived #+TODO
(ert-deftest test-pearl-build-org-content-derives-todo-from-states ()
"With a states list, the #+TODO line is derived (not the hardcoded default)."
(let ((out (pearl--build-org-content
'() '(:type filter :name "X" :filter nil) nil
'((:name "Todo" :type "unstarted")
(:name "Dev Review" :type "started")
(:name "Done" :type "completed")))))
(should (string-match-p "^#\\+TODO: TODO DEV-REVIEW \\| DONE$" out))))
(ert-deftest test-pearl-build-org-content-no-states-keeps-fallback ()
"With no states (nil), the #+TODO line stays the hardcoded default."
(let ((out (pearl--build-org-content '() '(:type filter :name "X" :filter nil) nil nil)))
(should (string-match-p "^#\\+TODO: TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED \\| DONE$" out))))
(provide 'test-pearl-todo-keywords)
;;; test-pearl-todo-keywords.el ends here
|