branch: elpa/cider
commit 547f8bce1b8d8afdcc361d76978440f2d838a0b8
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>

    Add cider-doctor, a setup diagnostics command
    
    M-x cider-doctor runs a series of sanity checks against the current
    Emacs setup and, when a REPL is connected, the active nREPL session,
    then renders a copy-pasteable report. The goal is to catch the common
    "CIDER won't start" and "feature X is silently dead" problems without a
    round of back-and-forth on the issue tracker.
    
    Offline checks cover the Emacs and CIDER versions, the Clojure major
    mode, hard dependencies, build tools on the executable path, the classic
    GUI-Emacs PATH mismatch, stale byte-compiled files, and leftover
    obsolete config. Online checks reuse the existing compatibility helpers
    to report the nREPL, cider-nrepl and Clojure versions, plus a probe of
    the middleware ops that back major features.
---
 CHANGELOG.md                                |   1 +
 doc/modules/ROOT/pages/troubleshooting.adoc |  17 ++
 lisp/cider-doctor.el                        | 361 ++++++++++++++++++++++++++++
 lisp/cider.el                               |   1 +
 test/cider-doctor-tests.el                  | 176 ++++++++++++++
 5 files changed, 556 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b1165ff55..7c0fd58842 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### New features
 
+- [#4094](https://github.com/clojure-emacs/cider/pull/4094): Add 
`cider-doctor`, which checks your Emacs setup (and the active nREPL session, if 
any) for common problems and shows a copy-pasteable report - 
Emacs/CIDER/dependency versions, build tools on the path, stale byte-code, 
leftover obsolete config, and cider-nrepl/nREPL/Clojure compatibility.
 - Render fetched `text/html` content as formatted text (via `shr`) in the REPL 
and the rich-content popup, and make the URL of an external-content result a 
clickable link that opens in the browser (next to its `[show content]` button). 
Remote images in that HTML are not fetched, so rendering a result never makes a 
network request on its own (only inline `data:` images render).
 - Honor content types for interactive evaluations too 
([#2476](https://github.com/clojure-emacs/cider/issues/2476)): a 
`cider-eval-last-sexp` returning an image can now render it, per the new 
`cider-eval-rich-content-destination` - `inline` (the default, in the result 
overlay at point), `repl` (like results of forms typed at the prompt, with the 
`[show content]` button for external references), `popup` (the `*cider-result*` 
buffer) or `nil` (plain values, the previous behavior).
 - Add a transient menu to the debugger (`cider-debug-menu`, bound to `?` 
during a debug session), grouping all the debugger's single-key commands; they 
are also proper named commands now (e.g. `cider-debug-next`, 
`cider-debug-quit`), so they can be invoked via `M-x` as well.
diff --git a/doc/modules/ROOT/pages/troubleshooting.adoc 
b/doc/modules/ROOT/pages/troubleshooting.adoc
index 4bb0c2a134..0be23b8826 100644
--- a/doc/modules/ROOT/pages/troubleshooting.adoc
+++ b/doc/modules/ROOT/pages/troubleshooting.adoc
@@ -12,6 +12,23 @@ Generally, it's not a bad idea to configure Emacs to spit 
the backtrace on error
 (instead of just logging the error in the `+*Messages*+` buffer). You can 
toggle
 this behavior by using kbd:[M-x] `toggle-debug-on-error`.
 
+== Diagnosing your setup with cider-doctor
+
+Before digging deeper, run kbd:[M-x] `cider-doctor`. It checks your setup
+for the most common problems and shows the results in a `+*cider-doctor*+`
+buffer.
+
+Some checks run without a connection (the Emacs and CIDER versions, the
+Clojure major mode, CIDER's dependencies, the build tools on your
+executable path, stale byte-compiled files, and leftover obsolete
+configuration). When a REPL is connected, `cider-doctor` also reports the
+nREPL, `cider-nrepl` and Clojure versions and probes the middleware ops
+that back the major features - so a missing feature shows up as a named
+missing op rather than a mystery.
+
+TIP: The report is meant to be copied into a bug report, which usually saves a
+round of back-and-forth about your environment.
+
 == Debugging CIDER commands
 
 Emacs features a super powerful built-in
diff --git a/lisp/cider-doctor.el b/lisp/cider-doctor.el
new file mode 100644
index 0000000000..ab27a420b9
--- /dev/null
+++ b/lisp/cider-doctor.el
@@ -0,0 +1,361 @@
+;;; cider-doctor.el --- Diagnose a CIDER setup           -*- lexical-binding: 
t; -*-
+
+;; Copyright © 2026  Bozhidar Batsov and CIDER contributors
+
+;; Author: Bozhidar Batsov <[email protected]>
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; `cider-doctor' runs a series of sanity checks against the current Emacs
+;; setup and (when connected) the active nREPL session, then renders a
+;; copy-pasteable report.  The intent is to catch the most common "CIDER
+;; won't start" and "feature X is dead" problems without a round of
+;; back-and-forth on the issue tracker.
+;;
+;; Checks come in two flavors:
+;;
+;; - offline: environment sanity that needs no connection (Emacs/CIDER
+;;   versions, dependencies, build tools on PATH, byte-compile staleness,
+;;   leftover obsolete config);
+;; - online: connection health that reuses the compatibility checks already
+;;   living in `cider-connection' and `cider-session'.
+
+;;; Code:
+
+(require 'seq)
+(require 'subr-x)
+
+(require 'cider-util)
+
+;;; Check results
+;;
+;; A check returns a plist with these keys:
+;;   :status  - one of `ok', `warn', `error', `info'
+;;   :label   - short headline string
+;;   :detail  - optional longer explanation (string or nil)
+;;   :section - optional manual anchor (see `cider--manual-button')
+;;   :hint    - optional one-line remediation string
+;;
+;; A check function returns either a single plist or a list of them.
+
+(defconst cider-doctor--status-glyphs
+  '((ok    . "✓")
+    (warn  . "⚠")
+    (error . "✗")
+    (info  . "•"))
+  "Mapping of check status to the glyph shown in the report.")
+
+(defun cider-doctor--result (status label &rest keys)
+  "Build a check result plist with STATUS and LABEL.
+KEYS may include :detail, :section and :hint."
+  (append (list :status status :label label) keys))
+
+;;; Offline checks
+
+(defun cider-doctor--check-emacs-version ()
+  "Check that Emacs meets CIDER's minimum version."
+  (if (version< emacs-version "28")
+      (cider-doctor--result 'error
+                            (format "Emacs %s is too old" emacs-version)
+                            :detail "CIDER requires Emacs 28 or newer."
+                            :section "basics/installation.html")
+    (cider-doctor--result 'ok (format "Emacs %s" emacs-version))))
+
+(defun cider-doctor--check-cider-version ()
+  "Report CIDER's version and how it was installed."
+  (cider-doctor--result 'info (format "CIDER %s" (cider--version))
+                        :detail (if-let* ((dir (cider-doctor--library-dir 
"cider")))
+                                    (format "loaded from %s" dir)
+                                  "load path unknown")))
+
+(defun cider-doctor--check-clojure-mode ()
+  "Check which Clojure major mode is available.
+Having both `clojure-mode' and `clojure-ts-mode' installed is fine, but
+worth surfacing since it is a common source of confusion."
+  (let ((cm (featurep 'clojure-mode))
+        (cts (featurep 'clojure-ts-mode)))
+    (cond
+     ((and cm cts)
+      (cider-doctor--result 'info "clojure-mode and clojure-ts-mode both 
loaded"
+                            :detail "CIDER supports either; be sure your hooks 
target the mode you actually use."))
+     (cm (cider-doctor--result 'ok "clojure-mode loaded"))
+     (cts (cider-doctor--result 'ok "clojure-ts-mode loaded"))
+     (t (cider-doctor--result 'error "No Clojure major mode loaded"
+                              :detail "Install clojure-mode or 
clojure-ts-mode."
+                              :section "basics/installation.html")))))
+
+(defun cider-doctor--check-dependencies ()
+  "Check that each hard dependency is loaded, reporting its version."
+  (mapcar
+   (lambda (dep)
+     (if (featurep dep)
+         (cider-doctor--result 'ok
+                               (format "%s %s" dep
+                                       (or (cider-doctor--package-version dep) 
"(version unknown)")))
+       (cider-doctor--result 'error (format "%s is not loaded" dep))))
+   '(clojure-mode compat parseedn queue spinner sesman transient)))
+
+(defun cider-doctor--check-build-tools ()
+  "Check for the common Clojure build tools on the executable search path.
+Absence is not fatal on its own (you might only use one), so a missing
+tool is reported as info rather than an error."
+  (mapcar
+   (lambda (tool)
+     (if-let* ((path (executable-find tool)))
+         (cider-doctor--result 'ok (format "%s found" tool) :detail path)
+       (cider-doctor--result 'info (format "%s not on PATH" tool))))
+   '("lein" "clojure" "clj" "bb" "node")))
+
+(defun cider-doctor--check-exec-path ()
+  "Flag the classic GUI-Emacs PATH mismatch.
+When Emacs was launched from a GUI on macOS it often inherits a minimal
+PATH, so jack-in fails to find `clojure' or `lein' even though they work
+in a shell."
+  (if (or (not (memq window-system '(ns mac)))
+          (seq-some #'executable-find '("clojure" "clj" "lein" "bb")))
+      (cider-doctor--result 'ok "exec-path can find a Clojure build tool")
+    (cider-doctor--result 'warn "No Clojure build tool on exec-path"
+                          :detail "GUI Emacs may not inherit your shell PATH."
+                          :hint "Try the exec-path-from-shell package."
+                          :section "basics/installation.html")))
+
+(defun cider-doctor--check-byte-compilation ()
+  "Warn if CIDER's source files are newer than their byte-compiled output.
+Stale .elc files lead to baffling behavior after an upgrade."
+  (if-let* ((el (locate-library "cider.el"))
+            (elc (concat el "c")))
+      (if (and (file-exists-p elc)
+               (file-newer-than-file-p el elc))
+          (cider-doctor--result 'warn "CIDER byte-compiled files are stale"
+                                :detail "cider.el is newer than cider.elc."
+                                :hint "Recompile CIDER (e.g. M-x 
byte-recompile-directory).")
+        (cider-doctor--result 'ok "Byte-compiled files are up to date"))
+    (cider-doctor--result 'info "Running from source (not byte-compiled)")))
+
+(defun cider-doctor--check-obsolete-config ()
+  "Detect obsolete CIDER variables the user still has customized.
+Leftover deprecated settings are a frequent cause of upgrade surprises."
+  (let (found)
+    (mapatoms
+     (lambda (sym)
+       (when (and (get sym 'byte-obsolete-variable)
+                  (get sym 'saved-value)
+                  (string-match-p "\\`\\(cider\\|nrepl\\)-" (symbol-name sym)))
+         (push sym found))))
+    (if found
+        (cider-doctor--result 'warn
+                              (format "%d obsolete option(s) still customized" 
(length found))
+                              :detail (mapconcat #'symbol-name (sort found 
#'string<) ", ")
+                              :hint "Migrate these to their replacements; see 
each variable's docstring.")
+      (cider-doctor--result 'ok "No obsolete options customized"))))
+
+;;; Online checks (active session)
+;;
+;; These reuse the compatibility helpers in `cider-connection' /
+;; `cider-session'.  They are only run when there's a live REPL.
+
+(declare-function cider-current-repl "cider-session")
+(declare-function cider--clojure-version "cider-session")
+(declare-function cider--nrepl-version "cider-session")
+(declare-function cider--java-version "cider-session")
+(declare-function cider-nrepl-op-supported-p "cider-client")
+(declare-function cider--compatible-middleware-version-p "cider-connection")
+(declare-function nrepl-aux-info "nrepl-client")
+(declare-function nrepl-dict-get "nrepl-dict")
+(defvar cider-required-nrepl-version)
+(defvar cider-minimum-clojure-version)
+(defvar cider-required-middleware-version)
+
+(defconst cider-doctor--expected-ops
+  '(("cider/info"                   . "symbol info, docs, source")
+    ("cider/complete"               . "code completion")
+    ("cider/eldoc"                  . "eldoc")
+    ("cider/classpath"              . "classpath and navigation")
+    ("cider/macroexpand"            . "macroexpansion")
+    ("cider/ns-path"                . "namespace navigation")
+    ("cider/analyze-last-stacktrace" . "error rendering")
+    ("cider/test-all"              . "test runner")
+    ("cider/fn-refs"               . "xref / find references")
+    ("cider/out-subscribe"         . "server output streaming"))
+  "Representative cider-nrepl ops mapped to the feature they enable.
+Probed by `cider-doctor--check-ops' to explain why a feature might be dead.")
+
+(defun cider-doctor--online-p ()
+  "Return non-nil when there's an active REPL to inspect."
+  (ignore-errors (cider-current-repl)))
+
+(defun cider-doctor--check-nrepl-version ()
+  "Check the connected nREPL server's version."
+  (if-let* ((ver (cider--nrepl-version)))
+      (if (version< ver cider-required-nrepl-version)
+          (cider-doctor--result 'error (format "nREPL %s is too old" ver)
+                                :detail (format "CIDER requires nREPL %s or 
newer."
+                                                cider-required-nrepl-version)
+                                :section 
"troubleshooting.html#warning-saying-you-have-to-use-newer-nrepl")
+        (cider-doctor--result 'ok (format "nREPL %s" ver)))
+    (cider-doctor--result 'warn "nREPL version unknown")))
+
+(defun cider-doctor--check-clojure-runtime ()
+  "Check the connected runtime's Clojure version."
+  (if-let* ((ver (cider--clojure-version))
+            (ver (car (split-string ver "-"))))
+      (if (version< ver cider-minimum-clojure-version)
+          (cider-doctor--result 'error (format "Clojure %s is unsupported" ver)
+                                :detail (format "Minimum supported version is 
%s."
+                                                cider-minimum-clojure-version)
+                                :section 
"basics/installation.html#prerequisites")
+        (cider-doctor--result 'ok (format "Clojure %s" ver)))
+    (cider-doctor--result 'info "Clojure version unknown (non-JVM runtime?)")))
+
+(defun cider-doctor--check-middleware ()
+  "Check that cider-nrepl is present and version-compatible.
+Reuses `cider--compatible-middleware-version-p' so the doctor and the
+connection-time warning agree on what \"compatible\" means."
+  (let* ((info (nrepl-aux-info "cider-version" (cider-current-repl)))
+         (ver (and info (nrepl-dict-get info "version-string"))))
+    (cond
+     ((null ver)
+      (cider-doctor--result 'error "cider-nrepl middleware not detected"
+                            :detail "Most CIDER features need cider-nrepl; 
only bare nREPL was found."
+                            :section 
"troubleshooting.html#cider-complains-of-the-cider-nrepl-version"))
+     ((not (cider--compatible-middleware-version-p 
cider-required-middleware-version ver))
+      (cider-doctor--result 'warn (format "cider-nrepl %s may be incompatible" 
ver)
+                            :detail (format "CIDER %s expects cider-nrepl %s."
+                                            (cider--version) 
cider-required-middleware-version)
+                            :section 
"troubleshooting.html#cider-complains-of-the-cider-nrepl-version"))
+     (t (cider-doctor--result 'ok (format "cider-nrepl %s" ver))))))
+
+(defun cider-doctor--check-ops ()
+  "Probe `cider-doctor--expected-ops' and report any that are missing.
+A missing op is the usual reason a feature is silently dead, so the
+detail names the affected feature."
+  (let ((repl (cider-current-repl))
+        missing)
+    (pcase-dolist (`(,op . ,feature) cider-doctor--expected-ops)
+      (unless (cider-nrepl-op-supported-p op repl 'skip-ensure)
+        (push (format "%s (%s)" op feature) missing)))
+    (if missing
+        (cider-doctor--result 'warn
+                              (format "%d expected op(s) unavailable" (length 
missing))
+                              :detail (mapconcat #'identity (nreverse missing) 
", ")
+                              :hint "Usually means an outdated or partial 
cider-nrepl; update it."
+                              :section 
"troubleshooting.html#cider-complains-of-the-cider-nrepl-version")
+      (cider-doctor--result 'ok "All probed middleware ops available"))))
+
+(defun cider-doctor--online-checks ()
+  "Run the checks that need an active connection."
+  (list (cider-doctor--check-nrepl-version)
+        (cider-doctor--check-middleware)
+        (cider-doctor--check-clojure-runtime)
+        (cider-doctor--check-ops)))
+
+;;; Assembly and rendering
+
+(defun cider-doctor--offline-checks ()
+  "Run the environment checks that need no connection."
+  (let (results)
+    (dolist (check '(cider-doctor--check-emacs-version
+                     cider-doctor--check-cider-version
+                     cider-doctor--check-clojure-mode
+                     cider-doctor--check-dependencies
+                     cider-doctor--check-build-tools
+                     cider-doctor--check-exec-path
+                     cider-doctor--check-byte-compilation
+                     cider-doctor--check-obsolete-config))
+      (let ((r (funcall check)))
+        ;; a check may return a single result or a list of them
+        (if (plist-member r :status)
+            (push r results)
+          (setq results (append (reverse r) results)))))
+    (nreverse results)))
+
+(defun cider-doctor--insert-result (result)
+  "Insert a single check RESULT into the current buffer."
+  (let ((glyph (cdr (assq (plist-get result :status) 
cider-doctor--status-glyphs)))
+        (detail (plist-get result :detail))
+        (hint (plist-get result :hint))
+        (section (plist-get result :section)))
+    (insert (format "  %s  %s\n" glyph (plist-get result :label)))
+    (when detail
+      (insert (format "       %s\n" detail)))
+    (when hint
+      (insert (format "       → %s\n" hint)))
+    (when section
+      (insert (format "       %s\n" (cider--manual-button "Manual" 
section))))))
+
+(defun cider-doctor--insert-section (title results)
+  "Insert a section headed TITLE listing RESULTS."
+  (insert (format "%s\n\n" title))
+  (mapc #'cider-doctor--insert-result results)
+  (insert "\n"))
+
+;;;###autoload
+(defun cider-doctor ()
+  "Diagnose the current CIDER setup and show a report.
+Runs offline environment checks and, when a REPL is connected, a set of
+connection-health checks.  The resulting buffer is meant to be pasted
+into bug reports."
+  (interactive)
+  (let ((buffer (cider-popup-buffer "*cider-doctor*" 'select)))
+    (with-current-buffer buffer
+      (let ((inhibit-read-only t))
+        (erase-buffer)
+        (insert "CIDER Doctor\n")
+        (insert "============\n\n")
+        (cider-doctor--insert-section "Environment" 
(cider-doctor--offline-checks))
+        (if (cider-doctor--online-p)
+            (cider-doctor--insert-section "Connection" 
(cider-doctor--online-checks))
+          (insert "Connection\n\nNo active REPL; connection checks 
skipped.\n\n"))
+        (goto-char (point-min))))
+    buffer))
+
+;;; Helpers
+
+(defun cider-doctor--library-dir (library)
+  "Return the directory LIBRARY was loaded from, or nil."
+  (when-let* ((file (locate-library library)))
+    (file-name-directory file)))
+
+(defun cider-doctor--package-version (feature)
+  "Return a best-effort version string for FEATURE, or nil.
+Tries, in order: package.el metadata (covers MELPA and similar
+installs), a `FEATURE-version' variable (e.g. `transient-version'), and
+finally the Version header of the feature's source file (covers
+built-in libraries like `seq')."
+  (or
+   (when-let* ((desc (car (alist-get feature package-alist))))
+     (package-version-join (package-desc-version desc)))
+   (let ((var (intern-soft (format "%s-version" feature))))
+     (and var (boundp var)
+          (let ((val (symbol-value var)))
+            (and (stringp val) val))))
+   (ignore-errors
+     (require 'lisp-mnt)
+     (require 'find-func)
+     (lm-with-file (find-library-name (symbol-name feature))
+       (lm-header "version")))))
+
+(declare-function cider-popup-buffer "cider-popup")
+(declare-function package-desc-version "package")
+(declare-function package-version-join "package")
+(declare-function find-library-name "find-func")
+(declare-function lm-header "lisp-mnt")
+(declare-function lm-with-file "lisp-mnt")
+(defvar package-alist)
+
+(provide 'cider-doctor)
+
+;;; cider-doctor.el ends here
diff --git a/lisp/cider.el b/lisp/cider.el
index 9f6134ebdb..301008c50a 100644
--- a/lisp/cider.el
+++ b/lisp/cider.el
@@ -101,6 +101,7 @@
 (require 'cider-mode)
 (require 'cider-common)
 (require 'cider-debug)
+(require 'cider-doctor)
 (require 'cider-util)
 
 (require 'tramp-sh)
diff --git a/test/cider-doctor-tests.el b/test/cider-doctor-tests.el
new file mode 100644
index 0000000000..ab78261545
--- /dev/null
+++ b/test/cider-doctor-tests.el
@@ -0,0 +1,176 @@
+;; -*- lexical-binding: t; -*-
+;;; cider-doctor-tests.el
+
+;; Copyright © 2026 Bozhidar Batsov and CIDER contributors
+
+;; Author: Bozhidar Batsov <[email protected]>
+
+;; This file is NOT part of GNU Emacs.
+
+;; 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 `http://www.gnu.org/licenses/'.
+
+;;; Commentary:
+
+;; This file is part of CIDER
+
+;;; Code:
+
+(require 'buttercup)
+(require 'nrepl-dict)
+(require 'cider-connection)
+(require 'cider-doctor)
+
+;; Please, for each `describe', ensure there's an `it' block, so that its 
execution is visible in CI.
+
+(describe "cider-doctor--result"
+  (it "builds a plist from the status and label"
+    (let ((r (cider-doctor--result 'ok "all good")))
+      (expect (plist-get r :status) :to-be 'ok)
+      (expect (plist-get r :label) :to-equal "all good")))
+
+  (it "passes extra keys through"
+    (let ((r (cider-doctor--result 'warn "hmm" :detail "why" :hint "do this")))
+      (expect (plist-get r :detail) :to-equal "why")
+      (expect (plist-get r :hint) :to-equal "do this"))))
+
+(describe "cider-doctor--check-emacs-version"
+  (it "errors on an Emacs older than 28"
+    (let ((emacs-version "27.2"))
+      (expect (plist-get (cider-doctor--check-emacs-version) :status)
+              :to-be 'error)))
+
+  (it "passes on a supported Emacs"
+    (let ((emacs-version "30.2"))
+      (expect (plist-get (cider-doctor--check-emacs-version) :status)
+              :to-be 'ok))))
+
+(describe "cider-doctor--check-clojure-mode"
+  (it "is ok when only clojure-mode is loaded"
+    (spy-on 'featurep :and-call-fake (lambda (f &rest _) (eq f 'clojure-mode)))
+    (expect (plist-get (cider-doctor--check-clojure-mode) :status) :to-be 'ok))
+
+  (it "is informational when both major modes are loaded"
+    (spy-on 'featurep :and-call-fake
+            (lambda (f &rest _) (memq f '(clojure-mode clojure-ts-mode))))
+    (expect (plist-get (cider-doctor--check-clojure-mode) :status) :to-be 
'info))
+
+  (it "errors when no Clojure major mode is loaded"
+    (spy-on 'featurep :and-return-value nil)
+    (expect (plist-get (cider-doctor--check-clojure-mode) :status) :to-be 
'error)))
+
+(describe "cider-doctor--check-dependencies"
+  (it "returns one result per dependency"
+    (let ((results (cider-doctor--check-dependencies)))
+      (expect (length results) :to-equal 7)
+      (expect (seq-every-p (lambda (r) (plist-member r :status)) results)
+              :to-be-truthy)))
+
+  (it "errors for a dependency that is not loaded"
+    (spy-on 'featurep :and-return-value nil)
+    (let ((results (cider-doctor--check-dependencies)))
+      (expect (seq-every-p (lambda (r) (eq (plist-get r :status) 'error)) 
results)
+              :to-be-truthy))))
+
+(describe "cider-doctor--check-build-tools"
+  (it "reports ok for a tool found on PATH and info otherwise"
+    (spy-on 'executable-find :and-call-fake
+            (lambda (tool &rest _) (when (equal tool "clojure") 
"/usr/bin/clojure")))
+    (let* ((results (cider-doctor--check-build-tools))
+           (statuses (mapcar (lambda (r) (plist-get r :status)) results)))
+      (expect (memq 'ok statuses) :to-be-truthy)
+      (expect (memq 'info statuses) :to-be-truthy))))
+
+(describe "cider-doctor--check-exec-path"
+  (it "is ok when a build tool is reachable"
+    (spy-on 'executable-find :and-return-value "/usr/bin/clojure")
+    (expect (plist-get (cider-doctor--check-exec-path) :status) :to-be 'ok))
+
+  (it "warns on a GUI Emacs with no build tool on exec-path"
+    (spy-on 'executable-find :and-return-value nil)
+    (let ((window-system 'ns))
+      (expect (plist-get (cider-doctor--check-exec-path) :status) :to-be 
'warn))))
+
+(describe "cider-doctor--check-obsolete-config"
+  (it "flags an obsolete cider option the user has saved"
+    (let ((sym 'cider-doctor-test-obsolete-var))
+      (unwind-protect
+          (progn
+            (put sym 'byte-obsolete-variable '(nil nil "1.0"))
+            (put sym 'saved-value '(t))
+            (let ((result (cider-doctor--check-obsolete-config)))
+              (expect (plist-get result :status) :to-be 'warn)
+              (expect (plist-get result :detail)
+                      :to-match (symbol-name sym))))
+        (put sym 'byte-obsolete-variable nil)
+        (put sym 'saved-value nil)))))
+
+(describe "cider-doctor--offline-checks"
+  (it "returns a flat list of result plists"
+    (let ((results (cider-doctor--offline-checks)))
+      (expect (seq-every-p (lambda (r) (plist-member r :status)) results)
+              :to-be-truthy))))
+
+(describe "cider-doctor--check-nrepl-version"
+  (it "errors on an nREPL older than the required version"
+    (spy-on 'cider--nrepl-version :and-return-value "0.5.0")
+    (expect (plist-get (cider-doctor--check-nrepl-version) :status) :to-be 
'error))
+
+  (it "is ok on a recent enough nREPL"
+    (spy-on 'cider--nrepl-version :and-return-value "1.0.0")
+    (expect (plist-get (cider-doctor--check-nrepl-version) :status) :to-be 
'ok)))
+
+(describe "cider-doctor--check-clojure-runtime"
+  (it "errors on an unsupported Clojure version"
+    (spy-on 'cider--clojure-version :and-return-value "1.9.0")
+    (expect (plist-get (cider-doctor--check-clojure-runtime) :status) :to-be 
'error))
+
+  (it "strips qualifiers before comparing"
+    (spy-on 'cider--clojure-version :and-return-value "1.12.0-master-SNAPSHOT")
+    (expect (plist-get (cider-doctor--check-clojure-runtime) :status) :to-be 
'ok)))
+
+(describe "cider-doctor--check-middleware"
+  (it "errors when no cider-nrepl version is reported"
+    (spy-on 'cider-current-repl :and-return-value nil)
+    (spy-on 'nrepl-aux-info :and-return-value nil)
+    (expect (plist-get (cider-doctor--check-middleware) :status) :to-be 
'error))
+
+  (it "is ok on a compatible middleware version"
+    (spy-on 'cider-current-repl :and-return-value nil)
+    (spy-on 'nrepl-aux-info :and-return-value
+            (nrepl-dict "version-string" cider-required-middleware-version))
+    (expect (plist-get (cider-doctor--check-middleware) :status) :to-be 'ok))
+
+  (it "warns on an incompatible middleware version"
+    (spy-on 'cider-current-repl :and-return-value nil)
+    (spy-on 'nrepl-aux-info :and-return-value
+            (nrepl-dict "version-string" "0.1.0"))
+    (expect (plist-get (cider-doctor--check-middleware) :status) :to-be 
'warn)))
+
+(describe "cider-doctor--check-ops"
+  (it "is ok when every probed op is supported"
+    (spy-on 'cider-current-repl :and-return-value nil)
+    (spy-on 'cider-nrepl-op-supported-p :and-return-value t)
+    (expect (plist-get (cider-doctor--check-ops) :status) :to-be 'ok))
+
+  (it "warns and names the feature when an op is missing"
+    (spy-on 'cider-current-repl :and-return-value nil)
+    (spy-on 'cider-nrepl-op-supported-p :and-call-fake
+            (lambda (op &rest _) (not (equal op "cider/fn-refs"))))
+    (let ((result (cider-doctor--check-ops)))
+      (expect (plist-get result :status) :to-be 'warn)
+      (expect (plist-get result :detail) :to-match "xref"))))
+
+(provide 'cider-doctor-tests)
+
+;;; cider-doctor-tests.el ends here

Reply via email to