;;; test-pearl-comments.el --- Tests for issue comments -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings ;; Author: Craig Jennings ;; 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 . ;;; 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