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