aboutsummaryrefslogtreecommitdiff
path: root/tests/test-pearl-bugfixes.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 07:33:16 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 07:33:16 -0500
commit291abb5d810d9910e80991881674439174bc56cf (patch)
treee7a2e289ae1f50682ca0587da76cb04b2fdc08f2 /tests/test-pearl-bugfixes.el
parent9b17dc89fc25417e207943dafe8bf09ac211ae32 (diff)
downloadpearl-291abb5d810d9910e80991881674439174bc56cf.tar.gz
pearl-291abb5d810d9910e80991881674439174bc56cf.zip
fix: seven quick bugs from the 2026-05-25 code review
A batch of small, independent fixes a code-review pass turned up, each its own :bug:quick:solo: todo. They share no code path beyond pearl.el, so the body lists them. Single-issue refresh false-stash: pearl-refresh-current-issue decided whether to stash "local edits" by hashing org->md against LINEAR-DESC-SHA256, the lossy markdown round-trip. A clean issue whose description has lossy markdown (a # heading, single-asterisk italics) looked dirty and got stashed before every refresh. It now uses pearl--subtree-dirty-p, the Org-hash-first check the merge refresh already uses. Comment outcome parent id: pearl--save-comment-field read :issue-id from the comment heading, which carries LINEAR-COMMENT-ID, not LINEAR-ID, so comment outcomes got :issue-id nil against the contract. A new pearl--issue-id-at-point climbs to the enclosing issue. save-all viewer-unavailable count: when the viewer lookup failed, save-all passed a nil viewer id and the prompt counted every dirty comment as read-only, disagreeing with the actual skipped/viewer-unavailable outcome. The counts and prompt now carry an explicit viewer-unavailable tally. Request-counter leak: pearl--graphql-request-async incremented pearl--active-requests before pearl--headers, which signals when the key is unset, so neither callback ran to decrement it. I build the headers before the increment. Issue commands from comment subtrees: save-issue, refresh, compose-description, open-issue, and the field setters read LINEAR-ID from the nearest heading, so running them from inside a comment hit the comment heading and rejected with "Not on a Linear issue heading". A new pearl--goto-issue-heading-or-error climbs to the issue heading; the commands use it. Project selector on no projects: pearl-select-project ran (string= selected "None") even when a team had no projects and selected was nil. I guard the nil case. (The reported wrong-type crash doesn't reproduce here, since string= treats nil as the symbol "nil", but the guard makes the optional-no-projects path explicit.) Query filter validation: pearl-run-saved-query, pearl-list-issues-filtered, and pearl-list-issues compiled their authoring plist without calling pearl--validate-issue-filter, so an unknown key in a saved query was silently ignored. They now validate at the command boundary, so typos surface as a clear user-error.
Diffstat (limited to 'tests/test-pearl-bugfixes.el')
-rw-r--r--tests/test-pearl-bugfixes.el155
1 files changed, 155 insertions, 0 deletions
diff --git a/tests/test-pearl-bugfixes.el b/tests/test-pearl-bugfixes.el
new file mode 100644
index 0000000..7f5c635
--- /dev/null
+++ b/tests/test-pearl-bugfixes.el
@@ -0,0 +1,155 @@
+;;; test-pearl-bugfixes.el --- Tests for the 2026-05-25 quick bug batch -*- 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 a batch of small bugs found in a 2026-05-25 code review of
+;; pearl.el (each was a :bug:quick:solo: todo): single-issue refresh
+;; false-stash, comment outcome parent-issue-id, save-all viewer-unavailable
+;; counting, request-counter leak on a missing key, issue commands climbing
+;; out of comment subtrees, the project selector on no projects, and
+;; command-path filter validation.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+(defmacro test-pearl-bug--in-org (content &rest body)
+ "Run BODY in an org-mode temp buffer holding CONTENT at point-min."
+ (declare (indent 1))
+ `(let ((org-todo-keywords '((sequence "TODO" "IN-PROGRESS" "|" "DONE")))
+ (pearl-state-to-todo-mapping '(("Todo" . "TODO")))
+ (org-element-use-cache nil))
+ (with-temp-buffer
+ (insert ,content)
+ (org-mode)
+ (goto-char (point-min))
+ ,@body)))
+
+;;; Bug: single-issue refresh falsely stashes clean lossy descriptions
+
+(ert-deftest test-pearl-refresh-clean-lossy-description-not-stashed ()
+ "A freshly rendered issue whose markdown is lossy under org->md is not stashed."
+ (test-pearl-bug--in-org
+ (pearl--format-issue-as-org-entry
+ '(:id "u" :identifier "ENG-1" :title "t" :priority 3
+ :state (:name "Todo") :description "# Heading\nbody text"))
+ (re-search-forward "body text")
+ (let ((stashed nil))
+ (cl-letf (((symbol-function 'pearl--stash-conflict-text)
+ (lambda (&rest _) (setq stashed t)))
+ ((symbol-function 'pearl--fetch-issue-async)
+ (lambda (&rest _) nil)))
+ (pearl-refresh-current-issue)
+ (should-not stashed)))))
+
+;;; Bug: comment save outcomes lose the parent issue id
+
+(ert-deftest test-pearl-issue-id-at-point-climbs-from-comment ()
+ "From inside a comment subtree, the enclosing issue's LINEAR-ID is found."
+ (test-pearl-bug--in-org
+ (concat "*** TODO Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n"
+ "**** Comments\n***** Me\n:PROPERTIES:\n:LINEAR-COMMENT-ID: c1\n:END:\nhi\n")
+ (re-search-forward "^\\*\\*\\*\\*\\* Me")
+ (should (string= "a" (pearl--issue-id-at-point)))))
+
+(ert-deftest test-pearl-save-comment-outcome-carries-issue-id ()
+ "A skipped comment outcome carries the enclosing issue id, not nil."
+ (test-pearl-bug--in-org
+ (concat "*** TODO Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n"
+ "**** Comments\n***** Other\n:PROPERTIES:\n:LINEAR-COMMENT-ID: c1\n"
+ ":LINEAR-COMMENT-AUTHOR-ID: u-other\n:END:\nhi\n")
+ (re-search-forward "^\\*\\*\\*\\*\\* Other")
+ (let (outcome)
+ ;; non-own comment -> skipped/read-only, no network; outcome should still
+ ;; name the parent issue.
+ (pearl--save-comment-field (point-marker) "u-me"
+ (lambda (o) (setq outcome o)))
+ (should (string= "a" (plist-get outcome :issue-id))))))
+
+;;; Bug: save-all prompt mislabels viewer-lookup failures as read-only
+
+(ert-deftest test-pearl-save-all-counts-viewer-failed-not-read-only ()
+ "With the viewer lookup failed, comment candidates count as viewer-unavailable."
+ (let* ((scan (list (cons (make-marker)
+ (list :title nil :description nil
+ :comment-candidates
+ '((:comment-id "c1" :author-id "u-me"))))))
+ (counts (pearl--save-all-counts scan nil t)))
+ (should (= 0 (plist-get counts :read-only-comments)))
+ (should (= 0 (plist-get counts :own-comments)))
+ (should (= 1 (plist-get counts :viewer-unavailable)))
+ (should (string-match-p "viewer unavailable" (pearl--save-all-prompt counts)))))
+
+;;; Bug: missing API key can leak the active request count
+
+(ert-deftest test-pearl-request-missing-key-does-not-leak-counter ()
+ "A missing key signals before dispatch and leaves the request counter intact."
+ (let ((pearl-api-key nil)
+ (pearl--active-requests 0))
+ (should-error (pearl--graphql-request-async "query { viewer { id } }"))
+ (should (= 0 pearl--active-requests))))
+
+;;; Bug: issue-level commands do not climb out of comment subtrees
+
+(ert-deftest test-pearl-goto-issue-heading-climbs-from-comment ()
+ "The issue-heading guard climbs from a comment to the enclosing issue heading."
+ (test-pearl-bug--in-org
+ (concat "*** TODO Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n"
+ "**** Comments\n***** Me\n:PROPERTIES:\n:LINEAR-COMMENT-ID: c1\n:END:\nhi\n")
+ (re-search-forward "^\\*\\*\\*\\*\\* Me")
+ (pearl--goto-issue-heading-or-error)
+ (should (string= "a" (org-entry-get nil "LINEAR-ID")))))
+
+(ert-deftest test-pearl-goto-issue-heading-errors-off-an-issue ()
+ "With no enclosing issue heading, the guard signals a user-error."
+ (test-pearl-bug--in-org "* Plain heading\nno linear id\n"
+ (goto-char (point-max))
+ (should-error (pearl--goto-issue-heading-or-error) :type 'user-error)))
+
+;;; Bug: project selector errors when a team has no projects
+
+(ert-deftest test-pearl-select-project-no-projects-returns-nil ()
+ "With no projects (or a failed fetch), the selector returns nil without erroring."
+ (cl-letf (((symbol-function 'pearl-get-projects) (lambda (_team) nil)))
+ (should (null (pearl-select-project "team-1")))))
+
+;;; Bug: saved/ad-hoc query filters bypass validation
+
+(ert-deftest test-pearl-run-saved-query-validates-before-fetch ()
+ "A saved query with an unknown filter key errors before any fetch."
+ (let ((pearl-saved-queries '(("bad" :filter (:bogus-key "x"))))
+ (fetched nil))
+ (cl-letf (((symbol-function 'pearl--query-issues-async)
+ (lambda (&rest _) (setq fetched t))))
+ (should-error (pearl-run-saved-query "bad") :type 'user-error)
+ (should-not fetched))))
+
+(ert-deftest test-pearl-run-saved-query-validates-bad-priority ()
+ "A saved query with an out-of-range priority errors before any fetch."
+ (let ((pearl-saved-queries '(("bad" :filter (:priority 9))))
+ (fetched nil))
+ (cl-letf (((symbol-function 'pearl--query-issues-async)
+ (lambda (&rest _) (setq fetched t))))
+ (should-error (pearl-run-saved-query "bad") :type 'user-error)
+ (should-not fetched))))
+
+(provide 'test-pearl-bugfixes)
+;;; test-pearl-bugfixes.el ends here