From 0975e7de0c7bfdc6f65bb64537d4eff1aeb09a7e Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 28 May 2026 09:36:06 -0500 Subject: feat(view-sync): pearl-publish-current-source convenience wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is Phase 5 of docs/saved-query-sync-spec.org (§ New: pearl-publish-current-source). When I'm reading a local saved query rendered in pearl and decide it should be a Linear view, I now have a one-chord publish instead of the M-x round-trip through pearl-sync-saved-query-to-linear with the name lookup. The new command reads the active buffer's #+LINEAR-SOURCE header and dispatches to pearl-sync-saved-query-to-linear with the name pre-filled when the source is a :type filter that matches an entry in pearl-saved-queries. Other shapes refuse with a clear message: no source header at all, the source is already a :type view (no publish needed), the type isn't filter, the source has no usable name (transient ad-hoc filter, or whitespace-only name), or the recorded name doesn't match any local saved query. I bound the command under C-; L f P in pearl-fetch-map. The transient gets ("U" "upload current source") in the Fetch group because P is taken in the transient's flat namespace, and the verb-prefix shape revisit task tracks reshaping that. 7 new tests cover each refusal branch and the dispatch path, including a regression for the whitespace-only :name case the pre-commit review surfaced. The prior check used string-empty-p, which let " " fall through to the no-matching-saved-query branch with a visibly-blank name in the error message. string-blank-p routes it to the cleaner no-name branch. The keymap and transient menu tests gained coverage for the new binding. 660 ert tests total green. make compile and make lint clean. --- tests/test-pearl-keymap.el | 4 ++- tests/test-pearl-menu.el | 1 + tests/test-pearl-saved-query-sync.el | 65 ++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/test-pearl-keymap.el b/tests/test-pearl-keymap.el index b1b5e23..1ae0d3d 100644 --- a/tests/test-pearl-keymap.el +++ b/tests/test-pearl-keymap.el @@ -74,7 +74,9 @@ The lowercase `c' is no longer a direct command -- it is the create prefix." (should (eq 'pearl-run-view (lookup-key pearl-prefix-map (kbd "f v")))) (should (eq 'pearl-run-saved-query (lookup-key pearl-prefix-map (kbd "f q")))) (should (eq 'pearl-sync-saved-query-to-linear - (lookup-key pearl-prefix-map (kbd "f S"))))) + (lookup-key pearl-prefix-map (kbd "f S")))) + (should (eq 'pearl-publish-current-source + (lookup-key pearl-prefix-map (kbd "f P"))))) (ert-deftest test-pearl-prefix-map-edit-group () "The edit group resolves each field-edit command." diff --git a/tests/test-pearl-menu.el b/tests/test-pearl-menu.el index c25c9a9..e2ea550 100644 --- a/tests/test-pearl-menu.el +++ b/tests/test-pearl-menu.el @@ -63,6 +63,7 @@ menu entry that still points at it fails here." pearl-run-view pearl-run-saved-query pearl-sync-saved-query-to-linear + pearl-publish-current-source pearl-save-issue pearl-save-all pearl-edit-state diff --git a/tests/test-pearl-saved-query-sync.el b/tests/test-pearl-saved-query-sync.el index 830f738..f9bab00 100644 --- a/tests/test-pearl-saved-query-sync.el +++ b/tests/test-pearl-saved-query-sync.el @@ -387,6 +387,71 @@ URL the create path already wrote." (let ((shared-value (cdr (assoc "shared" captured-input)))) (should (eq :json-false shared-value)))))) +;;; pearl-publish-current-source + +(defmacro test-pearl-publish--in-buffer (source-form &rest body) + "Run BODY in an org-mode temp buffer carrying SOURCE-FORM as #+LINEAR-SOURCE. +SOURCE-FORM is the source plist (or nil for no source header)." + (declare (indent 1)) + `(with-temp-buffer + (when ,source-form + (insert (format "#+title: test\n#+LINEAR-SOURCE: %s\n\n" + (prin1-to-string ,source-form)))) + (org-mode) + ,@body)) + +(ert-deftest test-pearl-publish-current-source-no-source-header-errors () + "A buffer with no #+LINEAR-SOURCE signals a clear user-error." + (test-pearl-publish--in-buffer nil + (should-error (pearl-publish-current-source) :type 'user-error))) + +(ert-deftest test-pearl-publish-current-source-view-source-errors () + "A view source signals user-error -- views are already on Linear." + (test-pearl-publish--in-buffer '(:type view :name "Eng dashboard" :id "v1") + (should-error (pearl-publish-current-source) :type 'user-error))) + +(ert-deftest test-pearl-publish-current-source-transient-filter-no-name-errors () + "A :type filter with no :name (transient ad-hoc) signals user-error." + (test-pearl-publish--in-buffer '(:type filter :filter (:open t)) + (should-error (pearl-publish-current-source) :type 'user-error))) + +(ert-deftest test-pearl-publish-current-source-no-matching-saved-query-errors () + "A :type filter source whose name doesn't match any pearl-saved-queries entry errors." + (let ((pearl-saved-queries '(("Other" :filter (:open t))))) + (test-pearl-publish--in-buffer + '(:type filter :name "Gone" :filter (:open t)) + (should-error (pearl-publish-current-source) :type 'user-error)))) + +(ert-deftest test-pearl-publish-current-source-dispatches-to-sync-with-name () + "A :type filter source whose name matches dispatches to pearl-sync-saved-query-to-linear." + (let ((pearl-saved-queries '(("Mine" :filter (:open t)))) + (sync-called-with nil)) + (cl-letf (((symbol-function 'pearl-sync-saved-query-to-linear) + (lambda (n) (setq sync-called-with n)))) + (test-pearl-publish--in-buffer + '(:type filter :name "Mine" :filter (:open t)) + (pearl-publish-current-source)) + (should (equal "Mine" sync-called-with))))) + +(ert-deftest test-pearl-publish-current-source-empty-name-errors () + "A :type filter source with an empty-string :name (degenerate) errors cleanly." + (test-pearl-publish--in-buffer + '(:type filter :name "" :filter (:open t)) + (should-error (pearl-publish-current-source) :type 'user-error))) + +(ert-deftest test-pearl-publish-current-source-whitespace-name-errors () + "A :type filter source with a whitespace-only :name takes the no-name branch +rather than falling through to the no-matching-saved-query branch with a +visibly-blank name in the error message." + (let ((pearl-saved-queries '())) + (test-pearl-publish--in-buffer + '(:type filter :name " " :filter (:open t)) + (let ((err (should-error (pearl-publish-current-source) + :type 'user-error))) + ;; The message should name the source-has-no-name reason, not the + ;; no-matching-query reason. + (should (string-match-p "no name" (error-message-string err))))))) + ;;; Cache-shape parity — pearl-get-teams-async and pearl--all-teams must agree (ert-deftest test-pearl-get-teams-async-query-fetches-key () -- cgit v1.2.3