From c2ba1d8f25463bf6342ac635850500a9738b31e9 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 1 Jun 2026 17:33:09 -0500 Subject: refactor: rename saved-query surface to local-view vocabulary I renamed Pearl's "saved query" surface to the local/Linear view vocabulary, with no obsolete aliases, since Pearl has no users to protect. The defcustom pearl-saved-queries is now pearl-local-views. The commands pearl-run-saved-query, pearl-delete-saved-query, pearl-sync-saved-query-to-linear, pearl-run-view, and pearl-publish-current-source become pearl-run-local-view, pearl-delete-local-view, pearl-publish-local-view, pearl-run-linear-view, and pearl-publish-current-view. User-facing prompts, messages, and docstrings drop "saved query" for "local view", and the publish command reads "publish" instead of "sync". Internal GraphQL and helper names keep their "query" terms, which a user never sees. A naming-regression test asserts the new symbols exist, the old ones are gone rather than aliased, and no public command exposes "query". Phase 1 of docs/local-and-linear-views-spec.org. No behavior change. Suite, compile, and lint are green. --- pearl.el | 228 +++++++++++++++++------------------ tests/test-integration-acceptance.el | 18 +-- tests/test-pearl-accounts.el | 16 +-- tests/test-pearl-adhoc.el | 54 ++++----- tests/test-pearl-bugfixes.el | 12 +- tests/test-pearl-favorites.el | 8 +- tests/test-pearl-keymap.el | 10 +- tests/test-pearl-menu.el | 8 +- tests/test-pearl-naming.el | 74 ++++++++++++ tests/test-pearl-resolve.el | 2 +- tests/test-pearl-saved-query-sync.el | 110 ++++++++--------- tests/test-pearl-saved.el | 16 +-- tests/test-pearl-views.el | 4 +- 13 files changed, 317 insertions(+), 243 deletions(-) create mode 100644 tests/test-pearl-naming.el diff --git a/pearl.el b/pearl.el index 2ef4fae..de803d5 100644 --- a/pearl.el +++ b/pearl.el @@ -27,7 +27,7 @@ ;; pearl integrates Linear.app issue tracking with Emacs and org-mode. ;; Fetch your issues -- open issues, a project, an ad-hoc filter, a Linear -;; Custom View, or a named saved query -- into a single self-describing org +;; Custom View, or a named local view -- into a single self-describing org ;; file: each issue is a heading, its description and comments render in the ;; body, and its structured fields live in a namespaced LINEAR-* drawer. ;; @@ -1067,7 +1067,7 @@ dashboard / other)." "Fetch the viewer's favorites and call CALLBACK with a list of normalized favorite plists (Linear sort-order preserved). Paged via first/after; on a transport/GraphQL error, CALLBACK receives whatever was collected so far so the -picker can fall back to local saved queries rather than hanging." +picker can fall back to local views rather than hanging." (let (acc) (cl-labels ((collect (data) @@ -1087,8 +1087,8 @@ picker can fall back to local saved queries rather than hanging." pearl--favorites-query nil #'collect (lambda (&rest _) ;; HP5: a fetch error is not "no favorites" -- message it so the - ;; picker's fallback to saved queries doesn't masquerade as success. - (message "Pearl: favorites fetch failed; showing saved queries only") + ;; picker's fallback to local views doesn't masquerade as success. + (message "Pearl: favorites fetch failed; showing local views only") (funcall callback (nreverse acc))))))) (defun pearl--normalize-comment (raw) @@ -3539,8 +3539,8 @@ just writing the file and leaving it off-screen." (file-name-nondirectory org-file-path)) (pearl--surface-buffer existing-buf))))))) -(defcustom pearl-saved-queries nil - "Named local issue queries, run with `pearl-run-saved-query'. +(defcustom pearl-local-views nil + "Named local views, run with `pearl-run-local-view'. Each entry is (NAME . SPEC) where SPEC is a plist with `:filter' (an authoring filter plist), and optional `:sort' (`updated', `created', `priority', or `title') and `:order' (`asc' or `desc', default `desc'). AND-only in v1; use a @@ -3575,9 +3575,9 @@ produces the same heading order rather than reshuffling into noise." ;;; Sentinel infrastructure for filter / pick-one prompts ;; ;; These sentinel constants and helpers back the prompt UX shared by -;; `pearl-run-saved-query', `pearl-delete-saved-query', +;; `pearl-run-local-view', `pearl-delete-local-view', ;; `pearl-list-issues-filtered', `pearl-pick-source', and -;; `pearl-sync-saved-query-to-linear'. Defined ahead of those commands so +;; `pearl-publish-local-view'. Defined ahead of those commands so ;; the byte-compiler resolves the references in order. (defconst pearl--filter-any "[ Any. ]" @@ -3590,7 +3590,7 @@ than a hidden empty-input idiom.") (defconst pearl--filter-cancel "[ Cancel. ]" "Sentinel candidate shown at the top of pick-an-existing-thing prompts -(delete a saved query, run a saved query) where the opt-out reads as +(delete a local view, run a local view) where the opt-out reads as \"don't do this\" rather than \"no constraint.\" Cancelling these prompts is a real choice, not a missing one.") @@ -3638,25 +3638,25 @@ prompts (delete, overwrite) where typing \"yes\" is a safety affordance." (string= "yes" choice))) ;;;###autoload -(defun pearl-run-saved-query (name) - "Run the saved query NAME from `pearl-saved-queries'. +(defun pearl-run-local-view (name) + "Run the local view NAME from `pearl-local-views'. Interactively, completes over the configured query names. Compiles the stored filter, fetches, sorts per the query's `:sort'/`:order', and renders into the active file with the query recorded as the source." (interactive - (list (if (null pearl-saved-queries) - (user-error "No saved queries configured") + (list (if (null pearl-local-views) + (user-error "No local views configured") (completing-read - "Saved query: " + "Local view: " (pearl--completion-table-keep-order (pearl--with-sentinel pearl--filter-cancel - (mapcar #'car pearl-saved-queries))) + (mapcar #'car pearl-local-views))) nil t)))) (when (pearl--filter-sentinel-value-p name) - (user-error "Cancelled; no saved query run")) - (let ((entry (assoc name pearl-saved-queries))) + (user-error "Cancelled; no local view run")) + (let ((entry (assoc name pearl-local-views))) (unless entry - (user-error "No saved query named %s" name)) + (user-error "No local view named %s" name)) (let* ((spec (cdr entry)) (filter-plist (plist-get spec :filter)) (sort (plist-get spec :sort)) @@ -3671,19 +3671,19 @@ active file with the query recorded as the source." (when (and pearl-accounts q-account) (let ((active (plist-get (pearl--current-account-context) :name))) (unless (string-equal q-account active) - (user-error "Saved query '%s' belongs to %s; switch accounts first" + (user-error "Local view '%s' belongs to %s; switch accounts first" name q-account))))) - ;; Validate the saved query's authoring plist at the command boundary so + ;; Validate the local view's authoring plist at the command boundary so ;; a typo'd key or bad value surfaces as a clear user-error instead of ;; being silently dropped or failing deeper in the compiler. (pearl--validate-issue-filter filter-plist) - (pearl--progress "Running saved query %s..." name) + (pearl--progress "Running local view %s..." name) (pearl--query-issues-async (pearl--build-issue-filter filter-plist) (lambda (result) (pearl--render-query-result result source)) (pearl--sort->order-by sort))))) -;;; pearl-pick-source -- the unified source picker (favorites + saved queries) +;;; pearl-pick-source -- the unified source picker (favorites + local views) (defun pearl--saved-query-scope-label (spec teams) "Return the scope display label for SPEC (a saved-query plist). @@ -3707,15 +3707,15 @@ user rather than silently mis-labelling)." (defun pearl--pick-source-candidates (favorites saved-queries &optional teams) "Build the picker's candidate alist `((DISPLAY . SOURCE-OR-ACTION) ...)'. FAVORITES is a list of normalized favorite plists; SAVED-QUERIES is the -`pearl-saved-queries' alist; TEAMS is the team alist list (see +`pearl-local-views' alist; TEAMS is the team alist list (see `pearl--all-teams') used to resolve the scope name of any saved-query entry carrying `:linear-view-id'. Favorites come first in ascending -`:sort-order'; saved queries follow alphabetically. +`:sort-order'; local views follow alphabetically. Each DISPLAY is one of: `[KIND] TITLE' for favorites - `[saved] NAME' for a local-only saved query - `[saved \\u2192 SCOPE] NAME' for a synced saved query + `[saved] NAME' for a local-only local view + `[saved \\u2192 SCOPE] NAME' for a synced local view where SCOPE is the team name resolved from `:linear-view-team-id' against TEAMS, or \"Personal\" when there's no team scope, or \"?\" when the team id is stale (no longer in TEAMS) or TEAMS is nil. @@ -3790,19 +3790,19 @@ on success so the user sees what happened." ;;;###autoload (defun pearl-pick-source () - "Pick a Linear favorite or local saved query and fetch it into the active file. -Favorites are listed first in Linear's sort order, then local saved queries + "Pick a Linear favorite or local view and fetch it into the active file. +Favorites are listed first in Linear's sort order, then local views alphabetically. Non-list favorites (issue, document, ...) open in the browser instead. On a favorites-fetch failure the picker still offers any local saved queries -- favorites-failure is not the same user state as \"no favorites\". -With neither favorites nor saved queries, signals a `user-error' naming the +With neither favorites nor local views, signals a `user-error' naming the missing setup." (interactive) (pearl--progress "Fetching favorites...") - (let ((saved pearl-saved-queries)) + (let ((saved pearl-local-views)) (pearl--favorites-async (lambda (favorites) - ;; Only fetch teams when at least one saved query is synced -- scope + ;; Only fetch teams when at least one local view is synced -- scope ;; resolution is the only thing teams data feeds, and an all-local ;; configuration has no scopes to resolve. (let* ((teams (and (cl-some (lambda (sq) @@ -3812,7 +3812,7 @@ missing setup." (cands (pearl--pick-source-candidates favorites saved teams))) (unless cands (user-error - "No favorites or saved queries -- star views in Linear, or set `pearl-saved-queries'")) + "No favorites or local views -- star views in Linear, or set `pearl-local-views'")) (let* ((display (completing-read "Source: " (mapcar #'car cands) nil t)) (source (cdr (assoc display cands)))) (cond @@ -3922,16 +3922,16 @@ an id), or `pearl--filter-any' (any assignee, no scoping)." open state project labels assignee))) (defun pearl--delete-saved-query-local (name) - "Remove NAME from `pearl-saved-queries' and persist via Customize. + "Remove NAME from `pearl-local-views' and persist via Customize. The unconditional persistence path shared by the local-only delete and the synced-entry unlink branch." - (setq pearl-saved-queries - (cl-remove name pearl-saved-queries :key #'car :test #'string=)) - (customize-save-variable 'pearl-saved-queries pearl-saved-queries)) + (setq pearl-local-views + (cl-remove name pearl-local-views :key #'car :test #'string=)) + (customize-save-variable 'pearl-local-views pearl-local-views)) ;;;###autoload -(defun pearl-delete-saved-query (name) - "Delete the saved query NAME from `pearl-saved-queries' after confirmation. +(defun pearl-delete-local-view (name) + "Delete the local view NAME from `pearl-local-views' after confirmation. Interactively, completes over the configured query names with `pearl--filter-cancel' at the top. Local-only entries are removed from the customized list and persisted via `customize-save-variable'; entries @@ -3943,29 +3943,29 @@ entry but leaves the Linear view in place; picking YES calls entry anyway (which leaves the Linear view orphaned -- the message names the id so it can be deleted by hand)." (interactive - (list (if (null pearl-saved-queries) - (user-error "No saved queries to delete") + (list (if (null pearl-local-views) + (user-error "No local views to delete") (completing-read - "Delete saved query: " + "Delete local view: " (pearl--completion-table-keep-order (pearl--with-sentinel pearl--filter-cancel - (mapcar #'car pearl-saved-queries))) + (mapcar #'car pearl-local-views))) nil t)))) (cond ((pearl--filter-sentinel-value-p name) - (message "Cancelled; no saved query deleted")) - ((not (assoc name pearl-saved-queries)) - (user-error "No saved query named %s" name)) - ((not (yes-or-no-p (format "Delete saved query %s? " name))) - (message "Kept saved query %s" name)) + (message "Cancelled; no local view deleted")) + ((not (assoc name pearl-local-views)) + (user-error "No local view named %s" name)) + ((not (yes-or-no-p (format "Delete local view %s? " name))) + (message "Kept local view %s" name)) (t - (let ((view-id (plist-get (cdr (assoc name pearl-saved-queries)) + (let ((view-id (plist-get (cdr (assoc name pearl-local-views)) :linear-view-id))) (cond ;; Local-only entry: unchanged behavior. ((null view-id) (pearl--delete-saved-query-local name) - (message "Deleted saved query %s" name)) + (message "Deleted local view %s" name)) ;; Synced entry: ask whether to also drop the Linear view. ((yes-or-no-p (format "Also delete the linked Linear view (\"%s\")? " name)) @@ -3973,7 +3973,7 @@ the id so it can be deleted by hand)." (t ;; Unlink only: drop the local entry, leave the Linear view alone. (pearl--delete-saved-query-local name) - (message "Deleted saved query %s; Linear view kept (id %s)" + (message "Deleted local view %s; Linear view kept (id %s)" name view-id))))))) (defun pearl--delete-saved-query-do-linear-delete (name view-id) @@ -3988,10 +3988,10 @@ by hand." (cond (success (pearl--delete-saved-query-local name) - (message "Deleted saved query %s and removed view from Linear" name)) + (message "Deleted local view %s and removed view from Linear" name)) ((plist-get result :timeout) (message - "Delete of Linear view for %s timed out after %ds; saved query left in place -- the delete may still have completed (view id %s) -- re-run after the network recovers." + "Delete of Linear view for %s timed out after %ds; local view left in place -- the delete may still have completed (view id %s) -- re-run after the network recovers." name pearl-request-timeout view-id)) ((yes-or-no-p (format @@ -3999,22 +3999,22 @@ by hand." (or (plist-get result :error) "unknown error"))) (pearl--delete-saved-query-local name) (message - "Deleted saved query %s; Linear view is orphaned (id %s, delete manually)" + "Deleted local view %s; Linear view is orphaned (id %s, delete manually)" name view-id)) (t - (message "Kept saved query %s; Linear view also untouched" name))))) + (message "Kept local view %s; Linear view also untouched" name))))) (defun pearl--save-query (name filter-plist &optional sort order) - "Save FILTER-PLIST as the saved query NAME, replacing any entry of that NAME. -Persists `pearl-saved-queries' via Customize. SORT and ORDER are + "Save FILTER-PLIST as the local view NAME, replacing any entry of that NAME. +Persists `pearl-local-views' via Customize. SORT and ORDER are stored when given." (let ((entry (cons name (append (list :filter filter-plist) (when sort (list :sort sort)) (when order (list :order order)))))) - (setq pearl-saved-queries - (cons entry (assoc-delete-all name (copy-sequence pearl-saved-queries)))) + (setq pearl-local-views + (cons entry (assoc-delete-all name (copy-sequence pearl-local-views)))) (ignore-errors - (customize-save-variable 'pearl-saved-queries pearl-saved-queries)))) + (customize-save-variable 'pearl-local-views pearl-local-views)))) ;;; Saved-query -> Linear view sync (see docs/saved-query-sync-spec.org) @@ -4033,7 +4033,7 @@ agree byte-for-byte." (defun pearl--sync-scope-candidates (teams filter-team-key) "Build the (DISPLAY . PLIST) candidate list for the scope-and-visibility prompt. TEAMS is the team alist list returned by `pearl--all-teams' (each carries -`id', `key', `name'). FILTER-TEAM-KEY is the saved query's filter `:team' +`id', `key', `name'). FILTER-TEAM-KEY is the local view's filter `:team' value (a team key string) or nil. Each candidate's PLIST is `(:team-id ID-OR-NIL :team-name NAME :shared @@ -4095,7 +4095,7 @@ Returns the matching view alist or nil." views))) (defun pearl--save-query-mark-synced (name team-id shared view-id &optional view-url) - "Persist the sync metadata for saved query NAME. + "Persist the sync metadata for local view NAME. TEAM-ID is the team UUID or nil (personal scope). SHARED is t or nil. VIEW-ID is the Linear custom view UUID returned by the API. VIEW-URL, when provided, is the view's Linear web URL so @@ -4106,11 +4106,11 @@ unavailable to leave the prior `:linear-view-url' untouched. Adds or overwrites `:linear-view-id', `:linear-view-team-id', `:linear-view-shared', `:linear-view-synced-at', and (when VIEW-URL is non-nil) `:linear-view-url' on the entry, then persists -`pearl-saved-queries' via Customize. Signals `user-error' when NAME -isn't in `pearl-saved-queries'." - (let ((entry (assoc name pearl-saved-queries))) +`pearl-local-views' via Customize. Signals `user-error' when NAME +isn't in `pearl-local-views'." + (let ((entry (assoc name pearl-local-views))) (unless entry - (user-error "No saved query named %s" name)) + (user-error "No local view named %s" name)) ;; 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 @@ -4127,10 +4127,10 @@ isn't in `pearl-saved-queries'." ;; 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 + (setq pearl-local-views (cons (cons name new-spec) - (assoc-delete-all name (copy-sequence pearl-saved-queries)))) - (customize-save-variable 'pearl-saved-queries pearl-saved-queries)))) + (assoc-delete-all name (copy-sequence pearl-local-views)))) + (customize-save-variable 'pearl-local-views pearl-local-views)))) (defun pearl--sync-record-or-orphan-error (name team-id shared view-id verb &optional scope-label view-url) @@ -4156,14 +4156,14 @@ local link couldn't be saved." t) (error (message - "View %s on Linear as %s but local link couldn't be saved (%s). Re-run `pearl-sync-saved-query-to-linear' and pick Replace to reconcile." + "View %s on Linear as %s but local link couldn't be saved (%s). Re-run `pearl-publish-local-view' and pick Replace to reconcile." (if (string= verb "Updated") "updated" "created") view-id (error-message-string err)) nil))) (defun pearl--sync-saved-query-pick-scope (filter-plist) "Prompt the user for the destination scope of a sync-up. -FILTER-PLIST is the saved query's authoring filter (used to derive the +FILTER-PLIST is the local view's authoring filter (used to derive the default candidate). Returns `(DISPLAY . PLIST)', or nil when the user cancels via the cancel sentinel. DISPLAY is the candidate string the user picked; PLIST is `(:team-id ID-OR-NIL :team-name NAME :shared @@ -4210,32 +4210,32 @@ so the user isn't told the API refused something that never reached it." (if done result (list :success nil :timeout t)))) ;;;###autoload -(defun pearl-sync-saved-query-to-linear (name) - "Sync the local saved query NAME up to Linear as a Custom View. -First-time sync prompts for the destination scope (team and visibility), -then calls `customViewCreate'. Re-syncing an already-synced entry calls +(defun pearl-publish-local-view (name) + "Publish the local view NAME to Linear as a Custom View. +First publish prompts for the destination scope (team and visibility), +then calls `customViewCreate'. Re-publishing an already-published entry calls `customViewUpdate' against the stored view id -- the stored team and shared flag stay (use Replace/Rename below to reset them). A same-name collision in the chosen scope prompts Replace / Rename / Cancel. On success the entry is extended with the four `:linear-view-*' keys via `customize-save-variable'. On a successful API call followed by a persist failure, the message names the orphan view's id explicitly so -the user can re-sync with Replace to reconcile." +the user can re-publish with Replace to reconcile." (interactive - (list (if (null pearl-saved-queries) - (user-error "No saved queries to sync") + (list (if (null pearl-local-views) + (user-error "No local views to publish") (completing-read - "Sync saved query: " + "Publish local view: " (pearl--completion-table-keep-order (pearl--with-sentinel pearl--filter-cancel - (mapcar #'car pearl-saved-queries))) + (mapcar #'car pearl-local-views))) nil t)))) (when (pearl--filter-sentinel-value-p name) - (user-error "Cancelled; no saved query synced")) + (user-error "Cancelled; no local view published")) (pearl--require-account-context t) - (let ((entry (assoc name pearl-saved-queries))) + (let ((entry (assoc name pearl-local-views))) (unless entry - (user-error "No saved query named %s" name)) + (user-error "No local view named %s" name)) (let* ((spec (cdr entry)) (filter-plist (plist-get spec :filter)) (existing-view-id (plist-get spec :linear-view-id)) @@ -4255,7 +4255,7 @@ the user can re-sync with Replace to reconcile." ;; clicking through the scope picker only to hit "Linear refused". (unless filter-data (user-error - "Saved query %s has no filter constraints; Linear views need at least one constraint -- add one and re-sync" + "Local view %s has no filter constraints; Linear views need at least one constraint -- add one and re-sync" name)) (let ((scope-pair (if existing-view-id @@ -4281,7 +4281,7 @@ the user can re-sync with Replace to reconcile." ;; nil (cancel). (pearl--sync-saved-query-pick-scope filter-plist)))) (unless scope-pair - (user-error "Cancelled; no saved query synced")) + (user-error "Cancelled; no local view published")) ;; Validate the filter at the boundary so a malformed entry surfaces ;; before the API call. (pearl--validate-issue-filter filter-plist) @@ -4302,7 +4302,7 @@ the user can re-sync with Replace to reconcile." target-name)))) (pcase action ('cancel - (message "Cancelled; %s left unsynced" target-name)) + (message "Cancelled; %s left unpublished" target-name)) ('rename (setq target-name (read-string (format "New view name (was %s): " name))) @@ -4326,7 +4326,7 @@ the user can re-sync with Replace to reconcile." (defun pearl--sync-saved-query-do-create (name target-name team-id shared filter-data scope-label) - "Create a Linear view from saved query NAME, persisting the link on success. + "Create a Linear view from local view NAME, persisting the link on success. TARGET-NAME is the view name to send to Linear (usually NAME, but may differ when the user picked Rename to dodge a collision). TEAM-ID, SHARED, FILTER-DATA are the Linear API inputs. SCOPE-LABEL is the @@ -4342,24 +4342,24 @@ scope-and-visibility display string for the success message." (cond ((and success view-id) (pearl--sync-record-or-orphan-error - name team-id shared view-id "Synced" scope-label view-url)) + name team-id shared view-id "Published" scope-label view-url)) ((plist-get result :timeout) (message - "Sync of %s timed out after %ds; the create may still complete on Linear -- re-run and pick Replace to reconcile if a duplicate appears." + "Publish of %s timed out after %ds; the create may still complete on Linear -- re-run and pick Replace to reconcile if a duplicate appears." name pearl-request-timeout)) (t - (message "Failed to sync %s: %s" name + (message "Failed to publish %s: %s" name (or (plist-get result :error) "Linear refused the create")))))) ;;;###autoload -(defun pearl-publish-current-source () - "Publish the active buffer's local saved query to Linear as a Custom View. +(defun pearl-publish-current-view () + "Publish the active buffer's local view to Linear as a Custom View. Convenience wrapper. Reads `#+LINEAR-SOURCE' from the current buffer. -When it's a `:type filter' source whose `:name' matches a local saved -query, dispatches to `pearl-sync-saved-query-to-linear' with that name -pre-filled. Otherwise refuses with a clear message: the source is -already a Linear view (no publish needed), the filter is transient (no -name to sync under), or no local saved query matches the recorded name. +When it's a `:type filter' source whose `:name' matches a local view, +dispatches to `pearl-publish-local-view' with that name pre-filled. +Otherwise refuses with a clear message: the source is already a Linear +view (no publish needed), the filter is transient (no name to publish +under), or no local view matches the recorded name. Bound under \\='C-; L f P\\=' inside `pearl-mode' buffers; reachable through the transient as the `U' (upload) entry in the Fetch group." @@ -4379,15 +4379,15 @@ through the transient as the `U' (upload) entry in the Fetch group." type)) ((or (null name) (and (stringp name) (string-blank-p name))) (user-error "Source has no name; save it first via pearl-list-issues-filtered then re-publish")) - ((not (assoc name pearl-saved-queries)) - (user-error "No local saved query named %s; save it first via pearl-list-issues-filtered" + ((not (assoc name pearl-local-views)) + (user-error "No local view named %s; save it first via pearl-list-issues-filtered" name)) (t - (pearl-sync-saved-query-to-linear name))))) + (pearl-publish-local-view name))))) (defun pearl--sync-saved-query-do-update (name view-id team-id shared filter-data scope-label) - "Update an existing Linear view from saved query NAME by VIEW-ID. + "Update an existing Linear view from local view NAME by VIEW-ID. TEAM-ID, SHARED, FILTER-DATA are the Linear API inputs. SCOPE-LABEL is the scope-and-visibility display string for the success message." (pearl--progress "Updating Linear view %s..." name) @@ -4423,7 +4423,7 @@ filter; SAVE-NAME, when given, persists it via `pearl--save-query'." (interactive (list (pearl--read-filter-interactively) (when (pearl--read-yes-no "Save this filter locally so you can re-run it (not pushed to Linear)? ") - (read-string "Name for this saved query: ")))) + (read-string "Name for this local view: ")))) ;; Validate before saving or running, so a bad filter never gets persisted ;; or run -- the command boundary is where the clear error belongs. (pearl--validate-issue-filter filter-plist) @@ -4463,7 +4463,7 @@ no Comments subtree." "Render a query RESULT into the active file, tagged with SOURCE. Normalizes the raw nodes, writes them via `pearl--update-org-from-issues' with SOURCE and the truncation flag, and reports the outcome. The one render -boundary shared by list-issues, the ad-hoc filter, saved queries, views, and +boundary shared by list-issues, the ad-hoc filter, local views, views, and refresh." (pcase (pearl--query-result-status result) ('ok @@ -5613,7 +5613,7 @@ commands. Errors if no source is recorded." (_ (user-error "Unknown Linear source type: %s" (plist-get source :type))))))) ;;;###autoload -(defun pearl-run-view (view-name) +(defun pearl-run-linear-view (view-name) "Run a Linear Custom View by VIEW-NAME and render it into the active file. Interactively, completes over the workspace's Custom Views. The view's own filter runs server-side; the result replaces the active file (behind the @@ -6155,17 +6155,17 @@ body stay." ["Delete" ("k" "delete issue" pearl-delete-current-issue) ("K" "delete comment" pearl-delete-current-comment) - ("q" "delete saved query" pearl-delete-saved-query)]] + ("q" "delete local view" pearl-delete-local-view)]] ["Workspace" ["Fetch" ("P" "pick source" pearl-pick-source) ("l" "my open issues" pearl-list-issues) ("p" "by project" pearl-list-issues-by-project) ("f" "build a filter" pearl-list-issues-filtered) - ("v" "custom view" pearl-run-view) - ("Q" "saved query" pearl-run-saved-query) - ("S" "sync saved query" pearl-sync-saved-query-to-linear) - ("U" "upload current source" pearl-publish-current-source)] + ("v" "custom view" pearl-run-linear-view) + ("Q" "local view" pearl-run-local-view) + ("S" "sync local view" pearl-publish-local-view) + ("U" "upload current source" pearl-publish-current-view)] ["View" ("g" "refresh view" pearl-refresh-current-view) ("r" "refresh issue" pearl-refresh-current-issue) @@ -6205,12 +6205,12 @@ body stay." (define-key map "o" (cons "my open issues" #'pearl-list-issues)) (define-key map "p" (cons "by project" #'pearl-list-issues-by-project)) (define-key map "f" (cons "build a filter" #'pearl-list-issues-filtered)) - (define-key map "v" (cons "custom view" #'pearl-run-view)) - (define-key map "q" (cons "saved query" #'pearl-run-saved-query)) - (define-key map "S" (cons "sync saved query to Linear" - #'pearl-sync-saved-query-to-linear)) + (define-key map "v" (cons "custom view" #'pearl-run-linear-view)) + (define-key map "q" (cons "local view" #'pearl-run-local-view)) + (define-key map "S" (cons "sync local view to Linear" + #'pearl-publish-local-view)) (define-key map "P" (cons "publish current source to Linear" - #'pearl-publish-current-source)) + #'pearl-publish-current-view)) map) "Pearl fetch commands; a sub-keymap of `pearl-prefix-map'.") @@ -6235,7 +6235,7 @@ body stay." (let ((map (make-sparse-keymap))) (define-key map "t" (cons "delete issue" #'pearl-delete-current-issue)) (define-key map "c" (cons "delete comment" #'pearl-delete-current-comment)) - (define-key map "q" (cons "delete saved query" #'pearl-delete-saved-query)) + (define-key map "q" (cons "delete local view" #'pearl-delete-local-view)) map) "Pearl delete commands; a sub-keymap of `pearl-prefix-map'.") diff --git a/tests/test-integration-acceptance.el b/tests/test-integration-acceptance.el index 523d1b3..0e1c64d 100644 --- a/tests/test-integration-acceptance.el +++ b/tests/test-integration-acceptance.el @@ -29,7 +29,7 @@ ;; and a field-setter command — runs for real against a temp active file. ;; ;; Components integrated (all real unless noted): -;; - pearl-run-saved-query -> --build-issue-filter -> --query-issues-async +;; - pearl-run-local-view -> --build-issue-filter -> --query-issues-async ;; - --query-issues-async -> --graphql-request-async (MOCKED at the wire) ;; - --render-query-result -> --normalize-issue -> --sort-issues ;; - --update-org-from-issues (real disk write + buffer surface, surface MOCKED) @@ -102,7 +102,7 @@ because the description-update mutation contains both `IssueDescription' and (pearl-org-file-path tmp) (pearl-api-key "test-key") (pearl-fold-after-update nil) - (pearl-saved-queries + (pearl-local-views '(("Open" :filter (:open t) :sort priority :order asc) ("Bugs" :filter (:labels ("bug") :open t)))) (test-integration--ops nil)) @@ -124,9 +124,9 @@ because the description-update mutation contains both `IssueDescription' and (ignore-errors (delete-file tmp)))))) (ert-deftest test-integration-run-saved-query-renders-active-file () - "A saved query fetches, renders to the active file with a source header, and surfaces a buffer." + "A local view fetches, renders to the active file with a source header, and surfaces a buffer." (test-integration--with-env - (pearl-run-saved-query "Open") + (pearl-run-local-view "Open") (let ((buf (find-buffer-visiting pearl-org-file-path))) (should (buffer-live-p buf)) (with-current-buffer buf @@ -146,7 +146,7 @@ because the description-update mutation contains both `IssueDescription' and (ert-deftest test-integration-refresh-reruns-recorded-source () "Refresh reads the recorded source from the buffer and merges a re-fetch in place." (test-integration--with-env - (pearl-run-saved-query "Open") + (pearl-run-local-view "Open") (with-current-buffer (find-buffer-visiting pearl-org-file-path) (setq test-integration--ops nil) (pearl-refresh-current-view) @@ -159,22 +159,22 @@ because the description-update mutation contains both `IssueDescription' and (should (re-search-forward "Second issue" nil t))))) (ert-deftest test-integration-switch-source-protects-dirty-buffer () - "Switching to a different saved query with unsaved edits does not overwrite them." + "Switching to a different local view with unsaved edits does not overwrite them." (test-integration--with-env - (pearl-run-saved-query "Open") + (pearl-run-local-view "Open") (with-current-buffer (find-buffer-visiting pearl-org-file-path) (goto-char (point-max)) (insert "\nUNSAVED LOCAL EDIT\n") (should (buffer-modified-p)) ;; the switch must defer rather than clobber the dirty buffer - (pearl-run-saved-query "Bugs") + (pearl-run-local-view "Bugs") (goto-char (point-min)) (should (re-search-forward "UNSAVED LOCAL EDIT" nil t))))) (ert-deftest test-integration-issue-commands-from-subtree () "From inside a rendered issue subtree: syncing an edited body pushes, adding a comment inserts it, and setting priority drives a field update — all through the real request path." (test-integration--with-env - (pearl-run-saved-query "Open") + (pearl-run-local-view "Open") (with-current-buffer (find-buffer-visiting pearl-org-file-path) ;; --- sync a description edit (remote unchanged -> clean push) --- (goto-char (point-min)) diff --git a/tests/test-pearl-accounts.el b/tests/test-pearl-accounts.el index f8bc06b..2a4cb69 100644 --- a/tests/test-pearl-accounts.el +++ b/tests/test-pearl-accounts.el @@ -403,35 +403,35 @@ fetching the active account's issues into it would cross workspaces." ;;; Saved-query :account guard (ert-deftest test-pearl-run-saved-query-refuses-other-account-before-network () - "A saved query tagged for another account refuses before any compilation or fetch." + "A local view tagged for another account refuses before any compilation or fetch." (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org") ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org"))) (pearl-active-account "home") (pearl-default-account "home") - (pearl-saved-queries '(("Work bugs" :account "work" :filter (:assignee :me)))) + (pearl-local-views '(("Work bugs" :account "work" :filter (:assignee :me)))) (api-called nil)) (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) - (let ((err (should-error (pearl-run-saved-query "Work bugs") :type 'error))) + (let ((err (should-error (pearl-run-local-view "Work bugs") :type 'error))) (should (string-match-p "work" (error-message-string err)))) (should-not api-called)))) (ert-deftest test-pearl-run-saved-query-account-match-proceeds-to-fetch () - "A saved query tagged for the active account compiles and dispatches." + "A local view tagged for the active account compiles and dispatches." (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) (pearl-active-account "work") (pearl-default-account "work") - (pearl-saved-queries '(("Work bugs" :account "work" :filter (:assignee :me)))) + (pearl-local-views '(("Work bugs" :account "work" :filter (:assignee :me)))) (api-called nil)) (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) - (pearl-run-saved-query "Work bugs") + (pearl-run-local-view "Work bugs") (should api-called)))) (ert-deftest test-pearl-run-saved-query-account-tag-ignored-in-legacy () "With no accounts configured, an `:account' tag is inert (shared query)." (let ((pearl-accounts nil) (pearl-active-account nil) (pearl-default-account nil) (pearl-api-key "lin_legacy") - (pearl-saved-queries '(("Work bugs" :account "work" :filter (:assignee :me)))) + (pearl-local-views '(("Work bugs" :account "work" :filter (:assignee :me)))) (api-called nil)) (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) - (pearl-run-saved-query "Work bugs") + (pearl-run-local-view "Work bugs") (should api-called)))) ;;; pearl-load-api-key-from-env legacy-only diff --git a/tests/test-pearl-adhoc.el b/tests/test-pearl-adhoc.el index 558578a..4959840 100644 --- a/tests/test-pearl-adhoc.el +++ b/tests/test-pearl-adhoc.el @@ -159,82 +159,82 @@ candidate list, so completion still works the way the framework expects." (ert-deftest test-pearl-save-query-adds-entry () "Saving a query adds it to the saved-queries variable." - (let ((pearl-saved-queries nil)) + (let ((pearl-local-views nil)) (cl-letf (((symbol-function 'customize-save-variable) (lambda (&rest _) nil))) (pearl--save-query "My filter" '(:open t :labels ("bug"))) - (let ((entry (assoc "My filter" pearl-saved-queries))) + (let ((entry (assoc "My filter" pearl-local-views))) (should entry) (should (equal '(:open t :labels ("bug")) (plist-get (cdr entry) :filter))))))) (ert-deftest test-pearl-save-query-replaces-same-name () "Saving under an existing name replaces that entry rather than duplicating." - (let ((pearl-saved-queries '(("Dup" :filter (:open t))))) + (let ((pearl-local-views '(("Dup" :filter (:open t))))) (cl-letf (((symbol-function 'customize-save-variable) (lambda (&rest _) nil))) (pearl--save-query "Dup" '(:priority 1)) - (should (= 1 (cl-count "Dup" pearl-saved-queries + (should (= 1 (cl-count "Dup" pearl-local-views :key #'car :test #'string=))) (should (equal '(:priority 1) - (plist-get (cdr (assoc "Dup" pearl-saved-queries)) :filter)))))) + (plist-get (cdr (assoc "Dup" pearl-local-views)) :filter)))))) -;;; pearl-delete-saved-query +;;; pearl-delete-local-view (ert-deftest test-pearl-delete-saved-query-removes-entry-on-yes () "Confirming the prompt removes the named entry and persists." - (let ((pearl-saved-queries '(("Keep" :filter (:open t)) + (let ((pearl-local-views '(("Keep" :filter (:open t)) ("Drop" :filter (:priority 1)))) (persisted nil)) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) ((symbol-function 'customize-save-variable) (lambda (&rest _) (setq persisted t)))) - (pearl-delete-saved-query "Drop") - (should-not (assoc "Drop" pearl-saved-queries)) - (should (assoc "Keep" pearl-saved-queries)) + (pearl-delete-local-view "Drop") + (should-not (assoc "Drop" pearl-local-views)) + (should (assoc "Keep" pearl-local-views)) (should persisted)))) (ert-deftest test-pearl-delete-saved-query-no-at-confirm-keeps-entry () "Declining the confirm prompt leaves the entry in place and does not persist." - (let ((pearl-saved-queries '(("Keep" :filter (:open t)))) + (let ((pearl-local-views '(("Keep" :filter (:open t)))) (persisted nil)) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) nil)) ((symbol-function 'customize-save-variable) (lambda (&rest _) (setq persisted t)))) - (pearl-delete-saved-query "Keep") - (should (assoc "Keep" pearl-saved-queries)) + (pearl-delete-local-view "Keep") + (should (assoc "Keep" pearl-local-views)) (should-not persisted)))) (ert-deftest test-pearl-delete-saved-query-sentinel-cancels () "Picking the `pearl--filter-cancel' sentinel cancels without touching anything." - (let ((pearl-saved-queries '(("Keep" :filter (:open t)))) + (let ((pearl-local-views '(("Keep" :filter (:open t)))) (confirmed nil)) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) (setq confirmed t) t)) ((symbol-function 'customize-save-variable) (lambda (&rest _) nil))) - (pearl-delete-saved-query pearl--filter-cancel) - (should (assoc "Keep" pearl-saved-queries)) + (pearl-delete-local-view pearl--filter-cancel) + (should (assoc "Keep" pearl-local-views)) (should-not confirmed)))) (ert-deftest test-pearl-delete-saved-query-unknown-name-errors () "An unknown name (e.g. typed past completion) signals a user-error." - (let ((pearl-saved-queries '(("Keep" :filter (:open t))))) - (should-error (pearl-delete-saved-query "Nope") :type 'user-error))) + (let ((pearl-local-views '(("Keep" :filter (:open t))))) + (should-error (pearl-delete-local-view "Nope") :type 'user-error))) (ert-deftest test-pearl-delete-saved-query-empty-list-errors-on-interactive () - "Called interactively with no saved queries, the command errors cleanly + "Called interactively with no local views, the command errors cleanly rather than offering an empty picker." - (let ((pearl-saved-queries nil)) - (should-error (call-interactively #'pearl-delete-saved-query) + (let ((pearl-local-views nil)) + (should-error (call-interactively #'pearl-delete-local-view) :type 'user-error))) -;;; pearl-run-saved-query sentinel cancel +;;; pearl-run-local-view sentinel cancel (ert-deftest test-pearl-run-saved-query-sentinel-cancels () "Picking the `pearl--filter-cancel' sentinel cancels the run without fetching." (let ((fetched nil) - (pearl-saved-queries '(("Open" :filter (:open t))))) + (pearl-local-views '(("Open" :filter (:open t))))) (cl-letf (((symbol-function 'pearl--query-issues-async) (lambda (&rest _) (setq fetched t)))) - (should-error (pearl-run-saved-query pearl--filter-cancel) :type 'user-error) + (should-error (pearl-run-local-view pearl--filter-cancel) :type 'user-error) (should-not fetched)))) ;;; pearl-list-issues-filtered @@ -255,8 +255,8 @@ rather than offering an empty picker." (should (equal '(:assignee :me :open t) (plist-get rendered-source :filter)))))) (ert-deftest test-pearl-list-issues-filtered-saves-when-named () - "Passing a save name persists the ad-hoc filter as a saved query." - (let ((pearl-saved-queries nil)) + "Passing a save name persists the ad-hoc filter as a local view." + (let ((pearl-local-views nil)) (cl-letf (((symbol-function 'pearl--build-issue-filter) (lambda (_p) nil)) ((symbol-function 'pearl--query-issues-async) (lambda (_f cb &optional _o) @@ -264,7 +264,7 @@ rather than offering an empty picker." ((symbol-function 'pearl--render-query-result) (lambda (&rest _) nil)) ((symbol-function 'customize-save-variable) (lambda (&rest _) nil))) (pearl-list-issues-filtered '(:open t) "Saved adhoc") - (should (assoc "Saved adhoc" pearl-saved-queries))))) + (should (assoc "Saved adhoc" pearl-local-views))))) (provide 'test-pearl-adhoc) ;;; test-pearl-adhoc.el ends here diff --git a/tests/test-pearl-bugfixes.el b/tests/test-pearl-bugfixes.el index 9fe2518..c1348f1 100644 --- a/tests/test-pearl-bugfixes.el +++ b/tests/test-pearl-bugfixes.el @@ -133,21 +133,21 @@ ;;; Bug: saved/ad-hoc query filters bypass validation (ert-deftest test-pearl-run-saved-query-validates-before-fetch () - "A saved query with an unknown filter key errors before any fetch." - (let ((pearl-saved-queries '(("bad" :filter (:bogus-key "x")))) + "A local view with an unknown filter key errors before any fetch." + (let ((pearl-local-views '(("bad" :filter (:bogus-key "x")))) (fetched nil)) (cl-letf (((symbol-function 'pearl--query-issues-async) (lambda (&rest _) (setq fetched t)))) - (should-error (pearl-run-saved-query "bad") :type 'user-error) + (should-error (pearl-run-local-view "bad") :type 'user-error) (should-not fetched)))) (ert-deftest test-pearl-run-saved-query-validates-bad-priority () - "A saved query with an out-of-range priority errors before any fetch." - (let ((pearl-saved-queries '(("bad" :filter (:priority 9)))) + "A local view with an out-of-range priority errors before any fetch." + (let ((pearl-local-views '(("bad" :filter (:priority 9)))) (fetched nil)) (cl-letf (((symbol-function 'pearl--query-issues-async) (lambda (&rest _) (setq fetched t)))) - (should-error (pearl-run-saved-query "bad") :type 'user-error) + (should-error (pearl-run-local-view "bad") :type 'user-error) (should-not fetched)))) (provide 'test-pearl-bugfixes) diff --git a/tests/test-pearl-favorites.el b/tests/test-pearl-favorites.el index 70507d7..b6235c7 100644 --- a/tests/test-pearl-favorites.el +++ b/tests/test-pearl-favorites.el @@ -177,7 +177,7 @@ For an issue node, IDENTIFIER fills the issue's user-facing key." ;;; pick-source candidate builder (ert-deftest test-pearl-pick-source-candidates-orders-favorites-by-sort-order () - "Favorites are listed first in ascending :sort-order, then saved queries alphabetically." + "Favorites are listed first in ascending :sort-order, then local views alphabetically." (let* ((favs (list (test-pearl-fav--norm 'project "p1" "Bravo") ; sort-order 0.0 (let ((f (test-pearl-fav--norm 'view "v1" "Alpha"))) (plist-put f :sort-order -1.0)))) ; lower, comes first @@ -210,10 +210,10 @@ For an issue node, IDENTIFIER fills the issue's user-facing key." (should (= 2 (length (delete-dups (copy-sequence labels))))))) (ert-deftest test-pearl-pick-source-candidates-empty () - "No favorites and no saved queries yields no candidates." + "No favorites and no local views yields no candidates." (should (null (pearl--pick-source-candidates nil nil)))) -;;; pick-source candidate builder -- synced saved queries (view-sync Phase 4) +;;; pick-source candidate builder -- synced local views (view-sync Phase 4) (ert-deftest test-pearl-pick-source-candidates-synced-saved-query-team-scope () "A team-scoped synced entry renders as `[saved \\u2192 ] '." @@ -285,7 +285,7 @@ source plist; the open-in-Linear command errors cleanly until the user re-syncs. (should-not (plist-get source :url)))) (ert-deftest test-pearl-pick-source-candidates-local-only-saved-query-keeps-saved-label () - "A saved query without :linear-view-id still renders as `[saved] ' and dispatches as filter." + "A local view without :linear-view-id still renders as `[saved] ' and dispatches as filter." (let* ((saved '(("Local" :filter (:open t)))) (cands (pearl--pick-source-candidates nil saved '())) (source (cdr (assoc "[saved] Local" cands)))) diff --git a/tests/test-pearl-keymap.el b/tests/test-pearl-keymap.el index 1ae0d3d..f80a002 100644 --- a/tests/test-pearl-keymap.el +++ b/tests/test-pearl-keymap.el @@ -71,11 +71,11 @@ The lowercase `c' is no longer a direct command -- it is the create prefix." (should (eq 'pearl-list-issues (lookup-key pearl-prefix-map (kbd "f o")))) (should (eq 'pearl-list-issues-by-project (lookup-key pearl-prefix-map (kbd "f p")))) (should (eq 'pearl-list-issues-filtered (lookup-key pearl-prefix-map (kbd "f f")))) - (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 + (should (eq 'pearl-run-linear-view (lookup-key pearl-prefix-map (kbd "f v")))) + (should (eq 'pearl-run-local-view (lookup-key pearl-prefix-map (kbd "f q")))) + (should (eq 'pearl-publish-local-view (lookup-key pearl-prefix-map (kbd "f S")))) - (should (eq 'pearl-publish-current-source + (should (eq 'pearl-publish-current-view (lookup-key pearl-prefix-map (kbd "f P"))))) (ert-deftest test-pearl-prefix-map-edit-group () @@ -94,7 +94,7 @@ The lowercase `c' is no longer a direct command -- it is the create prefix." (should (eq 'pearl-create-comment (lookup-key pearl-prefix-map (kbd "c c")))) (should (eq 'pearl-delete-current-issue (lookup-key pearl-prefix-map (kbd "k t")))) (should (eq 'pearl-delete-current-comment (lookup-key pearl-prefix-map (kbd "k c")))) - (should (eq 'pearl-delete-saved-query (lookup-key pearl-prefix-map (kbd "k q"))))) + (should (eq 'pearl-delete-local-view (lookup-key pearl-prefix-map (kbd "k q"))))) (ert-deftest test-pearl-prefix-map-open-and-copy-groups () "The open group resolves the two open actions; copy resolves copy-url." diff --git a/tests/test-pearl-menu.el b/tests/test-pearl-menu.el index e2ea550..9c97384 100644 --- a/tests/test-pearl-menu.el +++ b/tests/test-pearl-menu.el @@ -60,10 +60,10 @@ menu entry that still points at it fails here." "A representative slice of the command surface is reachable from the menu." (let ((cmds (mapcar #'cdr (test-pearl-menu--pairs)))) (dolist (expected '(pearl-list-issues - pearl-run-view - pearl-run-saved-query - pearl-sync-saved-query-to-linear - pearl-publish-current-source + pearl-run-linear-view + pearl-run-local-view + pearl-publish-local-view + pearl-publish-current-view pearl-save-issue pearl-save-all pearl-edit-state diff --git a/tests/test-pearl-naming.el b/tests/test-pearl-naming.el new file mode 100644 index 0000000..41077e9 --- /dev/null +++ b/tests/test-pearl-naming.el @@ -0,0 +1,74 @@ +;;; test-pearl-naming.el --- Naming-regression tests for local/Linear views -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;; Author: Craig Jennings + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; The local/Linear views spec renames Pearl's "saved query" surface to +;; local-view vocabulary with NO obsolete aliases (Pearl has no users). +;; These tests enforce the contract: the new public symbols exist, the old +;; ones are gone (not aliased), and no user-facing command exposes "query". + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +(defconst test-pearl-naming--new-commands + '(pearl-run-local-view + pearl-delete-local-view + pearl-publish-local-view + pearl-publish-current-view + pearl-run-linear-view) + "Public commands the rename introduces.") + +(defconst test-pearl-naming--old-symbols + '(pearl-saved-queries + pearl-run-saved-query + pearl-delete-saved-query + pearl-sync-saved-query-to-linear + pearl-run-view + pearl-publish-current-source) + "Old user-facing symbols that must be gone (renamed, not aliased).") + +(ert-deftest test-pearl-naming-new-defcustom-bound () + "`pearl-local-views' is the renamed local-view store." + (should (boundp 'pearl-local-views))) + +(ert-deftest test-pearl-naming-new-commands-exist () + "Every renamed public command is defined and interactive." + (dolist (cmd test-pearl-naming--new-commands) + (should (fboundp cmd)) + (should (commandp cmd)))) + +(ert-deftest test-pearl-naming-old-symbols-gone () + "The old saved-query symbols are not bound -- direct rename, no aliases." + (dolist (sym test-pearl-naming--old-symbols) + (should-not (fboundp sym)) + (should-not (boundp sym)))) + +(ert-deftest test-pearl-naming-no-user-facing-query-in-docstrings () + "Public local-view commands and the defcustom carry no \"saved query\" text. +\"query\" alone survives only in internal GraphQL helpers, not here." + (dolist (sym (cons 'pearl-local-views test-pearl-naming--new-commands)) + (let ((doc (or (documentation-property sym 'variable-documentation) + (ignore-errors (documentation sym)) + ""))) + (should-not (string-match-p "saved quer" doc))))) + +(provide 'test-pearl-naming) +;;; test-pearl-naming.el ends here diff --git a/tests/test-pearl-resolve.el b/tests/test-pearl-resolve.el index 0812f94..aefe27a 100644 --- a/tests/test-pearl-resolve.el +++ b/tests/test-pearl-resolve.el @@ -21,7 +21,7 @@ ;; Tests for the per-team cached collection fetch (`--team-collection') and ;; the name->id resolver (`--resolve-team-id'), which back the field commands, -;; the ad-hoc filter, and saved queries. The HTTP boundary is stubbed; the +;; the ad-hoc filter, and local views. The HTTP boundary is stubbed; the ;; cache is reset around each test. ;;; Code: diff --git a/tests/test-pearl-saved-query-sync.el b/tests/test-pearl-saved-query-sync.el index e237108..534993e 100644 --- a/tests/test-pearl-saved-query-sync.el +++ b/tests/test-pearl-saved-query-sync.el @@ -1,4 +1,4 @@ -;;; test-pearl-saved-query-sync.el --- Tests for syncing saved queries to Linear views -*- lexical-binding: t; -*- +;;; test-pearl-saved-query-sync.el --- Tests for syncing local views to Linear views -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings @@ -19,7 +19,7 @@ ;;; Commentary: -;; Tests for `pearl-sync-saved-query-to-linear' and its helpers +;; Tests for `pearl-publish-local-view' and its helpers ;; (`pearl--sync-scope-candidates', `pearl--find-view-by-name-in-scope', ;; `pearl--customview-create-async', `pearl--customview-update-async', ;; `pearl--save-query-mark-synced', `pearl--sync-record-or-orphan-error'). @@ -254,11 +254,11 @@ label, plist team-id nil + team-name \"Personal\" + shared nil." (ert-deftest test-pearl-save-query-mark-synced-adds-the-four-keys () "After mark-synced, the entry carries `:linear-view-id', `-team-id', `-shared', `-synced-at'." - (let ((pearl-saved-queries + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t)))))) (cl-letf (((symbol-function 'customize-save-variable) (lambda (_ _) nil))) (pearl--save-query-mark-synced "Q" "team-uuid" t "view-uuid")) - (let ((spec (cdr (assoc "Q" pearl-saved-queries)))) + (let ((spec (cdr (assoc "Q" pearl-local-views)))) (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))) @@ -268,39 +268,39 @@ label, plist team-id nil + team-name \"Personal\" + shared nil." (ert-deftest test-pearl-save-query-mark-synced-preserves-sort-and-order () "Mark-synced doesn't drop `:sort' or `:order' from the entry." - (let ((pearl-saved-queries + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :sort updated :order desc))))) (cl-letf (((symbol-function 'customize-save-variable) (lambda (_ _) nil))) (pearl--save-query-mark-synced "Q" nil nil "view-uuid")) - (let ((spec (cdr (assoc "Q" pearl-saved-queries)))) + (let ((spec (cdr (assoc "Q" pearl-local-views)))) (should (eq 'updated (plist-get spec :sort))) (should (eq 'desc (plist-get spec :order)))))) (ert-deftest test-pearl-save-query-mark-synced-personal-nil-team () "Personal scope persists `:linear-view-team-id' as nil and `:linear-view-shared' nil." - (let ((pearl-saved-queries + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t)))))) (cl-letf (((symbol-function 'customize-save-variable) (lambda (_ _) nil))) (pearl--save-query-mark-synced "Q" nil nil "view-uuid")) - (let ((spec (cdr (assoc "Q" pearl-saved-queries)))) + (let ((spec (cdr (assoc "Q" pearl-local-views)))) (should (null (plist-get spec :linear-view-team-id))) (should (null (plist-get spec :linear-view-shared)))))) (ert-deftest test-pearl-save-query-mark-synced-unknown-name-signals-user-error () "Mark-synced on a name that doesn't exist signals a `user-error'." - (let ((pearl-saved-queries '())) + (let ((pearl-local-views '())) (should-error (pearl--save-query-mark-synced "Q" nil nil "view-uuid") :type 'user-error))) (ert-deftest test-pearl-save-query-mark-synced-stores-view-url-when-given () "The optional VIEW-URL arg lands in the entry as :linear-view-url." - (let ((pearl-saved-queries + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t)))))) (cl-letf (((symbol-function 'customize-save-variable) (lambda (_ _) nil))) (pearl--save-query-mark-synced "Q" nil nil "view-uuid" "https://linear.app/ws/view/abc")) - (let ((spec (cdr (assoc "Q" pearl-saved-queries)))) + (let ((spec (cdr (assoc "Q" pearl-local-views)))) (should (equal "https://linear.app/ws/view/abc" (plist-get spec :linear-view-url)))))) @@ -308,7 +308,7 @@ label, plist team-id nil + team-name \"Personal\" + shared nil." "Omitting VIEW-URL on a re-sync preserves any previously stored URL. Re-sync paths whose API response doesn't carry a URL shouldn't erase a URL the create path already wrote." - (let ((pearl-saved-queries + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :linear-view-id "old-id" :linear-view-team-id nil @@ -317,7 +317,7 @@ URL the create path already wrote." (cl-letf (((symbol-function 'customize-save-variable) (lambda (_ _) nil))) ;; No VIEW-URL passed -- the prior URL should survive. (pearl--save-query-mark-synced "Q" nil nil "new-id")) - (let ((spec (cdr (assoc "Q" pearl-saved-queries)))) + (let ((spec (cdr (assoc "Q" pearl-local-views)))) (should (equal "https://linear.app/ws/view/old" (plist-get spec :linear-view-url)))))) @@ -327,7 +327,7 @@ 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 + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :sort updated :order desc @@ -335,7 +335,7 @@ erased on every re-sync." :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)))) + (let ((spec (cdr (assoc "Q" pearl-local-views)))) ;; Original keys survive. (should (equal '(:open t) (plist-get spec :filter))) (should (eq 'updated (plist-get spec :sort))) @@ -351,7 +351,7 @@ erased on every re-sync." (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 + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :linear-view-id "OLD-id" :linear-view-team-id "OLD-team" @@ -359,7 +359,7 @@ erased on every re-sync." :linear-view-synced-at "1970-01-01T00:00:00Z"))))) (cl-letf (((symbol-function 'customize-save-variable) (lambda (_ _) nil))) (pearl--save-query-mark-synced "Q" "NEW-team" t "NEW-id")) - (let ((spec (cdr (assoc "Q" pearl-saved-queries)))) + (let ((spec (cdr (assoc "Q" pearl-local-views)))) (should (equal "NEW-id" (plist-get spec :linear-view-id))) (should (equal "NEW-team" (plist-get spec :linear-view-team-id))) (should (eq t (plist-get spec :linear-view-shared))) @@ -445,7 +445,7 @@ erased on every re-sync." "Re-sync of an entry with `:linear-view-shared :json-false' must NOT flip it shared. `:json-false' is truthy in Elisp, so a stored `:json-false' would slip past `(if shared t :json-false)' in the encoder and silently make the view team-shared." - (let ((pearl-saved-queries + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :linear-view-id "view-id" :linear-view-team-id nil @@ -465,18 +465,18 @@ erased on every re-sync." (funcall cb (list :success t :view '((id . "view-id") (shared . :json-false))))))) - (pearl-sync-saved-query-to-linear "Q") + (pearl-publish-local-view "Q") (let ((shared-value (cdr (assoc "shared" captured-input)))) (should (eq :json-false shared-value)))))) -;;; pearl-sync-saved-query-to-linear — empty filter refusal +;;; pearl-publish-local-view — empty filter refusal (ert-deftest test-pearl-sync-empty-filter-refuses-before-scope-prompt () - "A first-time-sync saved query whose filter compiles to nil signals a clear + "A first-time-sync local view 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)))) + (let ((pearl-local-views (copy-tree '(("EmptyFilter" :filter nil)))) (scope-prompt-fired nil) (api-called nil)) (cl-letf (((symbol-function 'pearl--sync-saved-query-pick-scope) @@ -488,7 +488,7 @@ user has already clicked through the scope picker." (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") + (should-error (pearl-publish-local-view "EmptyFilter") :type 'user-error) (should-not scope-prompt-fired) (should-not api-called)))) @@ -498,7 +498,7 @@ user has already clicked through the scope picker." edited down to nil takes the same empty-filter guard, refusing without calling customViewUpdate. Guards against a future refactor accidentally hoisting the guard into the create-only branch." - (let ((pearl-saved-queries + (let ((pearl-local-views (copy-tree '(("EmptyFilter" :filter nil :linear-view-id "existing-view-uuid" :linear-view-team-id nil @@ -506,11 +506,11 @@ hoisting the guard into the create-only branch." (update-called nil)) (cl-letf (((symbol-function 'pearl--customview-update-async) (lambda (&rest _) (setq update-called t)))) - (should-error (pearl-sync-saved-query-to-linear "EmptyFilter") + (should-error (pearl-publish-local-view "EmptyFilter") :type 'user-error) (should-not update-called)))) -;;; pearl-publish-current-source +;;; pearl-publish-current-view (defmacro test-pearl-publish--in-buffer (source-form &rest body) "Run BODY in an org-mode temp buffer carrying SOURCE-FORM as #+LINEAR-SOURCE. @@ -526,50 +526,50 @@ SOURCE-FORM is the source plist (or nil for no source header)." (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))) + (should-error (pearl-publish-current-view) :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))) + (should-error (pearl-publish-current-view) :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))) + (should-error (pearl-publish-current-view) :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))))) + "A :type filter source whose name doesn't match any pearl-local-views entry errors." + (let ((pearl-local-views '(("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)))) + (should-error (pearl-publish-current-view) :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)))) + "A :type filter source whose name matches dispatches to pearl-publish-local-view." + (let ((pearl-local-views '(("Mine" :filter (:open t)))) (sync-called-with nil)) - (cl-letf (((symbol-function 'pearl-sync-saved-query-to-linear) + (cl-letf (((symbol-function 'pearl-publish-local-view) (lambda (n) (setq sync-called-with n)))) (test-pearl-publish--in-buffer '(:type filter :name "Mine" :filter (:open t)) - (pearl-publish-current-source)) + (pearl-publish-current-view)) (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))) + (should-error (pearl-publish-current-view) :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 '())) + (let ((pearl-local-views '())) (test-pearl-publish--in-buffer '(:type filter :name " " :filter (:open t)) - (let ((err (should-error (pearl-publish-current-source) + (let ((err (should-error (pearl-publish-current-view) :type 'user-error))) ;; The message should name the source-has-no-name reason, not the ;; no-matching-query reason. @@ -609,7 +609,7 @@ team-key lookups." (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 +;;; pearl-delete-local-view — 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. @@ -623,7 +623,7 @@ more prompts fire than answers were prepared for." (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 + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :linear-view-id "view-uuid" :linear-view-team-id nil @@ -638,13 +638,13 @@ more prompts fire than answers were prepared for." (lambda (vid cb) (setq delete-called-with vid) (funcall cb (list :success t))))) - (pearl-delete-saved-query "Q") + (pearl-delete-local-view "Q") (should (equal "view-uuid" delete-called-with)) - (should-not (assoc "Q" pearl-saved-queries))))) + (should-not (assoc "Q" pearl-local-views))))) (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 + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :linear-view-id "view-uuid")))) (delete-called nil)) @@ -655,13 +655,13 @@ more prompts fire than answers were prepared for." ((symbol-function 'message) (lambda (&rest _) nil)) ((symbol-function 'pearl--customview-delete-async) (lambda (_ _) (setq delete-called t)))) - (pearl-delete-saved-query "Q") + (pearl-delete-local-view "Q") (should-not delete-called) - (should-not (assoc "Q" pearl-saved-queries))))) + (should-not (assoc "Q" pearl-local-views))))) (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 + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :linear-view-id "ORPHAN-UUID")))) (last-message nil)) @@ -675,8 +675,8 @@ more prompts fire than answers were prepared for." ((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)) + (pearl-delete-local-view "Q") + (should-not (assoc "Q" pearl-local-views)) (should (string-match-p "ORPHAN-UUID" last-message)) (should (string-match-p "orphan" last-message))))) @@ -685,7 +685,7 @@ more prompts fire than answers were prepared for." 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 + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t) :linear-view-id "view-uuid")))) (delete-called-with nil)) @@ -698,13 +698,13 @@ that silently routes to the unlink branch and skips the API call." (lambda (vid cb) (setq delete-called-with vid) (funcall cb (list :success nil :error "permission denied"))))) - (pearl-delete-saved-query "Q") + (pearl-delete-local-view "Q") (should (equal "view-uuid" delete-called-with)) - (should (assoc "Q" pearl-saved-queries))))) + (should (assoc "Q" pearl-local-views))))) (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 + (let ((pearl-local-views (copy-tree '(("Q" :filter (:open t))))) (delete-called nil) (prompt-count 0)) @@ -714,10 +714,10 @@ that silently routes to the unlink branch and skips the API call." ((symbol-function 'message) (lambda (&rest _) nil)) ((symbol-function 'pearl--customview-delete-async) (lambda (_ _) (setq delete-called t)))) - (pearl-delete-saved-query "Q") + (pearl-delete-local-view "Q") (should (= 1 prompt-count)) (should-not delete-called) - (should-not (assoc "Q" pearl-saved-queries))))) + (should-not (assoc "Q" pearl-local-views))))) (provide 'test-pearl-saved-query-sync) ;;; test-pearl-saved-query-sync.el ends here diff --git a/tests/test-pearl-saved.el b/tests/test-pearl-saved.el index 88bf468..d5f14e0 100644 --- a/tests/test-pearl-saved.el +++ b/tests/test-pearl-saved.el @@ -1,4 +1,4 @@ -;;; test-pearl-saved.el --- Tests for saved queries + sort -*- lexical-binding: t; -*- +;;; test-pearl-saved.el --- Tests for local views + sort -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings @@ -19,8 +19,8 @@ ;;; Commentary: -;; Tests for local saved queries and the sort layer: the client-side -;; `--sort-issues', the sort->orderBy mapping, and `pearl-run-saved-query' +;; Tests for local views and the sort layer: the client-side +;; `--sort-issues', the sort->orderBy mapping, and `pearl-run-local-view' ;; threading the filter, source, and order through. ;;; Code: @@ -69,8 +69,8 @@ ;;; run-saved-query (ert-deftest test-pearl-run-saved-query-threads-filter-and-source () - "Running a saved query compiles its filter and renders with a sorted source." - (let ((pearl-saved-queries + "Running a local view compiles its filter and renders with a sorted source." + (let ((pearl-local-views '(("My bugs" :filter (:labels ("bug") :open t) :sort priority :order asc))) (built nil) (rendered-source nil) (order nil)) (cl-letf (((symbol-function 'pearl--build-issue-filter) @@ -80,7 +80,7 @@ (funcall cb (pearl--make-query-result 'ok :issues nil)))) ((symbol-function 'pearl--render-query-result) (lambda (_result source) (setq rendered-source source)))) - (pearl-run-saved-query "My bugs") + (pearl-run-local-view "My bugs") (should (equal '(:labels ("bug") :open t) built)) (should (eq 'priority (plist-get rendered-source :sort))) (should (eq 'asc (plist-get rendered-source :order))) @@ -89,8 +89,8 @@ (ert-deftest test-pearl-run-saved-query-unknown-errors () "An unknown saved-query name signals a user error." - (let ((pearl-saved-queries '(("Known" :filter (:open t))))) - (should-error (pearl-run-saved-query "Missing") :type 'user-error))) + (let ((pearl-local-views '(("Known" :filter (:open t))))) + (should-error (pearl-run-local-view "Missing") :type 'user-error))) ;;; render applies the source sort diff --git a/tests/test-pearl-views.el b/tests/test-pearl-views.el index 2f2adfd..a750ae0 100644 --- a/tests/test-pearl-views.el +++ b/tests/test-pearl-views.el @@ -20,7 +20,7 @@ ;;; Commentary: ;; Tests for reading and running Linear Custom Views: the cached views list, -;; the server-side `--query-view-async' run, `pearl-run-view', the view +;; the server-side `--query-view-async' run, `pearl-run-linear-view', the view ;; branch of refresh, and opening the active view in the browser. HTTP is ;; stubbed. @@ -75,7 +75,7 @@ (funcall cb (pearl--make-query-result 'ok :issues nil)))) ((symbol-function 'pearl--render-query-result) (lambda (_result source) (setq rendered-source source)))) - (pearl-run-view "My View") + (pearl-run-linear-view "My View") (should (string= "v1" ran-id)) (should (eq 'view (plist-get rendered-source :type))) (should (string= "v1" (plist-get rendered-source :id))) -- cgit v1.2.3