From 8fa0febbc3a3afe78d1a5abbbb944adf6dd3612a Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 25 May 2026 19:54:32 -0500 Subject: feat(save): add an atomic conflict gate for enum and relation fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The text conflict gate (pearl--resolve-conflict) is built for mergeable text — it can open an smerge buffer, stash to the kill ring, and advance a SHA hash. None of that fits a state, priority, assignee, or label conflict, where the value is a single id or scalar that can't be combined. pearl--resolve-atomic-conflict is the sibling for those fields: it prompts use-local / use-remote / cancel (no merge option), and delegates the actual push and the baseline advance to the caller's two callbacks, so it stays free of any field-specific mechanics. It maps to the same outcome contract as the text gate — cancel to conflict/cancelled, use-local to pushed or failed/push-failed, use-remote to resolved-remote. It's not wired into a saver yet — the per-field savers in the next commit call it. --- tests/test-pearl-conflict.el | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) (limited to 'tests') diff --git a/tests/test-pearl-conflict.el b/tests/test-pearl-conflict.el index 138e622..56d3c84 100644 --- a/tests/test-pearl-conflict.el +++ b/tests/test-pearl-conflict.el @@ -224,6 +224,62 @@ (should (string= (secure-hash 'sha256 "merged text") (org-entry-get marker "LINEAR-DESC-SHA256")))))))) +;;; --resolve-atomic-conflict (enum/relation fields, no merge) + +(ert-deftest test-pearl-resolve-atomic-conflict-cancel () + "Cancel pushes nothing, takes nothing, and reports conflict/cancelled." + (let (pushed took outcome) + (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution) + (lambda (_) 'cancel))) + (pearl--resolve-atomic-conflict + "ENG-1 state" "In Progress" "Done" + (lambda (cb) (setq pushed t) (funcall cb t)) + (lambda () (setq took t)) + (lambda (status reason) (setq outcome (list status reason)))) + (should-not pushed) + (should-not took) + (should (equal '(conflict cancelled) outcome))))) + +(ert-deftest test-pearl-resolve-atomic-conflict-use-local-pushes () + "Use-local runs the push; on success reports pushed." + (let (pushed took outcome) + (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution) + (lambda (_) 'use-local))) + (pearl--resolve-atomic-conflict + "ENG-1 state" "In Progress" "Done" + (lambda (cb) (setq pushed t) (funcall cb t)) + (lambda () (setq took t)) + (lambda (status reason) (setq outcome (list status reason)))) + (should pushed) + (should-not took) + (should (equal '(pushed nil) outcome))))) + +(ert-deftest test-pearl-resolve-atomic-conflict-use-local-push-failure () + "A failed local push reports failed/push-failed." + (let (outcome) + (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution) + (lambda (_) 'use-local))) + (pearl--resolve-atomic-conflict + "ENG-1 state" "In Progress" "Done" + (lambda (cb) (funcall cb nil)) + (lambda () nil) + (lambda (status reason) (setq outcome (list status reason)))) + (should (equal '(failed push-failed) outcome))))) + +(ert-deftest test-pearl-resolve-atomic-conflict-use-remote-takes-no-push () + "Use-remote adopts Linear's value, never pushes, and reports resolved-remote." + (let (pushed took outcome) + (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution) + (lambda (_) 'use-remote))) + (pearl--resolve-atomic-conflict + "ENG-1 state" "In Progress" "Done" + (lambda (cb) (setq pushed t) (funcall cb t)) + (lambda () (setq took t)) + (lambda (status reason) (setq outcome (list status reason)))) + (should took) + (should-not pushed) + (should (equal '(resolved-remote nil) outcome))))) + ;;; --conflict-has-markers-p (ert-deftest test-pearl-conflict-has-markers-p () -- cgit v1.2.3