diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-28 09:12:41 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-28 09:12:41 -0500 |
| commit | c8b9ad1b72c60f87c15e942f17f7ff1066a41cb2 (patch) | |
| tree | 714d360d2f0dc9e565f04ac67a7bd784650fd2bc /tests/test-pearl-saved-query-sync.el | |
| parent | 60a026be803e4fb2e56520dd9e2cc72ba6bfc0e3 (diff) | |
| download | pearl-c8b9ad1b72c60f87c15e942f17f7ff1066a41cb2.tar.gz pearl-c8b9ad1b72c60f87c15e942f17f7ff1066a41cb2.zip | |
feat(view-sync): extend pearl-delete-saved-query with delete-on-Linear prompt
Phase 3 of docs/saved-query-sync-spec.org. pearl-delete-saved-query on a synced entry (one carrying :linear-view-id) now asks a second question after the local-delete confirmation: also delete the linked Linear view? Yes calls customViewDelete, no unlinks only and leaves the Linear view in place. Local-only entries take the unchanged single-prompt path.
If customViewDelete fails on the API side, a fallback yes-or-no prompt asks whether to drop the local entry anyway. Accepting orphans the Linear view, and the success message names the view id explicitly so the user can clean it up by hand. The same view-id-in-message pattern fires on the timeout branch, since a timeout doesn't tell us whether the delete actually completed server-side.
I added pearl--customview-delete-async (the mutation, parallel to the create and update helpers from Phase 2) and pearl--delete-saved-query-local (factors the cl-remove + customize-save-variable that both the local-only and the synced-unlink branches need). The new pearl--delete-saved-query-do-linear-delete carries the API path so the top-level command stays readable.
I added 7 tests in test-pearl-saved-query-sync.el covering the customViewDelete success and failure parses, the synced yes/yes path (API call fires + local removed), the synced yes/no path (unlink only, no API call), the API-failure-then-delete-anyway path (orphan message names the view id), the API-failure-then-keep path (asserts the API call fired so a refactor can't silently route through the unlink branch), and the local-only entry unchanged path. The existing pearl-delete-saved-query tests in test-pearl-adhoc.el stay green. Their fixtures don't carry :linear-view-id, so they take the local-only branch. 638 tests total green. make compile and make lint clean.
Diffstat (limited to 'tests/test-pearl-saved-query-sync.el')
| -rw-r--r-- | tests/test-pearl-saved-query-sync.el | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/tests/test-pearl-saved-query-sync.el b/tests/test-pearl-saved-query-sync.el index 7a40566..ebf4622 100644 --- a/tests/test-pearl-saved-query-sync.el +++ b/tests/test-pearl-saved-query-sync.el @@ -375,5 +375,133 @@ team-key lookups." (should captured-query) (should (string-match-p "key" captured-query))))) +;;; pearl--customview-delete-async + +(ert-deftest test-pearl-customview-delete-async-success () + "A successful `customViewDelete' invokes the callback with `:success' t." + (testutil-linear-with-response + '((data (customViewDelete (success . t)))) + (let (result) + (pearl--customview-delete-async "view-uuid" (lambda (r) (setq result r))) + (should (eq t (plist-get result :success)))))) + +(ert-deftest test-pearl-customview-delete-async-failure () + "A non-success `customViewDelete' invokes the callback with `:success' nil." + (testutil-linear-with-response + '((data (customViewDelete (success . :json-false)))) + (let (result) + (pearl--customview-delete-async "view-uuid" (lambda (r) (setq result r))) + (should-not (plist-get result :success))))) + +;;; pearl-delete-saved-query — synced-entry second-stage prompt + +(defun test-pearl--make-yes-no-stub (answers) + "Return a `yes-or-no-p' replacement that returns each ANSWERS in order. +Each call pops the head; the test fails the call sequence assertion if +more prompts fire than answers were prepared for." + (let ((remaining answers)) + (lambda (&rest _) + (if remaining + (pop remaining) + (error "Unexpected extra yes-or-no-p prompt"))))) + +(ert-deftest test-pearl-delete-saved-query-synced-yes-yes-calls-customview-delete () + "Synced entry, both prompts yes: customViewDelete fires and the local entry is removed." + (let ((pearl-saved-queries + (copy-tree '(("Q" :filter (:open t) + :linear-view-id "view-uuid" + :linear-view-team-id nil + :linear-view-shared :json-false)))) + (delete-called-with nil)) + (cl-letf (((symbol-function 'yes-or-no-p) + (test-pearl--make-yes-no-stub '(t t))) + ((symbol-function 'customize-save-variable) (lambda (_ _) nil)) + ((symbol-function 'pearl--progress) (lambda (&rest _) nil)) + ((symbol-function 'message) (lambda (&rest _) nil)) + ((symbol-function 'pearl--customview-delete-async) + (lambda (vid cb) + (setq delete-called-with vid) + (funcall cb (list :success t))))) + (pearl-delete-saved-query "Q") + (should (equal "view-uuid" delete-called-with)) + (should-not (assoc "Q" pearl-saved-queries))))) + +(ert-deftest test-pearl-delete-saved-query-synced-yes-no-unlinks-only () + "Synced entry, first prompt yes second prompt no: local entry removed, customViewDelete not called." + (let ((pearl-saved-queries + (copy-tree '(("Q" :filter (:open t) + :linear-view-id "view-uuid")))) + (delete-called nil)) + (cl-letf (((symbol-function 'yes-or-no-p) + (test-pearl--make-yes-no-stub '(t nil))) + ((symbol-function 'customize-save-variable) (lambda (_ _) nil)) + ((symbol-function 'pearl--progress) (lambda (&rest _) nil)) + ((symbol-function 'message) (lambda (&rest _) nil)) + ((symbol-function 'pearl--customview-delete-async) + (lambda (_ _) (setq delete-called t)))) + (pearl-delete-saved-query "Q") + (should-not delete-called) + (should-not (assoc "Q" pearl-saved-queries))))) + +(ert-deftest test-pearl-delete-saved-query-synced-api-failure-then-delete-anyway () + "Synced entry, customViewDelete fails, user accepts delete-anyway prompt: local removed, message names the orphan id." + (let ((pearl-saved-queries + (copy-tree '(("Q" :filter (:open t) + :linear-view-id "ORPHAN-UUID")))) + (last-message nil)) + (cl-letf (((symbol-function 'yes-or-no-p) + (test-pearl--make-yes-no-stub '(t t t))) ; delete? + also-on-Linear? + delete-anyway? + ((symbol-function 'customize-save-variable) (lambda (_ _) nil)) + ((symbol-function 'pearl--progress) (lambda (&rest _) nil)) + ((symbol-function 'message) + (lambda (fmt &rest args) + (setq last-message (apply #'format fmt args)))) + ((symbol-function 'pearl--customview-delete-async) + (lambda (_ cb) + (funcall cb (list :success nil :error "permission denied"))))) + (pearl-delete-saved-query "Q") + (should-not (assoc "Q" pearl-saved-queries)) + (should (string-match-p "ORPHAN-UUID" last-message)) + (should (string-match-p "orphan" last-message))))) + +(ert-deftest test-pearl-delete-saved-query-synced-api-failure-then-keep () + "Synced entry, customViewDelete fails, user rejects delete-anyway: both kept untouched. +Verifies the API call actually fires (so the keep branch only runs when the +delete-anyway prompt explicitly declines), guarding against a future refactor +that silently routes to the unlink branch and skips the API call." + (let ((pearl-saved-queries + (copy-tree '(("Q" :filter (:open t) + :linear-view-id "view-uuid")))) + (delete-called-with nil)) + (cl-letf (((symbol-function 'yes-or-no-p) + (test-pearl--make-yes-no-stub '(t t nil))) ; yes / yes / no on delete-anyway + ((symbol-function 'customize-save-variable) (lambda (_ _) nil)) + ((symbol-function 'pearl--progress) (lambda (&rest _) nil)) + ((symbol-function 'message) (lambda (&rest _) nil)) + ((symbol-function 'pearl--customview-delete-async) + (lambda (vid cb) + (setq delete-called-with vid) + (funcall cb (list :success nil :error "permission denied"))))) + (pearl-delete-saved-query "Q") + (should (equal "view-uuid" delete-called-with)) + (should (assoc "Q" pearl-saved-queries))))) + +(ert-deftest test-pearl-delete-saved-query-local-only-entry-unchanged-behavior () + "Local-only entry (no `:linear-view-id'): one yes-or-no-p, no second prompt, no API call." + (let ((pearl-saved-queries + (copy-tree '(("Q" :filter (:open t))))) + (delete-called nil) + (prompt-count 0)) + (cl-letf (((symbol-function 'yes-or-no-p) + (lambda (&rest _) (cl-incf prompt-count) t)) + ((symbol-function 'customize-save-variable) (lambda (_ _) nil)) + ((symbol-function 'message) (lambda (&rest _) nil)) + ((symbol-function 'pearl--customview-delete-async) + (lambda (_ _) (setq delete-called t)))) + (pearl-delete-saved-query "Q") + (should (= 1 prompt-count)) + (should-not delete-called) + (should-not (assoc "Q" pearl-saved-queries))))) + (provide 'test-pearl-saved-query-sync) ;;; test-pearl-saved-query-sync.el ends here |
