aboutsummaryrefslogtreecommitdiff
path: root/tests/test-pearl-comments.el
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-pearl-comments.el')
-rw-r--r--tests/test-pearl-comments.el170
1 files changed, 170 insertions, 0 deletions
diff --git a/tests/test-pearl-comments.el b/tests/test-pearl-comments.el
new file mode 100644
index 0000000..7fa4482
--- /dev/null
+++ b/tests/test-pearl-comments.el
@@ -0,0 +1,170 @@
+;;; test-pearl-comments.el --- Tests for issue comments -*- 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 comment thread: rendering a normalized comment and the
+;; oldest-first Comments subtree, including comments in the issue render, the
+;; commentCreate helper (stubbed at the HTTP boundary), the in-place append
+;; under the Comments subtree (creating it when absent), and the
+;; `pearl-add-comment' command.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'testutil-request (expand-file-name "testutil-request.el"))
+(require 'cl-lib)
+
+(defmacro test-pearl--in-org (content &rest body)
+ "Run BODY in an org-mode temp buffer holding CONTENT, default mapping bound."
+ (declare (indent 1))
+ `(let ((pearl-state-to-todo-mapping
+ '(("Todo" . "TODO") ("In Progress" . "IN-PROGRESS") ("Done" . "DONE")))
+ (org-todo-keywords '((sequence "TODO" "IN-PROGRESS" "|" "DONE"))))
+ (with-temp-buffer
+ (insert ,content)
+ (org-mode)
+ (goto-char (point-min))
+ ,@body)))
+
+;;; --format-comment / --format-comments
+
+(ert-deftest test-pearl-format-comment-renders-author-time-body ()
+ "A comment renders as a level-4 heading with author and timestamp, body below."
+ (let ((out (pearl--format-comment
+ '(:id "c1" :author "Craig" :created-at "2026-05-23T10:00:00.000Z"
+ :body "Looks **good** to me"))))
+ (should (string-match-p "^\\*\\*\\*\\* Craig — 2026-05-23T10:00:00.000Z$" out))
+ ;; body runs through the md->org tier
+ (should (string-match-p "Looks \\*good\\* to me" out))))
+
+(ert-deftest test-pearl-format-comment-null-author ()
+ "A comment with no resolved author renders a placeholder, not an error."
+ (let ((out (pearl--format-comment
+ '(:id "c1" :author nil :created-at "2026-05-23T10:00:00.000Z" :body "hi"))))
+ (should (string-match-p "^\\*\\*\\*\\* (unknown) — 2026-05-23T10:00:00.000Z$" out))))
+
+(ert-deftest test-pearl-format-comments-empty-is-blank ()
+ "No comments renders nothing (no empty Comments subtree)."
+ (should (string= "" (pearl--format-comments nil))))
+
+(ert-deftest test-pearl-format-comments-oldest-first ()
+ "Comments render under a Comments heading, oldest first regardless of input order."
+ (let ((out (pearl--format-comments
+ '((:id "c2" :author "B" :created-at "2026-05-23T12:00:00.000Z" :body "second")
+ (:id "c1" :author "A" :created-at "2026-05-23T09:00:00.000Z" :body "first")))))
+ (should (string-match-p "^\\*\\*\\* Comments$" out))
+ (should (< (string-match "first" out) (string-match "second" out)))))
+
+;;; comments in the issue render
+
+(ert-deftest test-pearl-format-issue-includes-comments ()
+ "A normalized issue carrying comments renders the Comments subtree after the body."
+ (test-pearl--in-org ""
+ (let ((out (pearl--format-issue-as-org-entry
+ '(:id "u" :identifier "ENG-1" :title "Title" :priority 2
+ :state (:name "Todo") :description "Body text."
+ :comments ((:id "c1" :author "A" :created-at "2026-05-23T09:00:00.000Z"
+ :body "a comment"))))))
+ (should (string-match-p "^\\*\\*\\* Comments$" out))
+ (should (< (string-match "Body text." out) (string-match "a comment" out))))))
+
+;;; --create-comment-async
+
+(ert-deftest test-pearl-create-comment-parses-payload ()
+ "A successful commentCreate yields the normalized comment."
+ (testutil-linear-with-response
+ '((data (commentCreate
+ (success . t)
+ (comment (id . "c9") (body . "new one")
+ (createdAt . "2026-05-23T13:00:00.000Z")
+ (user (name . "Craig"))))))
+ (let (result)
+ (pearl--create-comment-async "issue-a" "new one" (lambda (r) (setq result r)))
+ (should (string= "c9" (plist-get result :id)))
+ (should (string= "Craig" (plist-get result :author))))))
+
+(ert-deftest test-pearl-create-comment-soft-fail ()
+ "A non-success commentCreate yields nil rather than erroring."
+ (testutil-linear-with-response
+ '((data (commentCreate (success . :json-false) (comment . nil))))
+ (let ((called nil) (result 'untouched))
+ (pearl--create-comment-async "issue-a" "x" (lambda (r) (setq called t result r)))
+ (should called)
+ (should (null result)))))
+
+;;; --append-comment-to-issue
+
+(ert-deftest test-pearl-append-comment-creates-subtree ()
+ "Appending to an issue with no Comments subtree creates one."
+ (test-pearl--in-org
+ "*** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n"
+ (pearl--append-comment-to-issue
+ '(:id "c1" :author "A" :created-at "2026-05-23T09:00:00.000Z" :body "first comment"))
+ (goto-char (point-min))
+ (should (re-search-forward "^\\*\\*\\* Comments$" nil t))
+ (should (re-search-forward "first comment" nil t))))
+
+(ert-deftest test-pearl-append-comment-after-existing ()
+ "A new comment appends after an existing one under the Comments subtree."
+ (test-pearl--in-org
+ "** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n*** Comments\n**** A — 2026-05-23T09:00:00.000Z\nfirst\n"
+ (pearl--append-comment-to-issue
+ '(:id "c2" :author "B" :created-at "2026-05-23T12:00:00.000Z" :body "second"))
+ (goto-char (point-min))
+ ;; only one Comments heading, and the new comment follows the first
+ (should (re-search-forward "^\\*\\*\\* Comments$" nil t))
+ (should-not (re-search-forward "^\\*\\*\\* Comments$" nil t))
+ (goto-char (point-min))
+ (should (< (progn (re-search-forward "first") (point))
+ (progn (re-search-forward "second") (point))))))
+
+;;; pearl-add-comment
+
+(ert-deftest test-pearl-add-comment-appends-returned-comment ()
+ "The command creates a comment and inserts the returned one in the buffer."
+ (test-pearl--in-org
+ "*** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n"
+ (cl-letf (((symbol-function 'pearl--create-comment-async)
+ (lambda (_id body cb)
+ (funcall cb (list :id "c1" :author "Craig"
+ :created-at "2026-05-23T14:00:00.000Z" :body body)))))
+ (re-search-forward "Body.")
+ (pearl-add-comment "my new comment")
+ (goto-char (point-min))
+ (should (re-search-forward "^\\*\\*\\* Comments$" nil t))
+ (should (re-search-forward "my new comment" nil t)))))
+
+(ert-deftest test-pearl-add-comment-reports-failure ()
+ "A failed create does not insert a Comments subtree."
+ (test-pearl--in-org
+ "*** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n"
+ (cl-letf (((symbol-function 'pearl--create-comment-async)
+ (lambda (_id _body cb) (funcall cb nil))))
+ (pearl-add-comment "x")
+ (goto-char (point-min))
+ (should-not (re-search-forward "^\\*\\*\\* Comments$" nil t)))))
+
+(ert-deftest test-pearl-add-comment-not-on-issue-errors ()
+ "Adding a comment outside a Linear issue heading signals a user error."
+ (test-pearl--in-org "* Plain heading\nno id\n"
+ (should-error (pearl-add-comment "x") :type 'user-error)))
+
+(provide 'test-pearl-comments)
+;;; test-pearl-comments.el ends here