aboutsummaryrefslogtreecommitdiff
path: root/tests/test-pearl-format.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 13:44:34 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 13:44:34 -0500
commitb081d62276378b3168c92c06153fd59db0589535 (patch)
tree9be7f7d22e0c9b4a73432fe744c09bb456c671a9 /tests/test-pearl-format.el
downloadpearl-b081d62276378b3168c92c06153fd59db0589535.tar.gz
pearl-b081d62276378b3168c92c06153fd59db0589535.zip
feat: pearl — manage Linear issues from org-mode
Pearl fetches Linear issues into an org file and syncs edits back. It covers list / custom views / saved queries, per-issue and bulk rendering with comments inline, conflict-aware sync of descriptions, titles, and comments, field commands for priority / state / assignee / labels, and a transient dispatch menu. The render folds to a scannable outline and nests issues under a sortable parent. Based on and inspired by Gael Blanchemain's linear-emacs.
Diffstat (limited to 'tests/test-pearl-format.el')
-rw-r--r--tests/test-pearl-format.el188
1 files changed, 188 insertions, 0 deletions
diff --git a/tests/test-pearl-format.el b/tests/test-pearl-format.el
new file mode 100644
index 0000000..7310413
--- /dev/null
+++ b/tests/test-pearl-format.el
@@ -0,0 +1,188 @@
+;;; test-pearl-format.el --- Tests for org entry 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 org renderer: `pearl--format-issue-as-org-entry' (a
+;; normalized issue plist -> heading + LINEAR-* drawer + body description),
+;; `pearl--description-to-org-body' (the interim heading guard), and
+;; `pearl--build-org-content'. Issues come from the shared fixtures via
+;; `pearl--normalize-issue', so the renderer is exercised on the same
+;; shapes production hands it.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'testutil-fixtures (expand-file-name "testutil-fixtures.el"))
+
+(defmacro test-pearl--with-default-mapping (&rest body)
+ "Run BODY with the default state mapping and a clean pattern cache."
+ (declare (indent 0))
+ `(let ((pearl-state-to-todo-mapping
+ '(("Todo" . "TODO") ("In Progress" . "IN-PROGRESS")
+ ("In Review" . "IN-REVIEW") ("Backlog" . "BACKLOG")
+ ("Blocked" . "BLOCKED") ("Done" . "DONE")))
+ (pearl-todo-states-pattern nil)
+ (pearl--todo-states-pattern-source nil))
+ ,@body))
+
+(defun test-pearl--norm-full ()
+ "A normalized fully-populated issue."
+ (pearl--normalize-issue (testutil-linear-fixture-issue-full)))
+
+(defun test-pearl--norm-bare ()
+ "A normalized issue with null/missing optional fields."
+ (pearl--normalize-issue (testutil-linear-fixture-issue-null-fields)))
+
+;;; format-issue-as-org-entry
+
+(ert-deftest test-pearl-format-issue-full-renders-heading-and-drawer ()
+ "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 "^:LINEAR-ID: +uuid-1$" out))
+ (should (string-match-p "^:LINEAR-IDENTIFIER: +ENG-42$" out))
+ (should (string-match-p "^:LINEAR-STATE-NAME: +In Progress$" out))
+ (should (string-match-p "^:LINEAR-TEAM-NAME: +Engineering$" out))
+ (should (string-match-p "^:LINEAR-PROJECT-NAME: +Platform$" out))
+ (should (string-match-p "^:LINEAR-ASSIGNEE-NAME: +Craig$" out))
+ (should (string-match-p "^:LINEAR-LABELS: +\\[bug, backend\\]$" out))
+ (should (string-match-p "^:LINEAR-DESC-SHA256: +[0-9a-f]\\{64\\}$" out))
+ (should (string-match-p "^:LINEAR-TITLE-SHA256: +[0-9a-f]\\{64\\}$" out))
+ (should (string-match-p "^:END:$" out)))))
+
+(ert-deftest test-pearl-format-issue-description-in-body-not-property ()
+ "The description renders as body text, not a :DESCRIPTION: property."
+ (test-pearl--with-default-mapping
+ (let ((out (pearl--format-issue-as-org-entry (test-pearl--norm-full))))
+ (should-not (string-match-p ":DESCRIPTION:" out))
+ (should (string-match-p "Line one" out))
+ (should (string-match-p "Line two" out)))))
+
+(ert-deftest test-pearl-format-issue-bare-empty-optionals ()
+ "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 "^:LINEAR-PROJECT-NAME: +$" out))
+ (should (string-match-p "^:LINEAR-ASSIGNEE-NAME: +$" out))
+ (should (string-match-p "^:LINEAR-LABELS: +\\[\\]$" out))
+ ;; null description -> nothing after :END:
+ (should (string-match-p ":END:\n\\'" out)))))
+
+(ert-deftest test-pearl-format-issue-strips-brackets-from-title ()
+ "Square brackets in the title are stripped so org parsing stays sane."
+ (test-pearl--with-default-mapping
+ (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
+ (format "^:LINEAR-TITLE-SHA256: +%s$" (secure-hash 'sha256 "Fix URGENT bug"))
+ out)))))
+
+;;; build-org-content
+
+(ert-deftest test-pearl-build-org-content-empty-issues-header-only ()
+ "With no issues the content is the file header plus the empty parent, no entries."
+ (test-pearl--with-default-mapping
+ (let ((out (pearl--build-org-content '())))
+ (should (string-match-p "^#\\+title:" out))
+ (should-not (string-match-p "^\\*\\* " out)))))
+
+(ert-deftest test-pearl-build-org-content-no-hardcoded-filetags ()
+ "The header carries no hardcoded =#+filetags= (a personal value used to leak in)."
+ (test-pearl--with-default-mapping
+ (let ((out (pearl--build-org-content '())))
+ (should-not (string-match-p "#\\+filetags" out))
+ (should-not (string-match-p "twai" out)))))
+
+(ert-deftest test-pearl-build-org-content-renders-view-parent-heading ()
+ "Issues nest under a single top-level heading named after the view, so they
+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))
+ '(:type filter :name "My open issues" :filter nil))))
+ (should (string-match-p "^\\* My open issues$" out))
+ ;; the parent precedes the issue, which renders one level deeper
+ (should (< (string-match "^\\* My open issues$" out)
+ (string-match "^\\*\\* IN-PROGRESS" out))))))
+
+(ert-deftest test-pearl-build-org-content-startup-show3levels ()
+ "The page opens folded to headings (parent, issues, Comments), bodies hidden."
+ (test-pearl--with-default-mapping
+ (let ((out (pearl--build-org-content '())))
+ (should (string-match-p "^#\\+STARTUP: show3levels$" out))
+ (should-not (string-match-p "^#\\+STARTUP: overview$" out)))))
+
+(ert-deftest test-pearl-build-org-content-no-shared-file-id ()
+ "The file header carries no hardcoded org :ID: drawer."
+ (test-pearl--with-default-mapping
+ (let ((out (pearl--build-org-content '())))
+ (should-not (string-match-p "a12acb12" out))
+ (should-not (string-match-p "^:PROPERTIES:$" out)))))
+
+(ert-deftest test-pearl-build-org-content-includes-each-issue ()
+ "Each issue contributes one heading to the rendered content."
+ (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)))))
+
+;;; --restore-page-visibility
+
+(defun test-pearl--line-visible-p (re)
+ "Non-nil when the line matching RE from point-min is not folded away."
+ (save-excursion
+ (goto-char (point-min))
+ (and (re-search-forward re nil t)
+ (not (invisible-p (line-beginning-position))))))
+
+(ert-deftest test-pearl-restore-page-visibility-folds-bodies-keeps-headings ()
+ "After a repopulation the page folds to headings: parent and issues stay
+visible while property drawers fold away."
+ (test-pearl--with-default-mapping
+ (let ((pearl-fold-after-update t))
+ (with-temp-buffer
+ (insert (pearl--build-org-content (list (test-pearl--norm-full))))
+ (org-mode)
+ (org-fold-show-all)
+ (pearl--restore-page-visibility)
+ (should (test-pearl--line-visible-p "^\\* "))
+ (should (test-pearl--line-visible-p "^\\*\\* IN-PROGRESS"))
+ (should-not (test-pearl--line-visible-p "^:LINEAR-ID:"))))))
+
+(ert-deftest test-pearl-restore-page-visibility-noop-when-disabled ()
+ "With `pearl-fold-after-update' nil the buffer is left fully expanded."
+ (test-pearl--with-default-mapping
+ (let ((pearl-fold-after-update nil))
+ (with-temp-buffer
+ (insert (pearl--build-org-content (list (test-pearl--norm-full))))
+ (org-mode)
+ (org-fold-show-all)
+ (pearl--restore-page-visibility)
+ (should (test-pearl--line-visible-p "^:LINEAR-ID:"))))))
+
+(provide 'test-pearl-format)
+;;; test-pearl-format.el ends here