From e749badfd974de42fe8d7cdbe2bfa4fa3e0771b2 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 15 Jun 2026 23:53:57 -0500 Subject: fix(api): signal missing-key as user-error with mode-aware advice A missing API key raised a raw `error`, so it backtraced under `debug-on-error`, and the stale advice pointed at customizing `pearl-api-key`. Now `pearl--headers` signals a `user-error` and branches the advice by mode: accounts mode points at `pearl-accounts` and `pearl-switch-account`, legacy mode at `pearl-api-key` and `pearl-load-api-key-from-env`. The credential-source resolver's three failure paths become `user-error` too, since a bad credential is a config problem, not a bug. --- pearl.el | 17 ++++++++++------- tests/test-pearl-accounts.el | 7 +++++++ tests/test-pearl-config.el | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/pearl.el b/pearl.el index 522af9a..8540b9d 100644 --- a/pearl.el +++ b/pearl.el @@ -513,20 +513,20 @@ lookup -- when the key cannot be found or SOURCE is malformed." (`(:literal ,key) key) (`(:env ,var) (or (getenv var) - (error "No API key for account %s: environment variable %s is unset" - account var))) + (user-error "No API key for account %s: environment variable %s is unset" + account var))) (`(:auth-source . ,plist) (let* ((found (car (apply #'auth-source-search (append plist '(:require (:secret) :max 1))))) (secret (and found (plist-get found :secret)))) (cond ((null secret) - (error "No API key for account %s: auth-source found no matching entry" - account)) + (user-error "No API key for account %s: auth-source found no matching entry" + account)) ((functionp secret) (funcall secret)) (t secret)))) - (_ (error "No API key for account %s: unrecognized :api-key-source form" - account)))) + (_ (user-error "No API key for account %s: unrecognized :api-key-source form" + account)))) (defun pearl--resolve-account (name) "Resolve account NAME from `pearl-accounts' to a context plist. @@ -723,7 +723,10 @@ when it exists." (defun pearl--headers () "Return headers for Linear API requests." (unless pearl-api-key - (error "Linear API key not set. Use M-x customize-variable RET pearl-api-key")) + (user-error + (if pearl-accounts + "Linear API key not set: no active account key resolved. Check `pearl-accounts' and its credential source, then M-x pearl-switch-account" + "Linear API key not set. Set `pearl-api-key', or load it from the environment with M-x pearl-load-api-key-from-env"))) ;; For personal API keys, the format is: "Authorization: " ;; No "Bearer" prefix for personal API keys diff --git a/tests/test-pearl-accounts.el b/tests/test-pearl-accounts.el index 7994905..4de9c2f 100644 --- a/tests/test-pearl-accounts.el +++ b/tests/test-pearl-accounts.el @@ -78,6 +78,13 @@ "An unrecognized source tag errors rather than silently yielding nil." (should-error (pearl--resolve-api-key-source '(:bogus "x") "work") :type 'error)) +(ert-deftest test-pearl-resolve-api-key-source-missing-is-user-error () + "A missing credential is a user-config problem, surfaced as a `user-error'." + (cl-letf (((symbol-function 'auth-source-search) (lambda (&rest _) nil))) + (should-error (pearl--resolve-api-key-source + '(:auth-source :host "api.linear.app" :user "work") "work") + :type 'user-error))) + ;;; pearl--resolve-account (ert-deftest test-pearl-resolve-account-returns-full-context () diff --git a/tests/test-pearl-config.el b/tests/test-pearl-config.el index e83414c..7d6c838 100644 --- a/tests/test-pearl-config.el +++ b/tests/test-pearl-config.el @@ -40,6 +40,25 @@ (should (string-equal "lin_api_abc123" (cdr (assoc "Authorization" headers)))) (should (string-equal "application/json" (cdr (assoc "Content-Type" headers))))))) +(ert-deftest test-pearl-headers-no-key-is-user-error () + "A missing key signals a `user-error', not a raw error that backtraces." + (let ((pearl-api-key nil) (pearl-accounts nil)) + (should-error (pearl--headers) :type 'user-error))) + +(ert-deftest test-pearl-headers-no-key-legacy-advises-api-key () + "Without accounts configured, the advice names `pearl-api-key', not customize." + (let ((pearl-api-key nil) (pearl-accounts nil)) + (let ((err (should-error (pearl--headers) :type 'user-error))) + (should (string-match-p "pearl-api-key" (error-message-string err))) + (should-not (string-match-p "customize" (error-message-string err)))))) + +(ert-deftest test-pearl-headers-no-key-accounts-mode-advises-accounts () + "In accounts mode the missing-key advice points at the account setup." + (let ((pearl-api-key nil) + (pearl-accounts '(("work" :api-key-source (:literal "k") :org-file "/tmp/w.org")))) + (let ((err (should-error (pearl--headers) :type 'user-error))) + (should (string-match-p "account" (error-message-string err)))))) + ;;; pearl-toggle-debug (ert-deftest test-pearl-toggle-debug-flips-from-nil () -- cgit v1.2.3