Subject: bug#75327: 31.0.50; ERC 5.6.1-git: M-TAB not autocorrecting with erc-spelling module and flyspell

From: "J.P." <[email-masked]>
Date: Sun, 05 Jan 2025 11:33:53 -0800

Trevor Arjeski <[email-masked]> writes:

> When using the erc-spelling module, pressing M-TAB to autocorrect a word
> does not trigger autocorrection, but using C-. or C-; does, even when
> `flyspell-use-meta-tab' is enabled.
>
> Minimal config:
>
> (use-package erc
>   :config
>   (setopt erc-modules '(spelling))
>   (erc-spelling-mode))
>
> 1. Open an erc buffer
> 2. Type 'thier' and hit M-TAB
> 3. Notice it passes through to ispell - perhaps due to the command
> passthrough in `flyspell-auto-correct-word'.

Regarding this "passthrough" behavior with "M-TAB" vs "C-.", I think
what's happening is that when you type "C-.", the call to

  (let (flyspell-mode) (key-binding (kbd "C-.")))

in `flyspell-auto-correct-word' returns nil because there's no global or
`erc-mode' binding for that key, so `flyspell-word' ultimately runs
further down in the function body. And so too does the predicate in
question, `erc-spelling-flyspell-verify', with `flyspell-word' bound as
a dynamic variable. By contrast,

  (let (flyspell-mode) (key-binding (kbd "C-M-i"))) ; or "M-TAB"

yields a non-nil result, namely `ispell-complete-word', because that's
the binding in `erc-mode-map'. And if you were to remove that with
`local-unset-key', you'd observe `flyspell-auto-correct-word' deferring
to `complete-symbol', which appeals to CAPF and `pcomplete'-related
machinery in ERC buffers.

> Setting `flyspell-generic-check-word-predicate' to nil in the erc buffer
> resolves the issue, but we lose verification that is done in
> `erc-spelling-flyspell-verify'.

Right, that basically nullifies the integration.

> Here is a patch to resolve this issue:
>
> From 3d32aea96cc6ec020e35901d35f1d02994912215 Mon Sep 17 00:00:00 2001
> From: Trevor Arjeski <[email-masked]>
> Date: Sat, 4 Jan 2025 08:45:29 +0300
> Subject: [PATCH] erc: bug#75327 fix flyspell verify
>
> Slightly refactored `erc-spelling-flyspell-verify' to use
> `flyspell-get-word', which in turn, allows M-TAB to call
> `flyspell-auto-correct-word'.

In the future, please use the GNU ChangeLog style for commit messages,
as described in CONTRIBUTE.

> ---
>  lisp/erc/erc-spelling.el | 29 +++++++++++++----------------
>  1 file changed, 13 insertions(+), 16 deletions(-)
>
> diff --git a/lisp/erc/erc-spelling.el b/lisp/erc/erc-spelling.el
> index 01e587af368..0bfb20fce2e 100644
> --- a/lisp/erc/erc-spelling.el
> +++ b/lisp/erc/erc-spelling.el
> @@ -92,22 +92,19 @@ erc-spelling-unhighlight-word
>  
>  (defun erc-spelling-flyspell-verify ()
>    "Flyspell only the input line, nothing else."
[...]
> +  (when-let* (((>= (point) erc-input-marker))
> +              (word-data (flyspell-get-word)))

While calling `flyspell-get-word' definitely seems to work and is
probably harmless, I'm not sure Flyspell's authors intended these
predicates to know about the current candidate, much less modify
existing overlays based on that info (both preexisting transgressions
and obviously not your doing).

There's also the small matter of `flyspell-get-word' running twice in
quick succession during the course of a normal `flyspell-word' call.
Again, probably harmless, but if there's a more idiomatic way to get at
this, we should probably abide (see attached), just in case future
flyspell.el hackers decide to assume all "consumers" of its API are well
behaved citizens.

> +    (cond
> +     ;; don't spell-check names of users
> +     ((and erc-channel-users
> +           (erc-get-channel-user (car word-data)))
> +      (erc-spelling-unhighlight-word word-data)
> +      nil)
> +     ;; if '/' occurs before the word, don't spell-check it
> +     ((eq (char-before (nth 1 word-data)) ?/)
> +      (erc-spelling-unhighlight-word word-data)
> +      nil)
> +     (t t))))
>  
>  (put 'erc-mode
>       'flyspell-mode-predicate

Please see the attached iteration, which has a few more changes than
your v1 patch. If we go with something similar, that would, by my
uninformed figuring, put you right at the copyright-exempt limit of
12-ish nontrivial lines (though we may be pushing the envelope a tad).

Thanks.

P.S. Have you already (or will you) put in for a copyright assignment?

>From ce275256955f20b2932ae50b9019394c8758b970 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Sun, 5 Jan 2025 09:39:31 -0800
Subject: [PATCH 0/2] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (1):
  [5.6.1] Declare unused functions in erc-spelling obsolete

Trevor Arjeski (1):
  [5.6.1] Improve use of flyspell's API in erc-spelling

 etc/ERC-NEWS                                  |  5 +
 lisp/erc/erc-spelling.el                      | 24 ++++-
 test/lisp/erc/erc-scenarios-spelling.el       | 95 +++++++++++++++++++
 .../erc/resources/spelling/auto-correct.eld   | 35 +++++++
 4 files changed, 157 insertions(+), 2 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-spelling.el
 create mode 100644 test/lisp/erc/resources/spelling/auto-correct.eld

Interdiff:
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index f79d45b94b6..d491c3e5132 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -96,6 +96,11 @@ Although this has always been the case, string values are now more
 likely to be seen because ERC no longer coerces service names to port
 numbers.
 
+*** The 'spelling' module makes better use of Flyspell's API.
+As a consequence, the library functions 'erc-spelling-flyspell-verify'
+and 'erc-spelling-unhighlight-word' are now unused and have been marked
+obsolete.
+
 
 * Changes in ERC 5.6
 
diff --git a/lisp/erc/erc-spelling.el b/lisp/erc/erc-spelling.el
index 0bfb20fce2e..f0a45683f4d 100644
--- a/lisp/erc/erc-spelling.el
+++ b/lisp/erc/erc-spelling.el
@@ -48,6 +48,7 @@ spelling
        (erc-spelling-init (current-buffer)))))
   ((remove-hook 'erc-connect-pre-hook #'erc-spelling-init)
    (dolist (buffer (erc-buffer-list))
+     (remove-hook 'flyspell-incorrect-hook #'erc-spelling--flyspell-check t)
      (with-current-buffer buffer (flyspell-mode 0)))))
 
 (defcustom erc-spelling-dictionaries nil
@@ -78,7 +79,7 @@ erc-spelling-init
               (if dicts
                   (cadr (car dicts))
                 (erc-with-server-buffer ispell-local-dictionary)))))
-    (setq flyspell-generic-check-word-predicate #'erc-spelling-flyspell-verify)
+    (add-hook 'flyspell-incorrect-hook #'erc-spelling--flyspell-check 20 t)
     (flyspell-mode 1)))
 
 (defun erc-spelling-unhighlight-word (word)
@@ -92,23 +93,45 @@ erc-spelling-unhighlight-word
 
 (defun erc-spelling-flyspell-verify ()
   "Flyspell only the input line, nothing else."
-  (when-let* (((>= (point) erc-input-marker))
-              (word-data (flyspell-get-word)))
-    (cond
-     ;; don't spell-check names of users
-     ((and erc-channel-users
-           (erc-get-channel-user (car word-data)))
-      (erc-spelling-unhighlight-word word-data)
-      nil)
-     ;; if '/' occurs before the word, don't spell-check it
-     ((eq (char-before (nth 1 word-data)) ?/)
-      (erc-spelling-unhighlight-word word-data)
-      nil)
-     (t t))))
+  (declare (obsolete erc-spelling--flyspell-input-p "31.1"))
+  ;; FIXME: Don't use `flyspell-word'!
+  (let ((word-data (and (boundp 'flyspell-word)
+                        flyspell-word)))
+    (when word-data
+      (cond ((< (point) erc-input-marker)
+             nil)
+            ;; don't spell-check names of users
+            ((and erc-channel-users
+                  (erc-get-channel-user (car word-data)))
+             (erc-spelling-unhighlight-word word-data)
+             nil)
+            ;; if '/' occurs before the word, don't spell-check it
+            ((eq (char-before (nth 1 word-data)) ?/)
+             (erc-spelling-unhighlight-word word-data)
+             nil)
+            (t t)))))
+
+;; Do this down here to avoid adding `with-suppressed-warnings'.
+(make-obsolete 'erc-spelling-unhighlight-word
+               "value from `flyspell-get-word' now unused" "31.1")
+
+(defun erc-spelling--flyspell-check (beg end _)
+  "Return non-nil and remove overlay if text between BEG and END is correct."
+  (or (and erc-channel-users
+           (erc-get-channel-user (buffer-substring-no-properties beg end))
+           (always (flyspell-unhighlight-at beg)))
+      (and erc-input-marker (> beg erc-input-marker) (eq (char-before beg) ?/)
+           (or (= beg (1+ erc-input-marker)) ; allow /unknown if initial
+               (erc-command-symbol (buffer-substring-no-properties beg end)))
+           (always (flyspell-unhighlight-at beg)))))
+
+(defun erc-spelling--flyspell-input-p ()
+  "Return non-nil if flyspell should check the prompt input at point."
+  (>= (point) erc-input-marker))
 
 (put 'erc-mode
      'flyspell-mode-predicate
-     #'erc-spelling-flyspell-verify)
+     #'erc-spelling--flyspell-input-p)
 
 (provide 'erc-spelling)
 
diff --git a/test/lisp/erc/erc-scenarios-spelling.el b/test/lisp/erc/erc-scenarios-spelling.el
new file mode 100644
index 00000000000..907fbd78de3
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-spelling.el
@@ -0,0 +1,95 @@
+;;; erc-scenarios-spelling.el --- Basic spelling scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-spelling)
+
+(ert-deftest erc-scenarios-spelling--auto-correct ()
+  :tags '(:expensive-test
+          ;; FIXME make contingent upon a feature test for an English
+          ;; ispell setup in the current environment.
+          :unstable
+          ,@(and (getenv "ERC_TESTS_GRAPHICAL") '(:erc--graphical)))
+
+  (ert-with-temp-directory erc-scenarios-spelling
+
+    (erc-scenarios-common-with-noninteractive-in-term
+        ((erc-scenarios-common-dialog "spelling")
+         (process-environment (cons
+                               (format "HOME=%s" erc-scenarios-spelling)
+                               process-environment))
+         (dumb-server (erc-d-run "localhost" t 'auto-correct))
+         (port (process-contact dumb-server :service))
+         (expect (erc-d-t-make-expecter))
+         (erc-autojoin-channels-alist '((foonet "#chan")))
+         (erc-modules (cons 'spelling erc-modules))
+         (erc-server-flood-penalty 0.1))
+
+      (ert-info ("Connect to foonet")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (funcall expect 10 "no longer marked as being")
+          (should erc-spelling-mode)
+          (should flyspell-mode)))
+
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (should erc-spelling-mode)
+        (should flyspell-mode)
+        (funcall expect 10 "<alice> tester, welcome!")
+
+        ;; Insert a command with one misspelled word.
+        (set-window-buffer nil (current-buffer))
+        (execute-kbd-macro "\M->/AMSG an/dor /gmsg one fsbot two frob my shoe")
+        (funcall expect 10 "shoe")
+
+        (let* ((ovs (overlays-in erc-input-marker (point)))
+               (ov1 (pop ovs))
+               (ov2 (pop ovs)))
+          ;; At this point, flyspell should have done its thing.  There
+          ;; should be two overlays: one on "dor" and the other on
+          ;; "frob".  The spelling module's modifications should have
+          ;; prevented the two valid slash commands as well as "fsbot"
+          ;; from being highlighted.
+          (should-not ovs)
+          (should (flyspell-overlay-p ov1))
+          (should (equal "dor" (buffer-substring (overlay-start ov1)
+                                                 (overlay-end ov1))))
+          (should (flyspell-overlay-p ov2))
+          (should (equal "frob" (buffer-substring (overlay-start ov2)
+                                                  (overlay-end ov2))))
+          (goto-char (overlay-start ov2))
+
+          ;; Depending on the machine, this should become something
+          ;; like: "/AMSG an/dor /gmsg one fsbot two Rob my shoe".
+          (execute-kbd-macro (key-parse "M-TAB"))
+          (should (equal (overlays-in erc-input-marker (point-max))
+                         (list ov1)))))
+
+      (when noninteractive
+        (erc-spelling-mode -1)))))
+
+;;; erc-scenarios-spelling.el ends here
diff --git a/test/lisp/erc/resources/spelling/auto-correct.eld b/test/lisp/erc/resources/spelling/auto-correct.eld
new file mode 100644
index 00000000000..0f00ee4825c
--- /dev/null
+++ b/test/lisp/erc/resources/spelling/auto-correct.eld
@@ -0,0 +1,35 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Wed, 05 May 2021 09:05:34 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 10 "JOIN #chan")
+ (0 ":[email-masked] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice fsbot @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list."))
+
+((mode-chan 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0 ":[email-masked] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email-masked] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email-masked] PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0 ":[email-masked] PRIVMSG #chan :bob: Nothing, but let him have thanks."))
-- 
2.47.1

>From 1329e0e05754bee70a8236d84d34d01ca42acea7 Mon Sep 17 00:00:00 2001
From: Trevor Arjeski <[email-masked]>
Date: Sat, 4 Jan 2025 08:45:29 +0300
Subject: [PATCH 1/2] [5.6.1] Improve use of flyspell's API in erc-spelling

* lisp/erc/erc-spelling.el: Change top-level assignment of `erc-mode'
symbol-property `flyspell-mode-predicate' from
`erc-spelling-flyspell-verify' to `erc-spelling--flyspell-input-p'.
(erc-spelling-mode, erc-spelling-disable): Remove local member from
`flyspell-incorrect-hook'.
(erc-spelling-init): Add `erc-spelling--flyspell-check' to
`flyspell-incorrect-hook' locally.  Don't bother explicitly setting
`flyspell-generic-check-word-predicate' because Flyspell already does
that for clients using the `flyspell-mode-predicte' interface.
(erc-spelling--flyspell-check, erc-spelling--flyspell-input-p): New
functions, essentially the two halves of a reworked and bifurcated
`erc-spelling-flyspell-verify'.  Though used as a predicate, the first
is not named as such because it performs side effects.  (Bug#75327)

Copyright-paperwork-exempt: yes
---
 lisp/erc/erc-spelling.el | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-spelling.el b/lisp/erc/erc-spelling.el
index 01e587af368..d6099d4991c 100644
--- a/lisp/erc/erc-spelling.el
+++ b/lisp/erc/erc-spelling.el
@@ -48,6 +48,7 @@ spelling
        (erc-spelling-init (current-buffer)))))
   ((remove-hook 'erc-connect-pre-hook #'erc-spelling-init)
    (dolist (buffer (erc-buffer-list))
+     (remove-hook 'flyspell-incorrect-hook #'erc-spelling--flyspell-check t)
      (with-current-buffer buffer (flyspell-mode 0)))))
 
 (defcustom erc-spelling-dictionaries nil
@@ -78,7 +79,7 @@ erc-spelling-init
               (if dicts
                   (cadr (car dicts))
                 (erc-with-server-buffer ispell-local-dictionary)))))
-    (setq flyspell-generic-check-word-predicate #'erc-spelling-flyspell-verify)
+    (add-hook 'flyspell-incorrect-hook #'erc-spelling--flyspell-check 20 t)
     (flyspell-mode 1)))
 
 (defun erc-spelling-unhighlight-word (word)
@@ -109,9 +110,23 @@ erc-spelling-flyspell-verify
              nil)
             (t t)))))
 
+(defun erc-spelling--flyspell-check (beg end _)
+  "Return non-nil and remove overlay if text between BEG and END is correct."
+  (or (and erc-channel-users
+           (erc-get-channel-user (buffer-substring-no-properties beg end))
+           (always (flyspell-unhighlight-at beg)))
+      (and erc-input-marker (> beg erc-input-marker) (eq (char-before beg) ?/)
+           (or (= beg (1+ erc-input-marker)) ; allow /unknown if initial
+               (erc-command-symbol (buffer-substring-no-properties beg end)))
+           (always (flyspell-unhighlight-at beg)))))
+
+(defun erc-spelling--flyspell-input-p ()
+  "Return non-nil if flyspell should check the prompt input at point."
+  (>= (point) erc-input-marker))
+
 (put 'erc-mode
      'flyspell-mode-predicate
-     #'erc-spelling-flyspell-verify)
+     #'erc-spelling--flyspell-input-p)
 
 (provide 'erc-spelling)
 
-- 
2.47.1

>From ce275256955f20b2932ae50b9019394c8758b970 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Sat, 4 Jan 2025 11:40:01 -0800
Subject: [PATCH 2/2] [5.6.1] Declare unused functions in erc-spelling obsolete

* etc/ERC-NEWS: Announce deprecation of `erc-spelling-flyspell-verify'
and `erc-spelling-unhighlight-word'.  A slight behavioral change not
worth mentioning is that previously, ERC arranged for Flyspell to ignore
any word immediately following a forward slash anywhere in the input,
even those for which the slash served as mere punctuation (a "stroke"),
as in "something/misspelt."  Now, Flyspell only unconditionally exempts
an initial slash-prepended word, like "tableflip" in "ERC> /tableflip",
and checks all others that follow against known slash commands.
* lisp/erc/erc-spelling.el (erc-spelling-flyspell-verify)
(erc-spelling-unhighlight-word): Mark obsolete.
* test/lisp/erc/erc-scenarios-spelling.el: New file.
* test/lisp/erc/resources/spelling/auto-correct.eld: New file.
(Bug#75327)
---
 etc/ERC-NEWS                                  |  5 +
 lisp/erc/erc-spelling.el                      |  5 +
 test/lisp/erc/erc-scenarios-spelling.el       | 95 +++++++++++++++++++
 .../erc/resources/spelling/auto-correct.eld   | 35 +++++++
 4 files changed, 140 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-spelling.el
 create mode 100644 test/lisp/erc/resources/spelling/auto-correct.eld

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index f79d45b94b6..d491c3e5132 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -96,6 +96,11 @@ Although this has always been the case, string values are now more
 likely to be seen because ERC no longer coerces service names to port
 numbers.
 
+*** The 'spelling' module makes better use of Flyspell's API.
+As a consequence, the library functions 'erc-spelling-flyspell-verify'
+and 'erc-spelling-unhighlight-word' are now unused and have been marked
+obsolete.
+
 
 * Changes in ERC 5.6
 
diff --git a/lisp/erc/erc-spelling.el b/lisp/erc/erc-spelling.el
index d6099d4991c..f0a45683f4d 100644
--- a/lisp/erc/erc-spelling.el
+++ b/lisp/erc/erc-spelling.el
@@ -93,6 +93,7 @@ erc-spelling-unhighlight-word
 
 (defun erc-spelling-flyspell-verify ()
   "Flyspell only the input line, nothing else."
+  (declare (obsolete erc-spelling--flyspell-input-p "31.1"))
   ;; FIXME: Don't use `flyspell-word'!
   (let ((word-data (and (boundp 'flyspell-word)
                         flyspell-word)))
@@ -110,6 +111,10 @@ erc-spelling-flyspell-verify
              nil)
             (t t)))))
 
+;; Do this down here to avoid adding `with-suppressed-warnings'.
+(make-obsolete 'erc-spelling-unhighlight-word
+               "value from `flyspell-get-word' now unused" "31.1")
+
 (defun erc-spelling--flyspell-check (beg end _)
   "Return non-nil and remove overlay if text between BEG and END is correct."
   (or (and erc-channel-users
diff --git a/test/lisp/erc/erc-scenarios-spelling.el b/test/lisp/erc/erc-scenarios-spelling.el
new file mode 100644
index 00000000000..907fbd78de3
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-spelling.el
@@ -0,0 +1,95 @@
+;;; erc-scenarios-spelling.el --- Basic spelling scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-spelling)
+
+(ert-deftest erc-scenarios-spelling--auto-correct ()
+  :tags '(:expensive-test
+          ;; FIXME make contingent upon a feature test for an English
+          ;; ispell setup in the current environment.
+          :unstable
+          ,@(and (getenv "ERC_TESTS_GRAPHICAL") '(:erc--graphical)))
+
+  (ert-with-temp-directory erc-scenarios-spelling
+
+    (erc-scenarios-common-with-noninteractive-in-term
+        ((erc-scenarios-common-dialog "spelling")
+         (process-environment (cons
+                               (format "HOME=%s" erc-scenarios-spelling)
+                               process-environment))
+         (dumb-server (erc-d-run "localhost" t 'auto-correct))
+         (port (process-contact dumb-server :service))
+         (expect (erc-d-t-make-expecter))
+         (erc-autojoin-channels-alist '((foonet "#chan")))
+         (erc-modules (cons 'spelling erc-modules))
+         (erc-server-flood-penalty 0.1))
+
+      (ert-info ("Connect to foonet")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (funcall expect 10 "no longer marked as being")
+          (should erc-spelling-mode)
+          (should flyspell-mode)))
+
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (should erc-spelling-mode)
+        (should flyspell-mode)
+        (funcall expect 10 "<alice> tester, welcome!")
+
+        ;; Insert a command with one misspelled word.
+        (set-window-buffer nil (current-buffer))
+        (execute-kbd-macro "\M->/AMSG an/dor /gmsg one fsbot two frob my shoe")
+        (funcall expect 10 "shoe")
+
+        (let* ((ovs (overlays-in erc-input-marker (point)))
+               (ov1 (pop ovs))
+               (ov2 (pop ovs)))
+          ;; At this point, flyspell should have done its thing.  There
+          ;; should be two overlays: one on "dor" and the other on
+          ;; "frob".  The spelling module's modifications should have
+          ;; prevented the two valid slash commands as well as "fsbot"
+          ;; from being highlighted.
+          (should-not ovs)
+          (should (flyspell-overlay-p ov1))
+          (should (equal "dor" (buffer-substring (overlay-start ov1)
+                                                 (overlay-end ov1))))
+          (should (flyspell-overlay-p ov2))
+          (should (equal "frob" (buffer-substring (overlay-start ov2)
+                                                  (overlay-end ov2))))
+          (goto-char (overlay-start ov2))
+
+          ;; Depending on the machine, this should become something
+          ;; like: "/AMSG an/dor /gmsg one fsbot two Rob my shoe".
+          (execute-kbd-macro (key-parse "M-TAB"))
+          (should (equal (overlays-in erc-input-marker (point-max))
+                         (list ov1)))))
+
+      (when noninteractive
+        (erc-spelling-mode -1)))))
+
+;;; erc-scenarios-spelling.el ends here
diff --git a/test/lisp/erc/resources/spelling/auto-correct.eld b/test/lisp/erc/resources/spelling/auto-correct.eld
new file mode 100644
index 00000000000..0f00ee4825c
--- /dev/null
+++ b/test/lisp/erc/resources/spelling/auto-correct.eld
@@ -0,0 +1,35 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Wed, 05 May 2021 09:05:34 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 10 "JOIN #chan")
+ (0 ":[email-masked] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice fsbot @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list."))
+
+((mode-chan 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0 ":[email-masked] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email-masked] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email-masked] PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0 ":[email-masked] PRIVMSG #chan :bob: Nothing, but let him have thanks."))
-- 
2.47.1