Re: bug#70840: 29.3; ERC 5.5.0.29.1: erc-kill-buffer-on-part kills server buffer

"J.P." <[email protected]> Thu, 16 May 2024 20:57:52 -0700
Newsgroups gmane.emacs.erc.general
Message-ID <[email protected]>
"J.P." <[email protected]> writes:

> "J.P." <[email protected]> writes:
>
>> There may well be other issues here I've not yet considered, so if you
>> can think of any, please share. Also, if you're up for taking this on
>> (not necessarily in the way I've described), please feel free to send a
>> patch. The same goes for anyone else out there. I can definitely help
>> with the leg work and test coverage if that makes things easier.
>> Otherwise, I may not be able to get to this in time for the next release
>> (not that there's any rush).
>
> I've gone ahead with a provisional patch (attached) that incorporates
> the points raised so far. Anyone interested, please take a look.

After thinking a bit more on this, I've flip-flopped entirely on the
notion of running `erc-part-hook' in `erc-kill-channel' and inhibiting
it in `erc-server-PART' with a would-be null buffer argument. Basically,
I've come to believe the current faulty behavior has been around too
long to justify such a change in the name of fixing a bug that's only
tangentially related. Instead, I think we should introduce a flag that
`erc-kill-channel-hook' members can use to detect when `erc-server-PART'
is killing a buffer on behalf of the option `erc-kill-buffer-on-part'.
Updated patch attached. Thanks.
0000-v1-v2.diff (text/x-patch, 11.4 KB)
From d4d766438cf2f059af20e695dd20a65dcf646c2e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 16 May 2024 20:33:06 -0700
Subject: [PATCH 0/1] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (1):
  [5.6] Don't kill server buffer with erc-kill-buffer-on-part

 etc/ERC-NEWS                                  |  8 ++
 lisp/erc/erc-backend.el                       |  9 +-
 lisp/erc/erc-log.el                           |  2 +-
 lisp/erc/erc.el                               | 32 ++++---
 .../erc/erc-scenarios-base-kill-on-part.el    | 95 +++++++++++++++++++
 5 files changed, 132 insertions(+), 14 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-base-kill-on-part.el

Interdiff:
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 9d5a26595be..9c536636a04 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -648,15 +648,13 @@ release lacks a similar solution for detecting "joinedness" directly,
 but users can turn to 'xor'-ing 'erc-default-target' and 'erc-target'
 as a makeshift kludge.
 
-*** The function 'erc-kill-channel' may run the hook 'erc-part-hook'.
-Previously, when a user killed a channel buffer, 'erc-part-hook' would
-run in the response handler upon receipt of the corresponding 'PART'
-response, meaning hook members would receive a nil channel-buffer
-argument.  ERC now runs this hook immediately when a user kills a
-channel, just before sending the 'PART' command to the server.  Those
-disrupted by this change should see the definition of 'erc-part-hook'
-for a temporary escape hatch.  Also, for clarity, the function
-'erc-kill-channel' has been renamed 'erc-part-channel-on-kill'.
+*** Killing on behalf of 'erc-kill-buffer-on-part' made detectable.
+Members of 'erc-kill-channel-hook' can use the variable
+'erc-killing-buffer-on-part-p' to detect when 'erc-server-PART' is
+killing a buffer on account of the option 'erc-kill-buffer-on-part'
+being non-nil.  Also, for clarity, the function 'erc-kill-channel',
+which normally emits a 'PART' when channel buffers are killed, has
+been renamed to 'erc-part-channel-on-kill'.
 
 *** Channel-mode handling has become stricter and more predictable.
 ERC has always processed channel modes using "standardized" letters
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 7e8e597da46..7d6a3499d06 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1891,9 +1891,7 @@ erc--server-determine-join-display-context
                            'PART ?n nick ?u login
                            ?h host ?c chnl ?r (or reason ""))
       (when (string= nick (erc-current-nick))
-        (defvar erc--run-part-hook-with-nil-buffer-p)
-        (when (or buffer erc--run-part-hook-with-nil-buffer-p)
-          (run-hook-with-args 'erc-part-hook buffer))
+        (run-hook-with-args 'erc-part-hook buffer)
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
@@ -1902,8 +1900,8 @@ erc--server-determine-join-display-context
         (erc-update-mode-line buffer)
         (defvar erc-kill-buffer-on-part)
         (when (and erc-kill-buffer-on-part buffer)
-          (defvar erc--inhibit-part-channel-on-kill-p)
-          (let ((erc--inhibit-part-channel-on-kill-p t))
+          (defvar erc-killing-buffer-on-part-p)
+          (let ((erc-killing-buffer-on-part-p t))
             (kill-buffer buffer)))))))
 
 (define-erc-response-handler (PING)
diff --git a/lisp/erc/erc-log.el b/lisp/erc/erc-log.el
index f0bd1b45e7c..019ac6c1612 100644
--- a/lisp/erc/erc-log.el
+++ b/lisp/erc/erc-log.el
@@ -290,10 +290,7 @@ erc-save-query-buffers
 
 (defun erc-conditional-save-buffer (buffer)
   "Save Query BUFFER if `erc-save-queries-on-quit' is t."
-  (when (and erc-save-buffer-on-part
-             ;; Don't run this from `erc-part-channel-on-kill' because
-             ;; `erc-save-buffer-in-logs' aready has it covered.
-             (not erc--inhibit-part-channel-on-kill-p))
+  (when (and buffer erc-save-buffer-on-part)
     (erc-save-buffer-in-logs buffer)))
 
 (defun erc-conditional-save-queries (process)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a1c94288e8d..c174ca4b30e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -470,22 +470,21 @@ erc-quit-hook
 
 (defcustom erc-part-hook nil
   "Hook run when processing a PART message directed at our nick.
-Called in the server buffer with one argument: the BUFFER being parted.
-Also runs via `erc-part-channel-on-kill' on `erc-kill-channel-hook' if
-killing BUFFER initiated the \"PART\".  Note that beginning with ERC
-5.6, this hook doesn't run upon receiving a \"PART\" from the server if
-BUFFER has already been killed.  See code near this option's definition
-for a possible escape hatch."
+Called in the server buffer with a single argument: the channel buffer
+being parted.  For historical reasons, the buffer argument may be nil if
+it's been killed or otherwise no longer exists.  This typically happens
+when the \"PART\" response being handled comes by way of a channel
+buffer being killed, which, by default, tells `erc-part-channel-on-kill'
+to emit a \"PART\".  Users needing to operate on a parted channel buffer
+before it's killed in this fashion should use `erc-kill-channel-hook'
+and condition their code on `erc-killing-buffer-on-part-p' being nil."
   :group 'erc-hooks
   :type 'hook)
 
-(defvar erc--run-part-hook-with-nil-buffer-p nil
-  "When non-nil, allow running `erc-part-hook' with a nil buffer argument.
-Moreover, forgo running the hook before sending a \"PART\" when the user
-kills a channel buffer.  This compat-oriented escape hatch exists for
-third-party code that expects to run unconditionally in the default
-response handler `erc-server-PART' when ERC receives a self-directed
-\"PART\" from the server.")
+(defvar erc-killing-buffer-on-part-p nil
+  "Non-nil when killing a target buffer while handling a \"PART\" response.
+Useful for preventing the redundant execution of code designed to run
+once when parting a channel.")
 
 (defcustom erc-kick-hook nil
   "Hook run when processing a KICK message directed at our nick.
@@ -1124,8 +1123,7 @@ erc-last-saved-position
 
 (defcustom erc-kill-buffer-on-part nil
   "Kill the channel buffer on PART.
-This variable should probably stay nil, as ERC can reuse buffers if
-you rejoin them later."
+Nil by default because ERC can reuse buffers later re-joined."
   :group 'erc-quit-and-part
   :type 'boolean)
 
@@ -9669,18 +9667,12 @@ erc-kill-server
     (setq erc-server-quitting t)
     (erc-server-send (format "QUIT :%s" (funcall erc-quit-reason nil)))))
 
-(defvar erc--inhibit-part-channel-on-kill-p nil)
 (define-obsolete-function-alias 'erc-kill-channel #'erc-part-channel-on-kill
   "30.1")
 (defun erc-part-channel-on-kill ()
-  "Run `erc-part-hook' and send a \"PART\" when killing a channel buffer."
-  (when (and (erc-server-process-alive)
-             (not erc--inhibit-part-channel-on-kill-p))
-    (unless erc--run-part-hook-with-nil-buffer-p
-      (let ((buffer (current-buffer))
-            (erc--inhibit-part-channel-on-kill-p t))
-        (erc-with-server-buffer
-          (run-hook-with-args 'erc-part-hook buffer))))
+  "Send a \"PART\" when killing a channel buffer."
+  (when (and (not erc-killing-buffer-on-part-p)
+             (erc-server-process-alive))
     (let ((tgt (erc-default-target)))
       (if tgt
          (erc-server-send (format "PART %s :%s" tgt
diff --git a/test/lisp/erc/erc-scenarios-base-kill-on-part.el b/test/lisp/erc/erc-scenarios-base-kill-on-part.el
index 109cf27b01a..0ca0b1ae054 100644
--- a/test/lisp/erc/erc-scenarios-base-kill-on-part.el
+++ b/test/lisp/erc/erc-scenarios-base-kill-on-part.el
@@ -25,7 +25,9 @@
     (require 'erc-scenarios-common)))
 
 ;; Assert channel buffer is killed when `erc-kill-buffer-on-part' is
-;; enabled and a user issues a /part.
+;; enabled and a user issues a /part.  Also assert that code in
+;; `erc-kill-channel-hook' can detect when `erc-response-PART' is
+;; killing a buffer on behalf of that option.
 (ert-deftest erc-scenarios-base-kill-on-part--enabled ()
   :tags '(:expensive-test)
   (should-not erc-kill-buffer-on-part)
@@ -38,6 +40,9 @@ erc-scenarios-base-kill-on-part--enabled
        (erc-kill-buffer-on-part t)
        (calls nil)
        (erc-part-hook (lambda (b) (push (buffer-name b) calls)))
+       (erc-kill-channel-hook
+        (cons (lambda () (push erc-killing-buffer-on-part-p calls))
+              erc-kill-channel-hook))
        (expect (erc-d-t-make-expecter)))
 
     (ert-info ("Connect to foonet")
@@ -53,7 +58,7 @@ erc-scenarios-base-kill-on-part--enabled
       (erc-scenarios-common-say "/part"))
 
     (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
-    (should (equal calls '("#chan")))))
+    (should (equal calls '(t "#chan")))))
 
 ;; When `erc-kill-buffer-on-part' is non-nil, and the parted buffer has
 ;; already been killed, don't kill the server buffer.  Bug#70840
@@ -67,10 +72,8 @@ erc-scenarios-base-kill-on-part--enabled/killed
        (dumb-server (erc-d-run "localhost" t 'foonet))
        (port (process-contact dumb-server :service))
        (erc-kill-buffer-on-part t)
-       (killingp nil)
        (calls nil)
-       (erc-part-hook (lambda (b) (push (cons (buffer-name b) killingp)
-                                        calls)))
+       (erc-part-hook (lambda (b) (push b calls)))
        (expect (erc-d-t-make-expecter)))
 
     (ert-info ("Connect to foonet")
@@ -83,50 +86,10 @@ erc-scenarios-base-kill-on-part--enabled/killed
 
     (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#chan"))
       (funcall expect 10 "<alice> bob: Whilst I can shake")
-      ;; There's a race here because the response handler may run
-      ;; before `killingp' is nil, but at least false negatives aren't
-      ;; possible.  Shadowing `erc-server-send-queue' temporarily
-      ;; would fix it, but that's too leaky for this style of test.
-      (setq killingp 'kill)
-      (kill-buffer)
-      (setq killingp nil))
+      (kill-buffer))
 
     (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
-    (should (equal calls '(("#chan" . kill))))
+    (erc-d-t-wait-for 10 (equal calls '(nil)))
     (erc-d-t-ensure-for 0.1 (get-buffer "foonet"))))
 
-(ert-deftest erc-scenarios-base-kill-on-part--enabled/killed/compat ()
-  :tags '(:expensive-test)
-  (should-not erc-kill-buffer-on-part)
-
-  (erc-scenarios-common-with-cleanup
-      ((erc-scenarios-common-dialog "base/reuse-buffers/channel")
-       (erc-server-flood-penalty 0.1)
-       (dumb-server (erc-d-run "localhost" t 'foonet))
-       (port (process-contact dumb-server :service))
-       (erc-kill-buffer-on-part t)
-       (erc--run-part-hook-with-nil-buffer-p t) ; option under test
-       (killingp nil)
-       (calls nil)
-       (erc-part-hook (lambda (b) (push (cons b killingp) calls)))
-       (expect (erc-d-t-make-expecter)))
-
-    (ert-info ("Connect to foonet")
-      (with-current-buffer (erc :server "127.0.0.1"
-                                :port port
-                                :nick "tester"
-                                :password "foonet:changeme"
-                                :full-name "tester")
-        (funcall expect 10 "This server is in debug mode")))
-
-    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#chan"))
-      (funcall expect 10 "<alice> bob: Whilst I can shake")
-      (setq killingp 'kill)
-      (kill-buffer)
-      (setq killingp nil))
-
-    (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
-    (erc-d-t-wait-for 20 (equal calls '((nil . nil))))
-    (should (get-buffer "foonet"))))
-
 ;;; erc-scenarios-base-kill-on-part.el ends here
-- 
2.45.0
0001-5.6-Don-t-kill-server-buffer-with-erc-kill-buffer-on.patch (text/x-patch, 11 KB)
From d4d766438cf2f059af20e695dd20a65dcf646c2e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 14 May 2024 21:09:55 -0700
Subject: [PATCH 1/1] [5.6] Don't kill server buffer with
 erc-kill-buffer-on-part

* etc/ERC-NEWS: Mention flag variable `erc-killing-buffer-on-part-p'
and renaming of function `erc-kill-channel'.
* lisp/erc/erc-backend.el (erc-server-PART): Only kill a buffer on
behalf of `erc-kill-buffer-on-part' when the buffer hasn't already
been killed, and bind `erc-killing-buffer-on-part-p' to t when doing
so.
* lisp/erc/erc-log.el (erc-conditional-save-buffer): Don't save logs
when the buffer parameter is nil because that causes the server buffer
to be saved out.
* lisp/erc/erc.el (erc-part-hook): Redo doc string.
(erc-killing-buffer-on-part-p): New variable, a flag to prevent
redundant execution of `erc-kill-channel-hook' members concerned with
parted channels.
(erc-kill-buffer-on-part): Tweak doc string.
(erc-kill-channel-hook): Use new name for `erc-kill-channel'.
(erc-kill-channel, erc-part-channel-on-kill): Rename former to latter,
and inhibit execution when `erc-killing-buffer-on-part-p' is non-nil.
* test/lisp/erc/erc-scenarios-base-kill-on-part.el: New file.
(Bug#70840)
---
 etc/ERC-NEWS                                  |  8 ++
 lisp/erc/erc-backend.el                       |  9 +-
 lisp/erc/erc-log.el                           |  2 +-
 lisp/erc/erc.el                               | 32 ++++---
 .../erc/erc-scenarios-base-kill-on-part.el    | 95 +++++++++++++++++++
 5 files changed, 132 insertions(+), 14 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-base-kill-on-part.el

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index b66ea6a7a02..9c536636a04 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -648,6 +648,14 @@ release lacks a similar solution for detecting "joinedness" directly,
 but users can turn to 'xor'-ing 'erc-default-target' and 'erc-target'
 as a makeshift kludge.
 
+*** Killing on behalf of 'erc-kill-buffer-on-part' made detectable.
+Members of 'erc-kill-channel-hook' can use the variable
+'erc-killing-buffer-on-part-p' to detect when 'erc-server-PART' is
+killing a buffer on account of the option 'erc-kill-buffer-on-part'
+being non-nil.  Also, for clarity, the function 'erc-kill-channel',
+which normally emits a 'PART' when channel buffers are killed, has
+been renamed to 'erc-part-channel-on-kill'.
+
 *** Channel-mode handling has become stricter and more predictable.
 ERC has always processed channel modes using "standardized" letters
 and popular status prefixes.  Starting with this release, ERC will
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index ab419d2b018..7d6a3499d06 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1883,6 +1883,9 @@ erc--server-determine-join-display-context
          (buffer (erc-get-buffer chnl proc)))
     (pcase-let ((`(,nick ,login ,host)
                  (erc-parse-user (erc-response.sender parsed))))
+      ;; When `buffer' is nil, `erc-remove-channel-member',
+      ;; `erc-remove-channel-users', and `erc-part-hook', don't run or
+      ;; do nothing, and the message is printed in the server buffer.
       (erc-remove-channel-member buffer nick)
       (erc-display-message parsed 'notice buffer
                            'PART ?n nick ?u login
@@ -1896,8 +1899,10 @@ erc--server-determine-join-display-context
           (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
         (defvar erc-kill-buffer-on-part)
-        (when erc-kill-buffer-on-part
-          (kill-buffer buffer))))))
+        (when (and erc-kill-buffer-on-part buffer)
+          (defvar erc-killing-buffer-on-part-p)
+          (let ((erc-killing-buffer-on-part-p t))
+            (kill-buffer buffer)))))))
 
 (define-erc-response-handler (PING)
   "Handle ping messages." nil
diff --git a/lisp/erc/erc-log.el b/lisp/erc/erc-log.el
index d5c56bcc2b3..019ac6c1612 100644
--- a/lisp/erc/erc-log.el
+++ b/lisp/erc/erc-log.el
@@ -290,7 +290,7 @@ erc-save-query-buffers
 
 (defun erc-conditional-save-buffer (buffer)
   "Save Query BUFFER if `erc-save-queries-on-quit' is t."
-  (when erc-save-buffer-on-part
+  (when (and buffer erc-save-buffer-on-part)
     (erc-save-buffer-in-logs buffer)))
 
 (defun erc-conditional-save-queries (process)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index c92fd42322a..c174ca4b30e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -470,13 +470,22 @@ erc-quit-hook
 
 (defcustom erc-part-hook nil
   "Hook run when processing a PART message directed at our nick.
-
-The hook receives one argument, the current BUFFER.
-See also `erc-server-QUIT-functions', `erc-quit-hook' and
-`erc-disconnected-hook'."
+Called in the server buffer with a single argument: the channel buffer
+being parted.  For historical reasons, the buffer argument may be nil if
+it's been killed or otherwise no longer exists.  This typically happens
+when the \"PART\" response being handled comes by way of a channel
+buffer being killed, which, by default, tells `erc-part-channel-on-kill'
+to emit a \"PART\".  Users needing to operate on a parted channel buffer
+before it's killed in this fashion should use `erc-kill-channel-hook'
+and condition their code on `erc-killing-buffer-on-part-p' being nil."
   :group 'erc-hooks
   :type 'hook)
 
+(defvar erc-killing-buffer-on-part-p nil
+  "Non-nil when killing a target buffer while handling a \"PART\" response.
+Useful for preventing the redundant execution of code designed to run
+once when parting a channel.")
+
 (defcustom erc-kick-hook nil
   "Hook run when processing a KICK message directed at our nick.
 
@@ -1114,8 +1123,7 @@ erc-last-saved-position
 
 (defcustom erc-kill-buffer-on-part nil
   "Kill the channel buffer on PART.
-This variable should probably stay nil, as ERC can reuse buffers if
-you rejoin them later."
+Nil by default because ERC can reuse buffers later re-joined."
   :group 'erc-quit-and-part
   :type 'boolean)
 
@@ -9598,7 +9606,7 @@ erc-kill-server-hook
   :type 'hook)
 
 (defcustom erc-kill-channel-hook
-  '(erc-kill-channel
+  '(erc-part-channel-on-kill
     erc-networks-shrink-ids-and-buffer-names
     erc-networks-rename-surviving-target-buffer)
   "Invoked whenever a channel-buffer is killed via `kill-buffer'."
@@ -9659,10 +9667,12 @@ erc-kill-server
     (setq erc-server-quitting t)
     (erc-server-send (format "QUIT :%s" (funcall erc-quit-reason nil)))))
 
-(defun erc-kill-channel ()
-  "Sends a PART command to the server when the channel buffer is killed.
-This function should be on `erc-kill-channel-hook'."
-  (when (erc-server-process-alive)
+(define-obsolete-function-alias 'erc-kill-channel #'erc-part-channel-on-kill
+  "30.1")
+(defun erc-part-channel-on-kill ()
+  "Send a \"PART\" when killing a channel buffer."
+  (when (and (not erc-killing-buffer-on-part-p)
+             (erc-server-process-alive))
     (let ((tgt (erc-default-target)))
       (if tgt
          (erc-server-send (format "PART %s :%s" tgt
diff --git a/test/lisp/erc/erc-scenarios-base-kill-on-part.el b/test/lisp/erc/erc-scenarios-base-kill-on-part.el
new file mode 100644
index 00000000000..0ca0b1ae054
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-base-kill-on-part.el
@@ -0,0 +1,95 @@
+;;; erc-scenarios-base-kill-on-part.el --- killing buffers on part -*- lexical-binding: t -*-
+
+;; Copyright (C) 2024 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)))
+
+;; Assert channel buffer is killed when `erc-kill-buffer-on-part' is
+;; enabled and a user issues a /part.  Also assert that code in
+;; `erc-kill-channel-hook' can detect when `erc-response-PART' is
+;; killing a buffer on behalf of that option.
+(ert-deftest erc-scenarios-base-kill-on-part--enabled ()
+  :tags '(:expensive-test)
+  (should-not erc-kill-buffer-on-part)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reuse-buffers/channel")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (erc-kill-buffer-on-part t)
+       (calls nil)
+       (erc-part-hook (lambda (b) (push (buffer-name b) calls)))
+       (erc-kill-channel-hook
+        (cons (lambda () (push erc-killing-buffer-on-part-p calls))
+              erc-kill-channel-hook))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> bob: Whilst I can shake")
+      (erc-scenarios-common-say "/part"))
+
+    (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
+    (should (equal calls '(t "#chan")))))
+
+;; When `erc-kill-buffer-on-part' is non-nil, and the parted buffer has
+;; already been killed, don't kill the server buffer.  Bug#70840
+(ert-deftest erc-scenarios-base-kill-on-part--enabled/killed ()
+  :tags '(:expensive-test)
+  (should-not erc-kill-buffer-on-part)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reuse-buffers/channel")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (erc-kill-buffer-on-part t)
+       (calls nil)
+       (erc-part-hook (lambda (b) (push b calls)))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> bob: Whilst I can shake")
+      (kill-buffer))
+
+    (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
+    (erc-d-t-wait-for 10 (equal calls '(nil)))
+    (erc-d-t-ensure-for 0.1 (get-buffer "foonet"))))
+
+;;; erc-scenarios-base-kill-on-part.el ends here
-- 
2.45.0