diff options
| -rw-r--r-- | pearl.el | 34 | ||||
| -rw-r--r-- | tests/test-pearl-save.el | 26 |
2 files changed, 46 insertions, 14 deletions
@@ -3246,6 +3246,15 @@ is nil or a symbol detailing a non-success outcome (`fetch-failed', :label (plist-get spec :label) :message (pearl--save-outcome-detail (plist-get spec :label) status reason))) +(defun pearl--marker-live-p (marker) + "Non-nil if MARKER is nil, or points into a still-live buffer. +A nil MARKER (synthetic callers with no buffer) passes; a marker into a killed +buffer fails. Guards async commit callbacks that touch the buffer after a push +the buffer may not have survived." + (or (null marker) + (let ((buffer (marker-buffer marker))) + (and buffer (buffer-live-p buffer))))) + (defun pearl--save-field-push (spec text callback) "Push TEXT for SPEC, advancing the provenance hash on success. CALLBACK gets a `pushed' outcome after any :after-push bookkeeping runs, or a @@ -3255,10 +3264,14 @@ CALLBACK gets a `pushed' outcome after any :after-push bookkeeping runs, or a (lambda (result) (if (plist-get result :success) (progn - (org-entry-put marker (plist-get spec :prop) - (secure-hash 'sha256 text)) - (when (plist-get spec :after-push) - (funcall (plist-get spec :after-push) result marker)) + ;; The push can land after the buffer was killed; only run + ;; the buffer-touching bookkeeping when it's still live, but + ;; still report the remote success. + (when (pearl--marker-live-p marker) + (org-entry-put marker (plist-get spec :prop) + (secure-hash 'sha256 text)) + (when (plist-get spec :after-push) + (funcall (plist-get spec :after-push) result marker))) (funcall callback (pearl--save-outcome spec 'pushed nil))) (funcall callback (pearl--save-outcome spec 'failed 'push-failed))))))) @@ -3330,7 +3343,9 @@ used by the local dirty scan), so the next scan sees the ticket as clean." (lambda (outcome) ;; A push or use-remote leaves the buffer body current; resync the Org ;; hash from it so the lossless local scan agrees with the markdown one. - (when (memq (plist-get outcome :status) '(pushed resolved-remote)) + ;; Skip when the buffer was killed before this async callback fired. + (when (and (memq (plist-get outcome :status) '(pushed resolved-remote)) + (pearl--marker-live-p marker)) (org-with-point-at marker (org-entry-put marker "LINEAR-DESC-ORG-SHA256" (secure-hash 'sha256 (pearl--entry-body-at-point))))) @@ -3447,15 +3462,6 @@ Surface BUFFER when any field pushed. The shared close-out for (message "%s: %s" label (pearl--save-summary-message outcomes)) (when pushed (pearl--surface-buffer buffer)))) -(defun pearl--marker-live-p (marker) - "Non-nil if MARKER is nil, or points into a still-live buffer. -A nil MARKER (synthetic callers with no buffer) passes; a marker into a killed -buffer fails. Guards async commit callbacks that touch the buffer after a push -the buffer may not have survived." - (or (null marker) - (let ((buffer (marker-buffer marker))) - (and buffer (buffer-live-p buffer))))) - (defun pearl--run-atomic-field-save (spec callback) "Save one atomic (enum/relation) field per SPEC, calling CALLBACK once. SPEC keys: :issue-id :identifier :field :marker :label; :live and :baseline diff --git a/tests/test-pearl-save.el b/tests/test-pearl-save.el index 8050e6d..1f4f273 100644 --- a/tests/test-pearl-save.el +++ b/tests/test-pearl-save.el @@ -288,6 +288,32 @@ the conflict choice (stubbed). Returns (:outcome :fetched :pushed :applied (should (equal '("remote changed") (plist-get r :applied))) (should (string= (secure-hash 'sha256 "remote changed") (plist-get r :hash))))) +(ert-deftest test-pearl-save-field-push-killed-buffer-no-signal () + "A text-field push whose buffer is killed before the success callback does not signal. +The hash-advance and :after-push touch the marker; on a dead buffer they must be +skipped, with the remote push still reported as pushed." + (let ((buf (generate-new-buffer "pearl-field-push-kill")) outcome after-ran) + (unwind-protect + (let (marker) + (with-current-buffer buf + (insert "* h\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\n") + (org-mode) + (goto-char (point-min)) + (setq marker (point-marker))) + (pearl--save-field-push + (list :issue-id "u" :identifier "ENG-1" :field 'title :marker marker + :prop "LINEAR-TITLE-SHA256" :label "ENG-1 title" + ;; the push kills the buffer, then reports success + :push (lambda (_text cb) (kill-buffer buf) (funcall cb '(:success t))) + ;; touching the dead marker would signal without the guard + :after-push (lambda (_r m) (setq after-ran t) (org-entry-put m "X" "y"))) + "new text" + (lambda (o) (setq outcome o))) + (should-not (buffer-live-p buf)) + (should (eq 'pushed (plist-get outcome :status))) + (should-not after-ran)) + (when (buffer-live-p buf) (kill-buffer buf))))) + ;;; --run-atomic-field-save (the per-field engine for enum/relation fields) (cl-defun test-pearl-save--atomic (&key live baseline remote (push-success t) resolution) |
