;;; test-pearl-save.el --- Tests for the unified ticket save 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 unified ticket save model (see ;; docs/ticket-save-model-spec.org). Phase 1: the local dirty scanners ;; (`pearl--issue-dirty-fields', `pearl--changed-comment-candidates') and the ;; viewer-based ownership classifier (`pearl--classify-comment-candidates'). ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (defun test-pearl-save--issue () "A normalized issue with a description and one own comment (author-id u-craig)." '(:id "u" :identifier "ENG-1" :title "fix the bug" :priority 2 :state (:name "Todo") :description "Original description." :comments ((:id "c1" :author "Craig" :author-id "u-craig" :created-at "2026-05-24T00:00:00.000Z" :body "my comment")))) (defmacro test-pearl-save--in-rendered (issue &rest body) "Render ISSUE into an org buffer, then run BODY with point at the start." (declare (indent 1)) `(let ((pearl-state-to-todo-mapping '(("Todo" . "TODO"))) (pearl-comment-sort-order 'newest-first)) (with-temp-buffer (insert (pearl--format-issue-as-org-entry ,issue)) (org-mode) (goto-char (point-min)) ,@body))) ;;; --issue-dirty-fields (Phase A, local) (ert-deftest test-pearl-dirty-fields-clean-issue () "A freshly rendered issue has no dirty fields and no comment candidates." (test-pearl-save--in-rendered (test-pearl-save--issue) (re-search-forward "Original description") (let ((d (pearl--issue-dirty-fields))) (should-not (plist-get d :title)) (should-not (plist-get d :description)) (should-not (plist-get d :comment-candidates))))) (ert-deftest test-pearl-dirty-fields-title-edit () "Editing the heading title (past the identifier prefix) marks the title dirty." (test-pearl-save--in-rendered (test-pearl-save--issue) (re-search-forward "Fix the Bug") (org-back-to-heading t) (org-edit-headline "ENG-1: Fix the Cache Bug") (let ((d (pearl--issue-dirty-fields))) (should (plist-get d :title)) (should-not (plist-get d :description))))) (ert-deftest test-pearl-dirty-fields-description-edit () "Editing the body marks the description dirty, not the title." (test-pearl-save--in-rendered (test-pearl-save--issue) (re-search-forward "Original description\\.") (end-of-line) (insert " EDITED") (let ((d (pearl--issue-dirty-fields))) (should (plist-get d :description)) (should-not (plist-get d :title))))) (ert-deftest test-pearl-dirty-fields-lossy-description-stays-clean () "A description whose markdown is lossy under org->md is not falsely dirty (HP1)." (test-pearl-save--in-rendered '(:id "u" :identifier "ENG-2" :title "t" :priority 3 :state (:name "Todo") :description "# Heading\nbody text") (re-search-forward "body text") (should-not (plist-get (pearl--issue-dirty-fields) :description)))) (ert-deftest test-pearl-dirty-fields-comment-candidate () "An edited comment surfaces as a candidate carrying its id and author-id." (test-pearl-save--in-rendered (test-pearl-save--issue) ;; edit the comment body (re-search-forward "my comment") (end-of-line) (insert " edited") ;; scan from inside the issue body (not the comment) (goto-char (point-min)) (re-search-forward "Original description") (let ((cands (plist-get (pearl--issue-dirty-fields) :comment-candidates))) (should (= 1 (length cands))) (should (string= "c1" (plist-get (car cands) :comment-id))) (should (string= "u-craig" (plist-get (car cands) :author-id)))))) (ert-deftest test-pearl-dirty-fields-unedited-comment-no-candidate () "An unedited comment is not a candidate." (test-pearl-save--in-rendered (test-pearl-save--issue) (re-search-forward "Original description") (should-not (plist-get (pearl--issue-dirty-fields) :comment-candidates)))) ;;; --classify-comment-candidates (Phase B, by viewer id) (ert-deftest test-pearl-classify-comment-candidates () "Candidates split into own (author = viewer) and read-only (everyone else)." (let* ((cands '((:comment-id "c1" :author-id "u-craig") (:comment-id "c2" :author-id "u-other") (:comment-id "c3" :author-id ""))) ; bot/external: not own (r (pearl--classify-comment-candidates cands "u-craig"))) (should (equal '("c1") (mapcar (lambda (c) (plist-get c :comment-id)) (plist-get r :own)))) (should (equal '("c2" "c3") (mapcar (lambda (c) (plist-get c :comment-id)) (plist-get r :read-only)))))) (provide 'test-pearl-save) ;;; test-pearl-save.el ends here