;;; test-pearl-org-parse.el --- Tests for pearl org parsing -*- 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 org reader `pearl--extract-org-heading-properties'. It 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. ;;; 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." (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 ((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 (the org-sync per-heading state push) was removed ;; in save-model-v2; the extractor above is retained for the keyword-cycle save. ;;; 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 ((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