From fc57dcac9a7c972fa59dbd0aded06ae0725000ca Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 28 May 2026 10:08:10 -0500 Subject: refactor(view-sync): pearl--save-query-mark-synced preserves unknown plist keys The prior implementation rebuilt the spec from a fixed key set (:filter, :sort, :order, plus the four :linear-view-* sync keys and the optional :linear-view-url) and silently dropped anything else. No keys outside that set live on a saved-query entry today, but the persistence layer was coupled to the current schema. A future :description, a user-added annotation, or any new pearl field would vanish on every re-sync. I switched to copy-then-plist-put: shallow-copy the existing spec, plist-put the four sync keys on top, and conditionally plist-put :linear-view-url when this call provided one. Arbitrary keys survive untouched. The URL-preservation behavior on a re-sync that omits view-url is unchanged: the prior :linear-view-url is already in the copied spec, so the when-view-url guard skips the put and leaves it in place. I added one regression test that seeds an entry with :description and :user-tag alongside the standard keys and asserts all of them survive a mark-synced call. All 661 ert tests pass. make compile and make lint are clean. --- pearl.el | 32 ++++++++++++++++---------------- tests/test-pearl-saved-query-sync.el | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/pearl.el b/pearl.el index 61c4934..39b7878 100644 --- a/pearl.el +++ b/pearl.el @@ -3820,22 +3820,22 @@ isn't in `pearl-saved-queries'." (let ((entry (assoc name pearl-saved-queries))) (unless entry (user-error "No saved query named %s" name)) - (let* ((spec (cdr entry)) - (filter (plist-get spec :filter)) - (sort (plist-get spec :sort)) - (order (plist-get spec :order)) - (existing-url (plist-get spec :linear-view-url)) - (effective-url (or view-url existing-url)) - (synced-at (format-time-string "%Y-%m-%dT%H:%M:%SZ" nil t)) - (new-spec (append (list :filter filter) - (when sort (list :sort sort)) - (when order (list :order order)) - (list :linear-view-id view-id - :linear-view-team-id team-id - :linear-view-shared shared - :linear-view-synced-at synced-at) - (when effective-url - (list :linear-view-url effective-url))))) + ;; Copy the existing spec and plist-put each sync key on top. Earlier + ;; versions rebuilt the spec from `:filter', `:sort', `:order' + the four + ;; sync keys, which silently dropped any other key on the entry -- a + ;; future `:description', a user-added annotation, or any new schema + ;; field would vanish on every re-sync. Copy-then-put preserves + ;; everything the user (or future pearl code) attached. + (let* ((synced-at (format-time-string "%Y-%m-%dT%H:%M:%SZ" nil t)) + (new-spec (copy-sequence (cdr entry)))) + (setq new-spec (plist-put new-spec :linear-view-id view-id)) + (setq new-spec (plist-put new-spec :linear-view-team-id team-id)) + (setq new-spec (plist-put new-spec :linear-view-shared shared)) + (setq new-spec (plist-put new-spec :linear-view-synced-at synced-at)) + ;; Only touch :linear-view-url when this call supplied one; a re-sync + ;; whose API response somehow lacks the URL must not erase the prior. + (when view-url + (setq new-spec (plist-put new-spec :linear-view-url view-url))) (setq pearl-saved-queries (cons (cons name new-spec) (assoc-delete-all name (copy-sequence pearl-saved-queries)))) diff --git a/tests/test-pearl-saved-query-sync.el b/tests/test-pearl-saved-query-sync.el index f9bab00..ceac5e6 100644 --- a/tests/test-pearl-saved-query-sync.el +++ b/tests/test-pearl-saved-query-sync.el @@ -267,6 +267,34 @@ URL the create path already wrote." (should (equal "https://linear.app/ws/view/old" (plist-get spec :linear-view-url)))))) +(ert-deftest test-pearl-save-query-mark-synced-preserves-unknown-plist-keys () + "Unknown keys on a saved-query spec survive a mark-synced call. +The earlier implementation rebuilt the spec from a fixed key set and +silently dropped anything outside it. Copy-then-plist-put preserves +arbitrary keys so a future schema field or user annotation isn't +erased on every re-sync." + (let ((pearl-saved-queries + (copy-tree '(("Q" :filter (:open t) + :sort updated + :order desc + :description "private note about this query" + :user-tag "experimental"))))) + (cl-letf (((symbol-function 'customize-save-variable) (lambda (_ _) nil))) + (pearl--save-query-mark-synced "Q" "team-uuid" t "view-uuid" "https://x")) + (let ((spec (cdr (assoc "Q" pearl-saved-queries)))) + ;; Original keys survive. + (should (equal '(:open t) (plist-get spec :filter))) + (should (eq 'updated (plist-get spec :sort))) + (should (eq 'desc (plist-get spec :order))) + (should (equal "private note about this query" + (plist-get spec :description))) + (should (equal "experimental" (plist-get spec :user-tag))) + ;; New sync keys layered on top. + (should (equal "view-uuid" (plist-get spec :linear-view-id))) + (should (equal "team-uuid" (plist-get spec :linear-view-team-id))) + (should (eq t (plist-get spec :linear-view-shared))) + (should (equal "https://x" (plist-get spec :linear-view-url)))))) + (ert-deftest test-pearl-save-query-mark-synced-overwrites-prior-sync-metadata () "Re-syncing an already-synced entry overwrites the four `:linear-view-*' keys." (let ((pearl-saved-queries -- cgit v1.2.3