aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 16:43:00 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 16:43:00 -0500
commitd03d5582197def92ad72e113815a3c4836da1330 (patch)
tree16901e08fcd7940a864799918e523176dcf0e157 /tests
parent0e3c7b23f610834b1300ef0821e2a5978201fbf6 (diff)
downloadpearl-d03d5582197def92ad72e113815a3c4836da1330.tar.gz
pearl-d03d5582197def92ad72e113815a3c4836da1330.zip
feat: render issue headings with the identifier prefix and title case
I added two display-only heading tweaks, each a defcustom defaulting on. pearl-show-identifier-in-heading prefixes the title with the Linear identifier (** TODO [#B] SE-401: Fix the Bug). pearl-title-case-headings renders the title in smart title case, keeping minor words lowercase unless first or last and leaving words that already carry an uppercase letter (acronyms, identifiers) untouched. Both stay display-only. The LINEAR-TITLE-SHA256 hash covers the rendered (cased, un-prefixed) title, and the title-sync reader strips the identifier prefix before hashing, so an unedited heading is a no-op and neither the casing nor the prefix ever pushes to Linear. A render-then-read round-trip test locks that invariant.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-pearl-format.el17
-rw-r--r--tests/test-pearl-heading.el117
-rw-r--r--tests/test-pearl-org-write.el4
-rw-r--r--tests/test-pearl-title-sync.el23
4 files changed, 151 insertions, 10 deletions
diff --git a/tests/test-pearl-format.el b/tests/test-pearl-format.el
index 7310413..1e9fa53 100644
--- a/tests/test-pearl-format.el
+++ b/tests/test-pearl-format.el
@@ -56,7 +56,7 @@
"A full issue renders the heading and the namespaced LINEAR-* drawer."
(test-pearl--with-default-mapping
(let ((out (pearl--format-issue-as-org-entry (test-pearl--norm-full))))
- (should (string-match-p "^\\*\\* IN-PROGRESS \\[#B\\] Fix the thing$" out))
+ (should (string-match-p "^\\*\\* IN-PROGRESS \\[#B\\] ENG-42: Fix the Thing$" out))
(should (string-match-p "^:LINEAR-ID: +uuid-1$" out))
(should (string-match-p "^:LINEAR-IDENTIFIER: +ENG-42$" out))
(should (string-match-p "^:LINEAR-STATE-NAME: +In Progress$" out))
@@ -80,7 +80,7 @@
"Null/missing optional fields render as empty values, and the body is empty."
(test-pearl--with-default-mapping
(let ((out (pearl--format-issue-as-org-entry (test-pearl--norm-bare))))
- (should (string-match-p "^\\*\\* TODO \\[#C\\] Bare issue$" out))
+ (should (string-match-p "^\\*\\* TODO \\[#C\\] ENG-7: Bare Issue$" out))
(should (string-match-p "^:LINEAR-PROJECT-NAME: +$" out))
(should (string-match-p "^:LINEAR-ASSIGNEE-NAME: +$" out))
(should (string-match-p "^:LINEAR-LABELS: +\\[\\]$" out))
@@ -93,11 +93,12 @@
(let ((out (pearl--format-issue-as-org-entry
'(:id "u" :identifier "ENG-1" :title "Fix [URGENT] bug"
:priority 1 :state (:name "Todo")))))
- (should (string-match-p "^\\*\\* TODO \\[#A\\] Fix URGENT bug$" out))
- ;; the title provenance hash is of the stripped (rendered) title, so a
- ;; later no-op title sync matches the heading and never clobbers brackets
+ (should (string-match-p "^\\*\\* TODO \\[#A\\] ENG-1: Fix URGENT Bug$" out))
+ ;; the title provenance hash is of the displayed (stripped + cased) title
+ ;; without the identifier prefix, so a later no-op title sync matches the
+ ;; heading and never clobbers brackets or pushes the prefix
(should (string-match-p
- (format "^:LINEAR-TITLE-SHA256: +%s$" (secure-hash 'sha256 "Fix URGENT bug"))
+ (format "^:LINEAR-TITLE-SHA256: +%s$" (secure-hash 'sha256 "Fix URGENT Bug"))
out)))))
;;; build-org-content
@@ -147,8 +148,8 @@ sort together (org-sort on the parent) instead of being orphan headings."
(test-pearl--with-default-mapping
(let ((out (pearl--build-org-content
(list (test-pearl--norm-full) (test-pearl--norm-bare)))))
- (should (string-match-p "^\\*\\* IN-PROGRESS \\[#B\\] Fix the thing$" out))
- (should (string-match-p "^\\*\\* TODO \\[#C\\] Bare issue$" out)))))
+ (should (string-match-p "^\\*\\* IN-PROGRESS \\[#B\\] ENG-42: Fix the Thing$" out))
+ (should (string-match-p "^\\*\\* TODO \\[#C\\] ENG-7: Bare Issue$" out)))))
;;; --restore-page-visibility
diff --git a/tests/test-pearl-heading.el b/tests/test-pearl-heading.el
new file mode 100644
index 0000000..2675518
--- /dev/null
+++ b/tests/test-pearl-heading.el
@@ -0,0 +1,117 @@
+;;; test-pearl-heading.el --- Tests for heading title rendering -*- 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 heading title transforms: smart title case
+;; (`pearl--title-case'), the identifier prefix (`pearl--heading-with-identifier'
+;; / `pearl--strip-identifier-prefix'), and the way `--format-issue-as-org-entry'
+;; renders them while keeping `LINEAR-TITLE-SHA256' over the bare displayed
+;; title so an unedited heading is a no-op on title sync.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+
+;;; --title-case
+
+(ert-deftest test-pearl-title-case-capitalizes-significant-words ()
+ "Significant words are capitalized; minor words mid-title stay lowercase."
+ (should (string= "Fix the Refresh Bug" (pearl--title-case "fix the refresh bug")))
+ (should (string= "A Tale of Two Cities" (pearl--title-case "a tale of two cities"))))
+
+(ert-deftest test-pearl-title-case-edges-capitalize-minor-words ()
+ "A minor word that is first or last is still capitalized."
+ (should (string= "Of Mice and Men" (pearl--title-case "of mice and men")))
+ (should (string= "What Is It For" (pearl--title-case "what is it for"))))
+
+(ert-deftest test-pearl-title-case-preserves-existing-uppercase ()
+ "A word that already has an uppercase letter (acronym, identifier) is left as-is."
+ (should (string= "API Rate Limits" (pearl--title-case "API rate limits")))
+ (should (string= "GraphQL and You" (pearl--title-case "GraphQL and you"))))
+
+(ert-deftest test-pearl-title-case-boundaries ()
+ "Empty, single-word, and extra-whitespace inputs behave."
+ (should (string= "" (pearl--title-case "")))
+ (should (string= "Bug" (pearl--title-case "bug")))
+ (should (string= "Fix the Bug" (pearl--title-case "fix the bug"))))
+
+(ert-deftest test-pearl-title-case-leaves-inner-punctuation-alone ()
+ "Only the first letter is upcased, so an apostrophe or hyphen mid-word is intact."
+ (should (string= "Don't Panic" (pearl--title-case "don't panic")))
+ (should (string= "Re-run the Task" (pearl--title-case "re-run the task"))))
+
+;;; --heading-with-identifier / --strip-identifier-prefix
+
+(ert-deftest test-pearl-heading-identifier-prefix-roundtrips ()
+ "Adding then stripping the identifier prefix is the identity."
+ (let ((pearl-show-identifier-in-heading t))
+ (let ((h (pearl--heading-with-identifier "Fix the Bug" "SE-401")))
+ (should (string= "SE-401: Fix the Bug" h))
+ (should (string= "Fix the Bug" (pearl--strip-identifier-prefix h "SE-401"))))))
+
+(ert-deftest test-pearl-heading-identifier-prefix-disabled ()
+ "With prefixing off, the title is returned unchanged."
+ (let ((pearl-show-identifier-in-heading nil))
+ (should (string= "Fix the Bug"
+ (pearl--heading-with-identifier "Fix the Bug" "SE-401")))))
+
+(ert-deftest test-pearl-strip-identifier-prefix-empty-or-absent ()
+ "Stripping is a no-op when the identifier is empty or not at the front."
+ (should (string= "Fix the Bug" (pearl--strip-identifier-prefix "Fix the Bug" "")))
+ (should (string= "Fix the Bug" (pearl--strip-identifier-prefix "Fix the Bug" nil)))
+ ;; an identifier that only appears mid-title is not stripped
+ (should (string= "see SE-9: later"
+ (pearl--strip-identifier-prefix "see SE-9: later" "SE-9"))))
+
+;;; rendering on / off
+
+(ert-deftest test-pearl-format-heading-defaults-prefix-and-title-case ()
+ "By default the heading carries the identifier prefix and a title-cased title."
+ (let ((pearl-show-identifier-in-heading t)
+ (pearl-title-case-headings t))
+ (let ((out (pearl--format-issue-as-org-entry
+ '(:id "u" :identifier "SE-401" :title "fix the refresh bug"
+ :priority 2 :state (:name "Todo")))))
+ (should (string-match-p "^\\*\\* TODO \\[#B\\] SE-401: Fix the Refresh Bug$" out)))))
+
+(ert-deftest test-pearl-format-heading-both-toggles-off-renders-verbatim ()
+ "With both toggles off, the heading is the bracket-stripped raw title, no prefix."
+ (let ((pearl-show-identifier-in-heading nil)
+ (pearl-title-case-headings nil))
+ (let ((out (pearl--format-issue-as-org-entry
+ '(:id "u" :identifier "SE-401" :title "fix the refresh bug"
+ :priority 2 :state (:name "Todo")))))
+ (should (string-match-p "^\\*\\* TODO \\[#B\\] fix the refresh bug$" out)))))
+
+(ert-deftest test-pearl-format-title-hash-is-over-displayed-title-no-prefix ()
+ "The title hash is over the displayed (cased) title without the identifier prefix.
+That is what makes a fetch + unedited heading a no-op on title sync."
+ (let ((pearl-show-identifier-in-heading t)
+ (pearl-title-case-headings t))
+ (let ((out (pearl--format-issue-as-org-entry
+ '(:id "u" :identifier "SE-401" :title "fix the refresh bug"
+ :priority 2 :state (:name "Todo")))))
+ (should (string-match-p
+ (format "^:LINEAR-TITLE-SHA256: +%s$"
+ (secure-hash 'sha256 "Fix the Refresh Bug"))
+ out)))))
+
+(provide 'test-pearl-heading)
+;;; test-pearl-heading.el ends here
diff --git a/tests/test-pearl-org-write.el b/tests/test-pearl-org-write.el
index bd06ef0..6491b54 100644
--- a/tests/test-pearl-org-write.el
+++ b/tests/test-pearl-org-write.el
@@ -55,7 +55,7 @@ The state mapping is bound so rendering is deterministic."
(let ((content (with-temp-buffer (insert-file-contents tmp) (buffer-string))))
(should (string-match-p "#\\+title: Linear" content))
(should (string-match-p "#\\+LINEAR-SOURCE: " content))
- (should (string-match-p "\\*\\* TODO \\[#C\\] T" content)))))
+ (should (string-match-p "\\*\\* TODO \\[#C\\] ENG-1: T" content)))))
(ert-deftest test-pearl-update-org-clean-buffer-replaces-contents ()
"A clean visiting buffer is replaced in place and saved."
@@ -67,7 +67,7 @@ The state mapping is bound so rendering is deterministic."
(pearl--update-org-from-issues test-pearl--sample-issues)
(with-current-buffer buf
(should-not (buffer-modified-p))
- (should (string-match-p "\\*\\* TODO \\[#C\\] T" (buffer-string)))
+ (should (string-match-p "\\*\\* TODO \\[#C\\] ENG-1: T" (buffer-string)))
(should-not (string-match-p "old content" (buffer-string)))))))
(ert-deftest test-pearl-update-org-dirty-buffer-not-overwritten ()
diff --git a/tests/test-pearl-title-sync.el b/tests/test-pearl-title-sync.el
index 512794c..fc84a15 100644
--- a/tests/test-pearl-title-sync.el
+++ b/tests/test-pearl-title-sync.el
@@ -53,6 +53,29 @@
"*** TODO [#B] My issue title :tag:\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\n"
(should (string= "My issue title" (pearl--issue-title-at-point)))))
+(ert-deftest test-pearl-issue-title-strips-identifier-prefix ()
+ "The extractor strips the rendered `IDENT: ' prefix using the drawer identifier."
+ (test-pearl--in-org
+ "*** TODO [#B] SE-401: Fix the Bug\n:PROPERTIES:\n:LINEAR-ID: a\n:LINEAR-IDENTIFIER: SE-401\n:END:\n"
+ (should (string= "Fix the Bug" (pearl--issue-title-at-point)))))
+
+(ert-deftest test-pearl-title-render-read-roundtrip-is-noop ()
+ "Rendering an issue then reading its heading back hashes to the stored title hash.
+This is the property that keeps a fetch + unedited heading from pushing the
+title-cased / identifier-prefixed display form to Linear."
+ (let ((pearl-show-identifier-in-heading t)
+ (pearl-title-case-headings t))
+ (test-pearl--in-org
+ (pearl--format-issue-as-org-entry
+ '(:id "a" :identifier "SE-401" :title "fix the refresh bug"
+ :priority 2 :state (:name "Todo")))
+ (goto-char (point-min))
+ (re-search-forward "SE-401")
+ (let ((stored (org-entry-get nil "LINEAR-TITLE-SHA256"))
+ (read-back (pearl--issue-title-at-point)))
+ (should (string= "Fix the Refresh Bug" read-back))
+ (should (string= stored (secure-hash 'sha256 read-back)))))))
+
;;; network helpers
(ert-deftest test-pearl-fetch-issue-title-parses-payload ()