diff options
Diffstat (limited to 'tests/test-pearl-org-parse.el')
| -rw-r--r-- | tests/test-pearl-org-parse.el | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/tests/test-pearl-org-parse.el b/tests/test-pearl-org-parse.el new file mode 100644 index 0000000..87f4a02 --- /dev/null +++ b/tests/test-pearl-org-parse.el @@ -0,0 +1,152 @@ +;;; test-pearl-org-parse.el --- Tests for pearl org parsing -*- 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 readers `pearl--extract-org-heading-properties' and +;; `pearl--process-heading-at-point'. The reader uses org APIs over the +;; LINEAR-* property drawer, so it works from anywhere in the entry, at any +;; heading depth, and is unbothered by body text or nested sub-entries. The one +;; network boundary reached during processing (`--update-issue-state-async') is +;; stubbed. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.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)) + ;; Declare the Linear keywords so `org-get-todo-state' recognizes them, the + ;; way the generated file's `#+TODO:' line does in real use. + `(let ((pearl-state-to-todo-mapping + '(("Todo" . "TODO") ("In Progress" . "IN-PROGRESS") ("Done" . "DONE"))) + (pearl-todo-states-pattern nil) + (pearl--todo-states-pattern-source nil) + (org-todo-keywords '((sequence "TODO" "IN-PROGRESS" "|" "DONE")))) + (with-temp-buffer + (insert ,content) + (org-mode) + (goto-char (point-min)) + ,@body))) + +;;; extract-org-heading-properties + +(ert-deftest test-pearl-extract-heading-properties-full () + "A complete Linear entry yields the todo keyword, ids, and team id." + (test-pearl--in-org + "*** TODO My issue\n:PROPERTIES:\n:LINEAR-ID: abc-123\n:LINEAR-IDENTIFIER: ENG-5\n:LINEAR-TEAM-ID: team-9\n:END:\n" + (re-search-forward "My issue") + (let ((props (pearl--extract-org-heading-properties))) + (should (string-equal "TODO" (plist-get props :todo-state))) + (should (string-equal "abc-123" (plist-get props :issue-id))) + (should (string-equal "ENG-5" (plist-get props :issue-identifier))) + (should (string-equal "team-9" (plist-get props :team-id)))))) + +(ert-deftest test-pearl-extract-reads-from-inside-the-entry () + "The reader works with point in the body, not only on the heading line." + (test-pearl--in-org + "*** TODO My issue\n:PROPERTIES:\n:LINEAR-ID: abc\n:END:\nsome body text here\n" + (goto-char (point-max)) + (should (string-equal "abc" (plist-get (pearl--extract-org-heading-properties) :issue-id))))) + +(ert-deftest test-pearl-extract-team-id-is-read-not-looked-up () + "The team id comes straight from LINEAR-TEAM-ID, with no network call." + (cl-letf (((symbol-function 'pearl--get-team-id-by-name) + (lambda (&rest _) (error "should not be called")))) + (test-pearl--in-org + "*** TODO x\n:PROPERTIES:\n:LINEAR-ID: i\n:LINEAR-TEAM-ID: t-1\n:END:\n" + (re-search-forward "x") + (should (string-equal "t-1" (plist-get (pearl--extract-org-heading-properties) :team-id)))))) + +(ert-deftest test-pearl-extract-missing-id () + "A drawer with no LINEAR-ID yields a nil issue-id." + (test-pearl--in-org + "*** TODO x\n:PROPERTIES:\n:LINEAR-IDENTIFIER: ENG-9\n:END:\n" + (re-search-forward "x") + (let ((props (pearl--extract-org-heading-properties))) + (should (string-equal "TODO" (plist-get props :todo-state))) + (should (null (plist-get props :issue-id))) + (should (string-equal "ENG-9" (plist-get props :issue-identifier)))))) + +(ert-deftest test-pearl-extract-deeper-heading-now-supported () + "A level-4 entry is read the same as level-3 (the reader is depth-agnostic)." + (test-pearl--in-org + "*** TODO parent\n**** TODO child\n:PROPERTIES:\n:LINEAR-ID: c\n:END:\n" + (re-search-forward "child") + (should (string-equal "c" (plist-get (pearl--extract-org-heading-properties) :issue-id))))) + +(ert-deftest test-pearl-extract-off-heading-nil () + "Before the first heading, nothing is extracted." + (test-pearl--in-org + "preamble line\n* Top\n" + (goto-char (point-min)) + (should (null (pearl--extract-org-heading-properties))))) + +;;; process-heading-at-point + +(ert-deftest test-pearl-process-heading-updates-when-complete () + "A complete entry triggers an async state update with the mapped state." + (let ((captured nil)) + (cl-letf (((symbol-function 'pearl--update-issue-state-async) + (lambda (id state team) (setq captured (list id state team))))) + (test-pearl--in-org + "*** IN-PROGRESS x\n:PROPERTIES:\n:LINEAR-ID: i-1\n:LINEAR-IDENTIFIER: ENG-2\n:LINEAR-TEAM-ID: t-1\n:END:\n" + (re-search-forward "x") + (pearl--process-heading-at-point) + (should (equal '("i-1" "In Progress" "t-1") captured)))))) + +(ert-deftest test-pearl-process-heading-skips-without-team () + "An entry missing its team id makes no API call." + (let ((called nil)) + (cl-letf (((symbol-function 'pearl--update-issue-state-async) + (lambda (&rest _) (setq called t)))) + (test-pearl--in-org + "*** TODO x\n:PROPERTIES:\n:LINEAR-ID: i-1\n:LINEAR-IDENTIFIER: ENG-2\n:END:\n" + (re-search-forward "x") + (pearl--process-heading-at-point) + (should-not called))))) + +;;; render -> parse round trip + +(ert-deftest test-pearl-render-parse-round-trip () + "An issue rendered by build-org-content parses back through the reader. +Locks the render/parse contract: the LINEAR-* drawer the renderer writes is +exactly what the reader extracts, and the rendered keyword is recognized." + (let ((pearl-state-to-todo-mapping '(("In Progress" . "IN-PROGRESS"))) + (pearl-todo-states-pattern nil) + (pearl--todo-states-pattern-source nil) + (org-todo-keywords '((sequence "TODO" "IN-PROGRESS" "|" "DONE")))) + (let ((content (pearl--build-org-content + '((:id "i-1" :identifier "ENG-2" :title "round trip" + :priority 2 :state (:name "In Progress") :team (:id "t-1")))))) + (with-temp-buffer + (insert content) + (org-mode) + (goto-char (point-min)) + (re-search-forward "round trip") + (let ((props (pearl--extract-org-heading-properties))) + (should (string-equal "i-1" (plist-get props :issue-id))) + (should (string-equal "ENG-2" (plist-get props :issue-identifier))) + (should (string-equal "IN-PROGRESS" (plist-get props :todo-state))) + (should (string-equal "t-1" (plist-get props :team-id)))))))) + +(provide 'test-pearl-org-parse) +;;; test-pearl-org-parse.el ends here |
