From 617d3a13fa23b472879327366c6c557de9cb90f4 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 24 May 2026 18:46:58 -0500 Subject: feat: add the local dirty scanners for the ticket save model First layer of the unified save model (docs/ticket-save-model-spec.org). pearl--issue-dirty-fields returns which of title, description, and comment candidates changed in the issue subtree at point, with no network call. Description dirtiness reuses pearl--subtree-dirty-p, which hashes the rendered Org against LINEAR-DESC-ORG-SHA256, so a description whose markdown is lossy under org->md isn't falsely flagged. Comment candidates hash org->md of each body against LINEAR-COMMENT-SHA256, matching how pearl-edit-current-comment computes its no-op check. Ownership is a separate phase: pearl--classify-comment-candidates splits candidates into own versus read-only once the viewer id is known, since the local scan can't tell who authored a comment without it. The save-issue and save-all commands that consume these scanners land in the next layers. --- tests/test-pearl-save.el | 123 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/test-pearl-save.el (limited to 'tests/test-pearl-save.el') diff --git a/tests/test-pearl-save.el b/tests/test-pearl-save.el new file mode 100644 index 0000000..24db264 --- /dev/null +++ b/tests/test-pearl-save.el @@ -0,0 +1,123 @@ +;;; 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 -- cgit v1.2.3