From 7939a598bc1f2e045dd4d4fe9e5dc4b7437147d5 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 28 May 2026 10:15:54 -0500 Subject: fix(view-sync): refuse sync of a saved query whose filter is empty A saved query with no filter constraints compiled to nil via pearl--build-issue-filter. The sync command then passed (cons "filterData" nil) to customViewCreate, which JSON-encoded as filterData=null. Linear's CustomViewCreateInput.filterData expects a JSON object, so the API rejected with an opaque error message after the user had already clicked through the scope picker. I added an early refusal in pearl-sync-saved-query-to-linear: when filter-data is nil, signal a user-error naming what's wrong before the scope prompt fires. The message tells the user to add a constraint and re-sync rather than leaving them to puzzle out an opaque API rejection. I restructured the let* nesting to put the empty-filter check between the bindings (where filter-data is computed) and the scope prompt (where the user is asked to pick a destination). The scope binding moved out of the outer let* into an inner let so it doesn't evaluate until after the filter-data guard runs. Two tests cover the guard. The first-time-sync test (no :linear-view-id on the entry) asserts neither the scope prompt nor customViewCreate fires. The re-sync test (entry has :linear-view-id) asserts customViewUpdate doesn't fire either. The re-sync test guards against a future refactor accidentally hoisting the check into the create-only branch. All 663 ert tests pass. make compile and make lint are clean. --- tests/test-pearl-saved-query-sync.el | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'tests') diff --git a/tests/test-pearl-saved-query-sync.el b/tests/test-pearl-saved-query-sync.el index ceac5e6..8c98539 100644 --- a/tests/test-pearl-saved-query-sync.el +++ b/tests/test-pearl-saved-query-sync.el @@ -415,6 +415,26 @@ erased on every re-sync." (let ((shared-value (cdr (assoc "shared" captured-input)))) (should (eq :json-false shared-value)))))) +;;; pearl-sync-saved-query-to-linear — empty filter refusal + +(ert-deftest test-pearl-sync-empty-filter-refuses-before-scope-prompt () + "A saved query whose filter compiles to nil signals a clear user-error +before any scope prompt fires. Without the guard, the sync would pass +filterData=null to Linear, which rejects with an opaque error after the +user has already clicked through the scope picker." + (let ((pearl-saved-queries (copy-tree '(("EmptyFilter" :filter nil)))) + (scope-prompt-fired nil) + (api-called nil)) + (cl-letf (((symbol-function 'pearl--sync-saved-query-pick-scope) + (lambda (&rest _) (setq scope-prompt-fired t) + (list :team-id nil :team-name "Personal" :shared nil))) + ((symbol-function 'pearl--customview-create-async) + (lambda (&rest _) (setq api-called t)))) + (should-error (pearl-sync-saved-query-to-linear "EmptyFilter") + :type 'user-error) + (should-not scope-prompt-fired) + (should-not api-called)))) + ;;; pearl-publish-current-source (defmacro test-pearl-publish--in-buffer (source-form &rest body) -- cgit v1.2.3