;;; test-pearl-output.el --- Tests for the active-file output model -*- 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 active-file output model: the filter summary, the ;; source-tracking header (with the affordance preamble) written by ;; `--build-org-content', reading the active source back from a buffer, and ;; `pearl-refresh-current-view' re-running the recorded source. ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (require 'cl-lib) ;;; --summarize-filter (ert-deftest test-pearl-summarize-filter-fields () "A filter plist summarizes its set dimensions in a readable string." (let ((s (pearl--summarize-filter '(:assignee :me :open t :state "In Progress")))) (should (string-match-p "assignee" s)) (should (string-match-p "open" s)) (should (string-match-p "In Progress" s)))) (ert-deftest test-pearl-summarize-filter-empty () "An empty filter summarizes as all issues." (should (string-match-p "all" (pearl--summarize-filter nil)))) ;;; --build-org-content with a source (ert-deftest test-pearl-build-org-content-source-header () "With a source, the header carries the title, serialized source, and count." (let* ((source '(:type filter :name "My open issues" :filter (:assignee :me :open t))) (out (pearl--build-org-content '() source))) (should (string-match-p "^#\\+title: Linear — My open issues$" out)) (should (string-match-p "^#\\+LINEAR-SOURCE: " out)) (should (string-match-p "^#\\+LINEAR-COUNT: 0$" out)) ;; affordance preamble is present as org comments, not content (should (string-match-p "^# .*pearl-sync-current-issue" out)))) (ert-deftest test-pearl-build-org-content-source-roundtrips () "The serialized source in the header reads back to the original plist." (let* ((source '(:type filter :name "Bugs" :filter (:labels ("bug") :open t))) (out (pearl--build-org-content '() source))) (with-temp-buffer (insert out) (should (equal source (pearl--read-active-source)))))) (ert-deftest test-pearl-build-org-content-default-source-back-compat () "Called with no source, the content still has a title and no entries." (let ((out (pearl--build-org-content '()))) (should (string-match-p "^#\\+title:" out)) (should-not (string-match-p "^\\*\\*\\* " out)))) ;;; --read-active-source (ert-deftest test-pearl-read-active-source-absent () "A buffer with no source header reads back nil." (with-temp-buffer (insert "#+title: something\n\n* a heading\n") (should-not (pearl--read-active-source)))) ;;; refresh-current-view (ert-deftest test-pearl-refresh-current-view-reruns-source () "Refresh reads the recorded filter source and merges the re-run result." (let ((ran nil) (merged-source nil) (source '(:type filter :name "My open issues" :filter (:assignee :me :open t)))) (with-temp-buffer (insert (format "#+title: Linear — My open issues\n#+LINEAR-SOURCE: %s\n\n" (prin1-to-string source))) (org-mode) (cl-letf (((symbol-function 'pearl--query-issues-async) (lambda (_filter cb) (setq ran t) (funcall cb (pearl--make-query-result 'ok :issues nil)))) ((symbol-function 'pearl--merge-query-result) (lambda (_result src) (setq merged-source src)))) (pearl-refresh-current-view) (should ran) (should (equal source merged-source)))))) (ert-deftest test-pearl-refresh-current-view-no-source-errors () "Refresh with no recorded source signals a user error." (with-temp-buffer (insert "#+title: plain\n") (org-mode) (should-error (pearl-refresh-current-view) :type 'user-error))) ;;; --update-org-from-issues surfaces the result (ert-deftest test-pearl-update-org-surfaces-fresh-buffer () "With no buffer visiting the file, the write creates one and surfaces it." (let* ((tmp (make-temp-file "pearl-out" nil ".org")) (pearl-org-file-path tmp) (surfaced nil)) (unwind-protect (progn (when (find-buffer-visiting tmp) (kill-buffer (find-buffer-visiting tmp))) (cl-letf (((symbol-function 'pearl--surface-buffer) (lambda (b) (setq surfaced b)))) (pearl--update-org-from-issues '() '(:type filter :name "X" :filter nil) nil)) (should (bufferp surfaced)) (should (buffer-live-p surfaced)) (should (string= (file-truename tmp) (file-truename (buffer-file-name surfaced))))) (when (find-buffer-visiting tmp) (kill-buffer (find-buffer-visiting tmp))) (ignore-errors (delete-file tmp))))) (ert-deftest test-pearl-update-org-surfaces-existing-buffer () "With a clean buffer visiting the file, the update surfaces that buffer." (let* ((tmp (make-temp-file "pearl-out" nil ".org")) (pearl-org-file-path tmp) (surfaced nil) (buf (find-file-noselect tmp))) (unwind-protect (progn (with-current-buffer buf (set-buffer-modified-p nil)) (cl-letf (((symbol-function 'pearl--surface-buffer) (lambda (b) (setq surfaced b)))) (pearl--update-org-from-issues '() '(:type filter :name "X" :filter nil) nil)) (should (eq surfaced buf))) (when (buffer-live-p buf) (with-current-buffer buf (set-buffer-modified-p nil)) (kill-buffer buf)) (ignore-errors (delete-file tmp))))) (provide 'test-pearl-output) ;;; test-pearl-output.el ends here